Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hoist Astro.globbed hoisted scripts in dev #3930

Merged
merged 5 commits into from
Jul 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -824,7 +824,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
Loading