Skip to content

Commit

Permalink
Hoist Astro.globbed hoisted scripts in dev (#3930)
Browse files Browse the repository at this point in the history
* Hoist Astro.globbed hoisted scripts in dev

* Adds a changeset

* Increase the timeout for the HMR test

* Fix e2e tests

* Refactor test
  • Loading branch information
matthewp committed Jul 18, 2022
1 parent 92b48b1 commit 3acb9ec
Show file tree
Hide file tree
Showing 11 changed files with 276 additions and 189 deletions.
5 changes: 5 additions & 0 deletions .changeset/quiet-toys-type.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Include hoisted scripts inside Astro.glob in dev
2 changes: 1 addition & 1 deletion packages/astro/e2e/shared-component-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ export function prepareTestFactory(opts) {
original.replace('id="client-idle" {...someProps}', 'id="client-idle" count={5}')
);

await expect(count, 'count prop updated').toHaveText('5');
await expect(count, 'count prop updated').toHaveText('5', { timeout: 10000 });
await expect(counter, 'component styles persisted').toHaveCSS('display', 'grid');

// Edit the client:only component's slot text
Expand Down
79 changes: 13 additions & 66 deletions packages/astro/src/core/render/dev/css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,9 @@ import type * as vite from 'vite';

import path from 'path';
import { RuntimeMode } from '../../../@types/astro.js';
import { unwrapId, viteID } from '../../util.js';
import { viteID } from '../../util.js';
import { STYLE_EXTENSIONS } from '../util.js';

/**
* List of file extensions signalling we can (and should) SSR ahead-of-time
* See usage below
*/
const fileExtensionsToSSR = new Set(['.md']);
import { crawlGraph } from './vite.js';

/** Given a filePath URL, crawl Vite’s module graph to find all style imports. */
export async function getStylesForURL(
Expand All @@ -20,69 +15,21 @@ export async function getStylesForURL(
const importedCssUrls = new Set<string>();
const importedStylesMap = new Map<string, string>();

/** recursively crawl the module graph to get all style files imported by parent id */
async function crawlCSS(_id: string, isFile: boolean, scanned = new Set<string>()) {
const id = unwrapId(_id);
const importedModules = new Set<vite.ModuleNode>();
const moduleEntriesForId = isFile
? // If isFile = true, then you are at the root of your module import tree.
// The `id` arg is a filepath, so use `getModulesByFile()` to collect all
// nodes for that file. This is needed for advanced imports like Tailwind.
viteServer.moduleGraph.getModulesByFile(id) ?? new Set()
: // Otherwise, you are following an import in the module import tree.
// You are safe to use getModuleById() here because Vite has already
// resolved the correct `id` for you, by creating the import you followed here.
new Set([viteServer.moduleGraph.getModuleById(id)]);

// Collect all imported modules for the module(s).
for (const entry of moduleEntriesForId) {
// Handle this in case an module entries weren't found for ID
// This seems possible with some virtual IDs (ex: `astro:markdown/*.md`)
if (!entry) {
continue;
}
if (id === entry.id) {
scanned.add(id);
for (const importedModule of entry.importedModules) {
// some dynamically imported modules are *not* server rendered in time
// to only SSR modules that we can safely transform, we check against
// a list of file extensions based on our built-in vite plugins
if (importedModule.id) {
// use URL to strip special query params like "?content"
const { pathname } = new URL(`file://${importedModule.id}`);
if (fileExtensionsToSSR.has(path.extname(pathname))) {
await viteServer.ssrLoadModule(importedModule.id);
}
}
importedModules.add(importedModule);
}
}
}

// scan imported modules for CSS imports & add them to our collection.
// Then, crawl that file to follow and scan all deep imports as well.
for (const importedModule of importedModules) {
if (!importedModule.id || scanned.has(importedModule.id)) {
continue;
}
const ext = path.extname(importedModule.url).toLowerCase();
if (STYLE_EXTENSIONS.has(ext)) {
if (
mode === 'development' && // only inline in development
typeof importedModule.ssrModule?.default === 'string' // ignore JS module styles
) {
importedStylesMap.set(importedModule.url, importedModule.ssrModule.default);
} else {
// NOTE: We use the `url` property here. `id` would break Windows.
importedCssUrls.add(importedModule.url);
}
for await(const importedModule of crawlGraph(viteServer, viteID(filePath), true)) {
const ext = path.extname(importedModule.url).toLowerCase();
if (STYLE_EXTENSIONS.has(ext)) {
if (
mode === 'development' && // only inline in development
typeof importedModule.ssrModule?.default === 'string' // ignore JS module styles
) {
importedStylesMap.set(importedModule.url, importedModule.ssrModule.default);
} else {
// NOTE: We use the `url` property here. `id` would break Windows.
importedCssUrls.add(importedModule.url);
}
await crawlCSS(importedModule.id, false, scanned);
}
}

// Crawl your import graph for CSS files, populating `importedCssUrls` as a result.
await crawlCSS(viteID(filePath), true);
return {
urls: importedCssUrls,
stylesMap: importedStylesMap,
Expand Down
6 changes: 2 additions & 4 deletions packages/astro/src/core/render/dev/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ import { LogOptions } from '../../logger/core.js';
import { isBuildingToSSR, isPage } from '../../util.js';
import { render as coreRender } from '../core.js';
import { RouteCache } from '../route-cache.js';
import { createModuleScriptElementWithSrcSet } from '../ssr-element.js';
import { collectMdMetadata } from '../util.js';
import { getStylesForURL } from './css.js';
import { getScriptsForURL } from './scripts.js';
import { resolveClientDevPath } from './resolve.js';

export interface SSROptions {
Expand Down Expand Up @@ -108,9 +108,7 @@ export async function render(
viteServer,
} = ssrOpts;
// Add hoisted script tags
const scripts = createModuleScriptElementWithSrcSet(
mod.hasOwnProperty('$$metadata') ? Array.from(mod.$$metadata.hoistedScriptPaths()) : []
);
const scripts = await getScriptsForURL(filePath, astroConfig, viteServer);

// Inject HMR scripts
if (isPage(filePath, astroConfig) && mode === 'development') {
Expand Down
44 changes: 44 additions & 0 deletions packages/astro/src/core/render/dev/scripts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { AstroConfig, SSRElement } from '../../../@types/astro';
import type { ModuleInfo } from 'rollup';
import type { PluginMetadata as AstroPluginMetadata } from '../../../vite-plugin-astro/types';
import vite from 'vite';
import slash from 'slash';
import { viteID } from '../../util.js';
import { createModuleScriptElementWithSrc } from '../ssr-element.js';
import { crawlGraph } from './vite.js';
import { fileURLToPath } from 'url';

export async function getScriptsForURL(
filePath: URL,
astroConfig: AstroConfig,
viteServer: vite.ViteDevServer,
): Promise<Set<SSRElement>> {
const elements = new Set<SSRElement>();
const rootID = viteID(filePath);
let rootProjectFolder = slash(fileURLToPath(astroConfig.root));
const modInfo = viteServer.pluginContainer.getModuleInfo(rootID);
addHoistedScripts(elements, modInfo, rootProjectFolder);
for await(const moduleNode of crawlGraph(viteServer, rootID, true)) {
const id = moduleNode.id;
if(id) {
const info = viteServer.pluginContainer.getModuleInfo(id);
addHoistedScripts(elements, info, rootProjectFolder);
}
}

return elements;
}

function addHoistedScripts(set: Set<SSRElement>, info: ModuleInfo | null, rootProjectFolder: string) {
if(!info?.meta?.astro) {
return;
}

let id = info.id;
const astro = info?.meta?.astro as AstroPluginMetadata['astro'];
for(let i = 0; i < astro.scripts.length; i++) {
const scriptId = `${id}?astro&type=script&index=${i}&lang.ts`;
const element = createModuleScriptElementWithSrc(scriptId);
set.add(element);
}
}
68 changes: 68 additions & 0 deletions packages/astro/src/core/render/dev/vite.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import vite from 'vite';
import npath from 'path';
import { unwrapId } from '../../util.js';

/**
* List of file extensions signalling we can (and should) SSR ahead-of-time
* See usage below
*/
const fileExtensionsToSSR = new Set(['.astro', '.md']);

/** recursively crawl the module graph to get all style files imported by parent id */
export async function * crawlGraph(
viteServer: vite.ViteDevServer,
_id: string,
isFile: boolean,
scanned = new Set<string>()
) : AsyncGenerator<vite.ModuleNode, void, unknown> {
const id = unwrapId(_id);
const importedModules = new Set<vite.ModuleNode>();
const moduleEntriesForId = isFile
? // If isFile = true, then you are at the root of your module import tree.
// The `id` arg is a filepath, so use `getModulesByFile()` to collect all
// nodes for that file. This is needed for advanced imports like Tailwind.
viteServer.moduleGraph.getModulesByFile(id) ?? new Set()
: // Otherwise, you are following an import in the module import tree.
// You are safe to use getModuleById() here because Vite has already
// resolved the correct `id` for you, by creating the import you followed here.
new Set([viteServer.moduleGraph.getModuleById(id)]);

// Collect all imported modules for the module(s).
for (const entry of moduleEntriesForId) {
// Handle this in case an module entries weren't found for ID
// This seems possible with some virtual IDs (ex: `astro:markdown/*.md`)
if (!entry) {
continue;
}
if (id === entry.id) {
scanned.add(id);
for (const importedModule of entry.importedModules) {
// some dynamically imported modules are *not* server rendered in time
// to only SSR modules that we can safely transform, we check against
// a list of file extensions based on our built-in vite plugins
if (importedModule.id) {
// use URL to strip special query params like "?content"
const { pathname } = new URL(`file://${importedModule.id}`);
if (fileExtensionsToSSR.has(npath.extname(pathname))) {
const mod = viteServer.moduleGraph.getModuleById(importedModule.id);
if(!mod?.ssrModule) {
await viteServer.ssrLoadModule(importedModule.id);
}
}
}
importedModules.add(importedModule);
}
}
}

// scan imported modules for CSS imports & add them to our collection.
// Then, crawl that file to follow and scan all deep imports as well.
for (const importedModule of importedModules) {
if (!importedModule.id || scanned.has(importedModule.id)) {
continue;
}

yield importedModule;
yield * crawlGraph(viteServer, importedModule.id, false, scanned);
}
}
2 changes: 1 addition & 1 deletion packages/astro/src/runtime/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -825,7 +825,7 @@ export function renderHead(result: SSRResult): Promise<string> {
const scripts = Array.from(result.scripts)
.filter(uniqueElements)
.map((script, i) => {
return renderElement('script', script);
return renderElement('script', script, false);
});
const links = Array.from(result.links)
.filter(uniqueElements)
Expand Down
33 changes: 0 additions & 33 deletions packages/astro/src/runtime/server/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,39 +58,6 @@ export class Metadata {
return metadata?.componentExport || null;
}

*hoistedScriptPaths() {
for (const metadata of this.deepMetadata()) {
let i = 0,
pathname = metadata.mockURL.pathname;

while (i < metadata.hoisted.length) {
// Strip off the leading "/@fs" added during compilation.
yield `${pathname.replace('/@fs', '')}?astro&type=script&index=${i}&lang.ts`;
i++;
}
}
}

private *deepMetadata(): Generator<Metadata, void, unknown> {
// Yield self
yield this;
// Keep a Set of metadata objects so we only yield them out once.
const seen = new Set<Metadata>();
for (const { module: mod } of this.modules) {
if (typeof mod.$$metadata !== 'undefined') {
const md = mod.$$metadata as Metadata;
// Call children deepMetadata() which will yield the child metadata
// and any of its children metadatas
for (const childMetdata of md.deepMetadata()) {
if (!seen.has(childMetdata)) {
seen.add(childMetdata);
yield childMetdata;
}
}
}
}
}

private getComponentMetadata(Component: any): ComponentMetadata | null {
if (this.metadataCache.has(Component)) {
return this.metadataCache.get(Component)!;
Expand Down

0 comments on commit 3acb9ec

Please sign in to comment.