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

Refactor prerendering chunk handling #11245

Merged
merged 12 commits into from
Jun 17, 2024
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/three-boxes-sniff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"astro": patch
---

Refactors prerendering chunk handling to correctly remove unused code during the SSR runtime
6 changes: 6 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,12 @@ Any tests for `astro build` output should use the main `mocha` tests rather than

If a test needs to validate what happens on the page after it's loading in the browser, that's a perfect use for E2E dev server tests, i.e. to verify that hot-module reloading works in `astro dev` or that components were client hydrated and are interactive.

#### Creating tests

When creating new tests, it's best to reference other existing test files and replicate the same setup. Some other tips include:

- When re-using a fixture multiple times with different configurations, you should also configure unique `outDir`, `build.client`, and `build.server` values so the build output runtime isn't cached and shared by ESM between test runs.

### Other useful commands

```shell
Expand Down
6 changes: 6 additions & 0 deletions packages/astro/src/core/build/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,11 @@ export interface BuildInternals {
ssrSplitEntryChunks: Map<string, Rollup.OutputChunk>;
componentMetadata: SSRResult['componentMetadata'];
middlewareEntryPoint?: URL;

/**
* Chunks in the bundle that are only used in prerendering that we can delete later
*/
prerenderOnlyChunks: Rollup.OutputChunk[];
}

/**
Expand Down Expand Up @@ -151,6 +156,7 @@ export function createBuildInternals(): BuildInternals {
ssrSplitEntryChunks: new Map(),
entryPoints: new Map(),
cacheManifestUsed: false,
prerenderOnlyChunks: [],
};
}

Expand Down
2 changes: 0 additions & 2 deletions packages/astro/src/core/build/page-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ export async function collectPagesData(
moduleSpecifier: '',
styles: [],
hoistedScript: undefined,
hasSharedModules: false,
};

clearInterval(routeCollectionLogTimeout);
Expand All @@ -80,7 +79,6 @@ export async function collectPagesData(
moduleSpecifier: '',
styles: [],
hoistedScript: undefined,
hasSharedModules: false,
};
}

Expand Down
9 changes: 9 additions & 0 deletions packages/astro/src/core/build/plugins/plugin-chunks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ export function vitePluginChunks(): VitePlugin {
if (id.includes('astro/dist/runtime/server/')) {
return 'astro/server';
}
// Split the Astro runtime into a separate chunk for readability
if (id.includes('astro/dist/runtime')) {
return 'astro';
}
// Place `astro/env/setup` import in its own chunk to prevent Rollup's TLA bug
// https://github.com/rollup/rollup/issues/4708
if (id.includes('astro/dist/env/setup')) {
return 'astro/env-setup';
}
bluwy marked this conversation as resolved.
Show resolved Hide resolved
},
});
},
Expand Down
160 changes: 89 additions & 71 deletions packages/astro/src/core/build/plugins/plugin-prerender.ts
Original file line number Diff line number Diff line change
@@ -1,87 +1,105 @@
import path from 'node:path';
import type { Plugin as VitePlugin } from 'vite';
import type { Rollup, Plugin as VitePlugin } from 'vite';
import { getPrerenderMetadata } from '../../../prerender/metadata.js';
import type { BuildInternals } from '../internal.js';
import type { AstroBuildPlugin } from '../plugin.js';
import type { StaticBuildOptions } from '../types.js';
import { extendManualChunks } from './util.js';
import { ASTRO_PAGE_RESOLVED_MODULE_ID } from './plugin-pages.js';
import { getPagesFromVirtualModulePageName } from './util.js';

function vitePluginPrerender(opts: StaticBuildOptions, internals: BuildInternals): VitePlugin {
function vitePluginPrerender(internals: BuildInternals): VitePlugin {
return {
bluwy marked this conversation as resolved.
Show resolved Hide resolved
name: 'astro:rollup-plugin-prerender',

outputOptions(outputOptions) {
extendManualChunks(outputOptions, {
after(id, meta) {
// Split the Astro runtime into a separate chunk for readability
if (id.includes('astro/dist/runtime')) {
return 'astro';
}
const pageInfo = internals.pagesByViteID.get(id);
let hasSharedModules = false;
if (pageInfo) {
// prerendered pages should be split into their own chunk
// Important: this can't be in the `pages/` directory!
if (getPrerenderMetadata(meta.getModuleInfo(id)!)) {
const infoMeta = meta.getModuleInfo(id)!;
generateBundle(_, bundle) {
const moduleIds = this.getModuleIds();
for (const id of moduleIds) {
const pageInfo = internals.pagesByViteID.get(id);
if (!pageInfo) continue;
const moduleInfo = this.getModuleInfo(id);
if (!moduleInfo) continue;

// Here, we check if this page is importing modules that are shared among other modules e.g. middleware, other SSR pages, etc.
// we loop the modules that the current page imports
for (const moduleId of infoMeta.importedIds) {
// we retrieve the metadata of the module
const moduleMeta = meta.getModuleInfo(moduleId)!;
if (
// a shared modules should be inside the `src/` folder, at least
moduleMeta.id.startsWith(opts.settings.config.srcDir.pathname) &&
// and has at least two importers: the current page and something else
moduleMeta.importers.length > 1
) {
// Now, we have to trace back the modules imported and analyze them;
// understanding if a module is eventually shared between two pages isn't easy, because a module could
// be imported by a page and a component that is eventually imported by a page.
//
// Given the previous statement, we only check if
// - the module is a page, and it's not pre-rendered
// - the module is the middleware
// If one of these conditions is met, we need a separate chunk
for (const importer of moduleMeta.importedIds) {
// we don't want to analyze the same module again, so we skip it
if (importer !== id) {
const importerModuleMeta = meta.getModuleInfo(importer);
if (importerModuleMeta) {
// if the module is inside the pages
if (importerModuleMeta.id.includes('/pages')) {
// we check if it's not pre-rendered
if (getPrerenderMetadata(importerModuleMeta) === false) {
hasSharedModules = true;
break;
}
}
// module isn't an Astro route/page, it could be a middleware
else if (importerModuleMeta.id.includes('/middleware')) {
hasSharedModules = true;
break;
}
}
}
}
}
}
const prerender = !!getPrerenderMetadata(moduleInfo);
pageInfo.route.prerender = prerender;
}

pageInfo.hasSharedModules = hasSharedModules;
pageInfo.route.prerender = true;
return 'prerender';
}
pageInfo.route.prerender = false;
// dynamic pages should all go in their own chunk in the pages/* directory
return `pages/${path.basename(pageInfo.component)}`;
}
},
});
// Find all chunks used in the SSR runtime (that aren't used for prerendering only), then use
// the Set to find the inverse, where chunks that are only used for prerendering. It's faster
// to compute `internals.prerenderOnlyChunks` this way. The prerendered chunks will be deleted
// after we finish prerendering.
const nonPrerenderOnlyChunks = getNonPrerenderOnlyChunks(bundle, internals);
internals.prerenderOnlyChunks = Object.values(bundle).filter((chunk) => {
return chunk.type === 'chunk' && !nonPrerenderOnlyChunks.has(chunk);
}) as Rollup.OutputChunk[];
},
};
}

function getNonPrerenderOnlyChunks(bundle: Rollup.OutputBundle, internals: BuildInternals) {
const chunks = Object.values(bundle);

const prerenderOnlyEntryChunks = new Set<Rollup.OutputChunk>();
const nonPrerenderOnlyEntryChunks = new Set<Rollup.OutputChunk>();
for (const chunk of chunks) {
if (chunk.type === 'chunk' && (chunk.isEntry || chunk.isDynamicEntry)) {
// See if this entry chunk is prerendered, if so, skip it
if (chunk.facadeModuleId?.startsWith(ASTRO_PAGE_RESOLVED_MODULE_ID)) {
const pageDatas = getPagesFromVirtualModulePageName(
internals,
ASTRO_PAGE_RESOLVED_MODULE_ID,
chunk.facadeModuleId
);
const prerender = pageDatas.every((pageData) => pageData.route.prerender);
if (prerender) {
prerenderOnlyEntryChunks.add(chunk);
continue;
}
}
// Ideally we should record entries when `functionPerRoute` is enabled, but this breaks some tests
// that expect the entrypoint to still exist even if it should be unused.
// TODO: Revisit this so we can delete additional unused chunks
// else if (chunk.facadeModuleId?.startsWith(RESOLVED_SPLIT_MODULE_ID)) {
// const pageDatas = getPagesFromVirtualModulePageName(
// internals,
// RESOLVED_SPLIT_MODULE_ID,
// chunk.facadeModuleId
// );
// const prerender = pageDatas.every((pageData) => pageData.route.prerender);
// if (prerender) {
// prerenderOnlyEntryChunks.add(chunk);
// continue;
// }
// }

nonPrerenderOnlyEntryChunks.add(chunk);
}
}

// From the `nonPrerenderedEntryChunks`, we crawl all the imports/dynamicImports to find all
// other chunks that are use by the non-prerendered runtime
const nonPrerenderOnlyChunks = new Set(nonPrerenderOnlyEntryChunks);
for (const chunk of nonPrerenderOnlyChunks) {
for (const importFileName of chunk.imports) {
const importChunk = bundle[importFileName];
if (importChunk?.type === 'chunk') {
nonPrerenderOnlyChunks.add(importChunk);
}
}
for (const dynamicImportFileName of chunk.dynamicImports) {
const dynamicImportChunk = bundle[dynamicImportFileName];
// The main server entry (entry.mjs) may import a prerender-only entry chunk, we skip in this case
// to prevent incorrectly marking it as non-prerendered.
if (
dynamicImportChunk?.type === 'chunk' &&
!prerenderOnlyEntryChunks.has(dynamicImportChunk)
) {
nonPrerenderOnlyChunks.add(dynamicImportChunk);
}
}
}

return nonPrerenderOnlyChunks;
}

export function pluginPrerender(
opts: StaticBuildOptions,
internals: BuildInternals
Expand All @@ -96,7 +114,7 @@ export function pluginPrerender(
hooks: {
'build:before': () => {
return {
vitePlugin: vitePluginPrerender(opts, internals),
vitePlugin: vitePluginPrerender(internals),
};
},
},
Expand Down
31 changes: 18 additions & 13 deletions packages/astro/src/core/build/plugins/plugin-ssr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,17 @@ function vitePluginSSR(
name: '@astrojs/vite-plugin-astro-ssr-server',
enforce: 'post',
options(opts) {
return addRollupInput(opts, [SSR_VIRTUAL_MODULE_ID]);
const inputs = new Set<string>();

for (const pageData of Object.values(options.allPages)) {
if (routeIsRedirect(pageData.route)) {
continue;
}
inputs.add(getVirtualModulePageName(ASTRO_PAGE_MODULE_ID, pageData.component));
}

inputs.add(SSR_VIRTUAL_MODULE_ID);
return addRollupInput(opts, Array.from(inputs));
},
resolveId(id) {
if (id === SSR_VIRTUAL_MODULE_ID) {
Expand Down Expand Up @@ -72,7 +82,6 @@ function vitePluginSSR(
contents.push(...ssrCode.contents);
return [...imports, ...contents, ...exports].join('\n');
}
return void 0;
},
async generateBundle(_opts, bundle) {
// Add assets from this SSR chunk as well.
Expand Down Expand Up @@ -141,23 +150,20 @@ function vitePluginSSRSplit(
adapter: AstroAdapter,
options: StaticBuildOptions
): VitePlugin {
const functionPerRouteEnabled = isFunctionPerRouteEnabled(options.settings.adapter);
return {
name: '@astrojs/vite-plugin-astro-ssr-split',
enforce: 'post',
options(opts) {
if (functionPerRouteEnabled) {
bluwy marked this conversation as resolved.
Show resolved Hide resolved
const inputs = new Set<string>();
const inputs = new Set<string>();

for (const pageData of Object.values(options.allPages)) {
if (routeIsRedirect(pageData.route)) {
continue;
}
inputs.add(getVirtualModulePageName(SPLIT_MODULE_ID, pageData.component));
for (const pageData of Object.values(options.allPages)) {
if (routeIsRedirect(pageData.route)) {
continue;
}

return addRollupInput(opts, Array.from(inputs));
inputs.add(getVirtualModulePageName(SPLIT_MODULE_ID, pageData.component));
}

return addRollupInput(opts, Array.from(inputs));
},
resolveId(id) {
if (id.startsWith(SPLIT_MODULE_ID)) {
Expand Down Expand Up @@ -185,7 +191,6 @@ function vitePluginSSRSplit(

return [...imports, ...contents, ...exports].join('\n');
}
return void 0;
},
async generateBundle(_opts, bundle) {
// Add assets from this SSR chunk as well.
Expand Down
Loading
Loading