diff --git a/.changeset/three-boxes-sniff.md b/.changeset/three-boxes-sniff.md new file mode 100644 index 000000000000..50b99c08d242 --- /dev/null +++ b/.changeset/three-boxes-sniff.md @@ -0,0 +1,5 @@ +--- +"astro": patch +--- + +Refactors prerendering chunk handling to correctly remove unused code during the SSR runtime diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0fbff3dc0c23..6c8c34509106 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 diff --git a/packages/astro/src/core/build/internal.ts b/packages/astro/src/core/build/internal.ts index 615d366401ed..07ecf261d7a1 100644 --- a/packages/astro/src/core/build/internal.ts +++ b/packages/astro/src/core/build/internal.ts @@ -113,6 +113,11 @@ export interface BuildInternals { ssrSplitEntryChunks: Map; componentMetadata: SSRResult['componentMetadata']; middlewareEntryPoint?: URL; + + /** + * Chunks in the bundle that are only used in prerendering that we can delete later + */ + prerenderOnlyChunks: Rollup.OutputChunk[]; } /** @@ -151,6 +156,7 @@ export function createBuildInternals(): BuildInternals { ssrSplitEntryChunks: new Map(), entryPoints: new Map(), cacheManifestUsed: false, + prerenderOnlyChunks: [], }; } diff --git a/packages/astro/src/core/build/page-data.ts b/packages/astro/src/core/build/page-data.ts index 6358a6f55364..c4c1e180cd1b 100644 --- a/packages/astro/src/core/build/page-data.ts +++ b/packages/astro/src/core/build/page-data.ts @@ -57,7 +57,6 @@ export async function collectPagesData( moduleSpecifier: '', styles: [], hoistedScript: undefined, - hasSharedModules: false, }; clearInterval(routeCollectionLogTimeout); @@ -80,7 +79,6 @@ export async function collectPagesData( moduleSpecifier: '', styles: [], hoistedScript: undefined, - hasSharedModules: false, }; } diff --git a/packages/astro/src/core/build/plugins/plugin-chunks.ts b/packages/astro/src/core/build/plugins/plugin-chunks.ts index 30b3e4938d28..bcd1d15bf5ca 100644 --- a/packages/astro/src/core/build/plugins/plugin-chunks.ts +++ b/packages/astro/src/core/build/plugins/plugin-chunks.ts @@ -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'; + } }, }); }, diff --git a/packages/astro/src/core/build/plugins/plugin-prerender.ts b/packages/astro/src/core/build/plugins/plugin-prerender.ts index 171d998b7b9b..b7feb70e36a0 100644 --- a/packages/astro/src/core/build/plugins/plugin-prerender.ts +++ b/packages/astro/src/core/build/plugins/plugin-prerender.ts @@ -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 { 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(); + const nonPrerenderOnlyEntryChunks = new Set(); + 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 @@ -96,7 +114,7 @@ export function pluginPrerender( hooks: { 'build:before': () => { return { - vitePlugin: vitePluginPrerender(opts, internals), + vitePlugin: vitePluginPrerender(internals), }; }, }, diff --git a/packages/astro/src/core/build/plugins/plugin-ssr.ts b/packages/astro/src/core/build/plugins/plugin-ssr.ts index 0a10110cd217..880a4d6a8eb9 100644 --- a/packages/astro/src/core/build/plugins/plugin-ssr.ts +++ b/packages/astro/src/core/build/plugins/plugin-ssr.ts @@ -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(); + + 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) { @@ -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. @@ -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) { - const inputs = new Set(); + const inputs = new Set(); - 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)) { @@ -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. diff --git a/packages/astro/src/core/build/static-build.ts b/packages/astro/src/core/build/static-build.ts index 73f3a19c58e6..5b75dbc9354f 100644 --- a/packages/astro/src/core/build/static-build.ts +++ b/packages/astro/src/core/build/static-build.ts @@ -2,7 +2,6 @@ import fs from 'node:fs'; import path, { extname } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { teardown } from '@astrojs/compiler'; -import * as eslexer from 'es-module-lexer'; import glob from 'fast-glob'; import { bgGreen, bgMagenta, black, green } from 'kleur/colors'; import * as vite from 'vite'; @@ -156,7 +155,7 @@ export async function staticBuild( case isServerLikeOutput(settings.config): { settings.timer.start('Server generate'); await generatePages(opts, internals); - await cleanStaticOutput(opts, internals, ssrOutputChunkNames); + await cleanStaticOutput(opts, internals); opts.logger.info(null, `\n${bgMagenta(black(' finalizing server assets '))}\n`); await ssrMoveAssets(opts); settings.timer.end('Server generate'); @@ -199,6 +198,8 @@ async function ssrBuild( copyPublicDir: !ssr, rollupOptions: { ...viteConfig.build?.rollupOptions, + // Setting as `exports-only` allows us to safely delete inputs that are only used during prerendering + preserveEntrySignatures: 'exports-only', input: [], output: { hoistTransitiveImports: isContentCache, @@ -381,65 +382,35 @@ async function runPostBuildHooks( } /** - * For each statically prerendered page, replace their SSR file with a noop. - * This allows us to run the SSR build only once, but still remove dependencies for statically rendered routes. - * If a component is shared between a statically rendered route and a SSR route, it will still be included in the SSR build. + * Remove chunks that are used for prerendering only */ -async function cleanStaticOutput( - opts: StaticBuildOptions, - internals: BuildInternals, - ssrOutputChunkNames: string[] -) { - const prerenderedFiles = new Set(); - const onDemandsFiles = new Set(); - for (const pageData of internals.pagesByKeys.values()) { - const { moduleSpecifier } = pageData; - const bundleId = - internals.pageToBundleMap.get(moduleSpecifier) ?? - internals.entrySpecifierToBundleMap.get(moduleSpecifier); - if (pageData.route.prerender && !pageData.hasSharedModules && !onDemandsFiles.has(bundleId)) { - prerenderedFiles.add(bundleId); - } else { - onDemandsFiles.add(bundleId); - // Check if the component was not previously added to the static build by a statically rendered route - if (prerenderedFiles.has(bundleId)) { - prerenderedFiles.delete(bundleId); - } - } - } +async function cleanStaticOutput(opts: StaticBuildOptions, internals: BuildInternals) { const ssr = isServerLikeOutput(opts.settings.config); const out = ssr ? opts.settings.config.build.server : getOutDirWithinCwd(opts.settings.config.outDir); - // The SSR output chunks for Astro are all .mjs files - const files = ssrOutputChunkNames.filter((f) => f.endsWith('.mjs')); - - if (files.length) { - await eslexer.init; - - // Cleanup prerendered chunks. - // This has to happen AFTER the SSR build runs as a final step, because we need the code in order to generate the pages. - // These chunks should only contain prerendering logic, so they are safe to modify. - await Promise.all( - files.map(async (filename) => { - if (!prerenderedFiles.has(filename)) { - return; - } - const url = new URL(filename, out); - const text = await fs.promises.readFile(url, { encoding: 'utf8' }); - const [, exports] = eslexer.parse(text); - // Replace exports (only prerendered pages) with a noop - let value = 'const noop = () => {};'; - for (const e of exports) { - if (e.n === 'default') value += `\n export default noop;`; - else value += `\nexport const ${e.n} = noop;`; + await Promise.all( + internals.prerenderOnlyChunks.map(async (chunk) => { + const url = new URL(chunk.fileName, out); + try { + // Entry chunks may be referenced by non-deleted code, so we don't actually delete it + // but only empty its content. These chunks should never be executed in practice, but + // it should prevent broken import paths if adapters do a secondary bundle. + if (chunk.isEntry || chunk.isDynamicEntry) { + await fs.promises.writeFile( + url, + "// Contents removed by Astro as it's used for prerendering only", + 'utf-8' + ); + } else { + await fs.promises.unlink(url); } - await fs.promises.writeFile(url, value, { encoding: 'utf8' }); - }) - ); - - removeEmptyDirs(out); - } + } catch { + // Best-effort only. Sometimes some chunks may be deleted by other plugins, like pure CSS chunks, + // so they may already not exist. + } + }) + ); } async function cleanServerOutput( diff --git a/packages/astro/src/core/build/types.ts b/packages/astro/src/core/build/types.ts index 53c6dcb93350..b75b6415ccbd 100644 --- a/packages/astro/src/core/build/types.ts +++ b/packages/astro/src/core/build/types.ts @@ -29,7 +29,6 @@ export interface PageBuildData { moduleSpecifier: string; hoistedScript: HoistedScriptAsset | undefined; styles: Array<{ depth: number; order: number; sheet: StylesheetAsset }>; - hasSharedModules: boolean; } export type AllPagesData = Record; diff --git a/packages/astro/test/astro-assets-prefix.test.js b/packages/astro/test/astro-assets-prefix.test.js index 08af026c1543..4987c64e19ba 100644 --- a/packages/astro/test/astro-assets-prefix.test.js +++ b/packages/astro/test/astro-assets-prefix.test.js @@ -14,6 +14,11 @@ describe('Assets Prefix - Static', () => { before(async () => { fixture = await loadFixture({ root: './fixtures/astro-assets-prefix/', + outDir: './dist/static', + build: { + client: './dist/static/client', + server: './dist/static/server', + }, }); await fixture.build(); }); @@ -72,7 +77,10 @@ describe('Assets Prefix - with path prefix', () => { before(async () => { fixture = await loadFixture({ root: './fixtures/astro-assets-prefix/', + outDir: './dist/server', build: { + client: './dist/server/client', + server: './dist/server/server', assetsPrefix: '/starting-slash', }, }); @@ -97,6 +105,11 @@ describe('Assets Prefix, server', () => { root: './fixtures/astro-assets-prefix/', output: 'server', adapter: testAdapter(), + outDir: './dist/server', + build: { + client: './dist/server/client', + server: './dist/server/server', + }, }); await fixture.build(); app = await fixture.loadTestAdapterApp(); @@ -154,7 +167,10 @@ describe('Assets Prefix, with path prefix', () => { root: './fixtures/astro-assets-prefix/', output: 'server', adapter: testAdapter(), + outDir: './dist/server-path-prefix', build: { + client: './dist/server-path-prefix/client', + server: './dist/server-path-prefix/server', assetsPrefix: '/starting-slash', }, }); diff --git a/packages/astro/test/before-hydration.test.js b/packages/astro/test/before-hydration.test.js index d14b347bfe42..75acafa00e45 100644 --- a/packages/astro/test/before-hydration.test.js +++ b/packages/astro/test/before-hydration.test.js @@ -14,6 +14,11 @@ describe('Astro Scripts before-hydration', () => { before(async () => { fixture = await loadFixture({ root: './fixtures/before-hydration/', + outDir: './dist/static-integration', + build: { + client: './dist/static-integration/client', + server: './dist/static-integration/server', + }, integrations: [ preact(), { @@ -68,6 +73,11 @@ describe('Astro Scripts before-hydration', () => { before(async () => { fixture = await loadFixture({ root: './fixtures/before-hydration/', + outDir: './dist/static-no-integration', + build: { + client: './dist/static-no-integration/client', + server: './dist/static-no-integration/server', + }, }); }); @@ -115,6 +125,11 @@ describe('Astro Scripts before-hydration', () => { root: './fixtures/before-hydration/', output: 'server', adapter: testAdapter(), + outDir: './dist/server-integration', + build: { + client: './dist/server-integration/client', + server: './dist/server-integration/server', + }, integrations: [ preact(), { @@ -153,6 +168,11 @@ describe('Astro Scripts before-hydration', () => { fixture = await loadFixture({ root: './fixtures/before-hydration/', output: 'server', + outDir: './dist/static-no-integration', + build: { + client: './dist/static-no-integration/client', + server: './dist/static-no-integration/server', + }, adapter: testAdapter(), }); }); diff --git a/packages/astro/test/core-image.test.js b/packages/astro/test/core-image.test.js index a4fd13fcbdcb..bb4ac31279a1 100644 --- a/packages/astro/test/core-image.test.js +++ b/packages/astro/test/core-image.test.js @@ -799,6 +799,11 @@ describe('astro:image', () => { const fixtureWithBase = await loadFixture({ root: './fixtures/core-image-ssr/', output: 'server', + outDir: './dist/server-base-path', + build: { + client: './dist/server-base-path/client', + server: './dist/server-base-path/server', + }, adapter: testAdapter(), image: { service: testImageService(), @@ -1080,6 +1085,11 @@ describe('astro:image', () => { fixture = await loadFixture({ root: './fixtures/core-image-ssr/', output: 'server', + outDir: './dist/server-dev', + build: { + client: './dist/server-dev/client', + server: './dist/server-dev/server', + }, adapter: testAdapter(), base: 'some-base', image: { @@ -1114,6 +1124,11 @@ describe('astro:image', () => { fixture = await loadFixture({ root: './fixtures/core-image-ssr/', output: 'server', + outDir: './dist/server-prod', + build: { + client: './dist/server-prod/client', + server: './dist/server-prod/server', + }, adapter: testAdapter(), image: { endpoint: 'astro/assets/endpoint/node', @@ -1127,6 +1142,7 @@ describe('astro:image', () => { const app = await fixture.loadTestAdapterApp(); let request = new Request('http://example.com/'); let response = await app.render(request); + console.log assert.equal(response.status, 200); const html = await response.text(); const $ = cheerio.load(html); diff --git a/packages/astro/test/css-inline-stylesheets.test.js b/packages/astro/test/css-inline-stylesheets.test.js index d066f530a6d3..66a3da11e84a 100644 --- a/packages/astro/test/css-inline-stylesheets.test.js +++ b/packages/astro/test/css-inline-stylesheets.test.js @@ -15,7 +15,10 @@ describe('Setting inlineStylesheets to never in static output', () => { site: 'https://test.dev/', root: './fixtures/css-inline-stylesheets/', output: 'static', + outDir: './dist/static-inline-stylesheets-never', build: { + client: './dist/static-inline-stylesheets-never/client', + server: './dist/static-inline-stylesheets-never/server', inlineStylesheets: 'never', }, }); @@ -53,7 +56,10 @@ describe('Setting inlineStylesheets to never in server output', () => { root: './fixtures/css-inline-stylesheets/', output: 'server', adapter: testAdapter(), + outDir: './dist/server-inline-stylesheets-never', build: { + client: './dist/server-inline-stylesheets-never/client', + server: './dist/server-inline-stylesheets-never/server', inlineStylesheets: 'never', }, }); @@ -92,7 +98,10 @@ describe('Setting inlineStylesheets to auto in static output', () => { site: 'https://test.info/', root: './fixtures/css-inline-stylesheets/', output: 'static', + outDir: './dist/static-inline-stylesheets-auto', build: { + client: './dist/static-inline-stylesheets-auto/client', + server: './dist/static-inline-stylesheets-auto/server', inlineStylesheets: 'auto', }, vite: { @@ -137,7 +146,10 @@ describe('Setting inlineStylesheets to auto in server output', () => { root: './fixtures/css-inline-stylesheets/', output: 'server', adapter: testAdapter(), + outDir: './dist/server-inline-stylesheets-auto', build: { + client: './dist/server-inline-stylesheets-auto/client', + server: './dist/server-inline-stylesheets-auto/server', inlineStylesheets: 'auto', }, vite: { @@ -184,7 +196,10 @@ describe('Setting inlineStylesheets to always in static output', () => { site: 'https://test.net/', root: './fixtures/css-inline-stylesheets/', output: 'static', + outDir: './dist/static-inline-stylesheets-always', build: { + client: './dist/static-inline-stylesheets-always/client', + server: './dist/static-inline-stylesheets-always/server', inlineStylesheets: 'always', }, }); @@ -221,7 +236,10 @@ describe('Setting inlineStylesheets to always in server output', () => { root: './fixtures/css-inline-stylesheets/', output: 'server', adapter: testAdapter(), + outDir: './dist/server-inline-stylesheets-always', build: { + client: './dist/server-inline-stylesheets-always/client', + server: './dist/server-inline-stylesheets-always/server', inlineStylesheets: 'always', }, }); diff --git a/packages/astro/test/experimental-content-collections-css-inline-stylesheets.test.js b/packages/astro/test/experimental-content-collections-css-inline-stylesheets.test.js index d6c509de5b64..5c02c66b57d2 100644 --- a/packages/astro/test/experimental-content-collections-css-inline-stylesheets.test.js +++ b/packages/astro/test/experimental-content-collections-css-inline-stylesheets.test.js @@ -59,7 +59,10 @@ describe('Experimental Content Collections cache - inlineStylesheets to never in root: './fixtures/css-inline-stylesheets/', output: 'server', adapter: testAdapter(), + outDir: './dist/inline-stylesheets-never', build: { + client: './dist/inline-stylesheets-never/client', + server: './dist/inline-stylesheets-never/server', inlineStylesheets: 'never', }, experimental: { @@ -103,7 +106,10 @@ describe('Experimental Content Collections cache - inlineStylesheets to auto in site: 'https://test.info/', root: './fixtures/css-inline-stylesheets/', output: 'static', + outDir: './dist/inline-stylesheets-auto', build: { + client: './dist/inline-stylesheets-auto/client', + server: './dist/inline-stylesheets-auto/server', inlineStylesheets: 'auto', }, vite: { @@ -202,7 +208,10 @@ describe('Setting inlineStylesheets to always in server output', () => { root: './fixtures/css-inline-stylesheets/', output: 'server', adapter: testAdapter(), + outDir: './dist/inline-stylesheets-always', build: { + client: './dist/inline-stylesheets-always/client', + server: './dist/inline-stylesheets-always/server', inlineStylesheets: 'always', }, experimental: { diff --git a/packages/astro/test/fixtures/ssr-prerender-chunks/astro.config.mjs b/packages/astro/test/fixtures/ssr-prerender-chunks/astro.config.mjs new file mode 100644 index 000000000000..ad35a2317c6e --- /dev/null +++ b/packages/astro/test/fixtures/ssr-prerender-chunks/astro.config.mjs @@ -0,0 +1,10 @@ +import serverlessAdapter from '@test/ssr-prerender-chunks-test-adapter'; + import { defineConfig } from 'astro/config'; + import react from "@astrojs/react"; + + // https://astro.build/config + export default defineConfig({ + adapter: serverlessAdapter(), + output: 'server', + integrations: [react()] + }) \ No newline at end of file diff --git a/packages/astro/test/fixtures/ssr-prerender-chunks/deps/test-adapter/index.js b/packages/astro/test/fixtures/ssr-prerender-chunks/deps/test-adapter/index.js new file mode 100644 index 000000000000..82b7b64b1fc9 --- /dev/null +++ b/packages/astro/test/fixtures/ssr-prerender-chunks/deps/test-adapter/index.js @@ -0,0 +1,85 @@ +/** + * + * @returns {import('../src/@types/astro').AstroIntegration} + */ +export default function () { + return { + name: '@test/ssr-prerender-chunks-test-adapter', + hooks: { + 'astro:config:setup': ({ updateConfig, config }) => { + updateConfig({ + build: { + client: config.outDir, + server: new URL('./_worker.js/', config.outDir), + serverEntry: 'index.js', + redirects: false, + } + }); + }, + 'astro:config:done': ({ setAdapter }) => { + setAdapter({ + name: '@test/ssr-prerender-chunks-test-adapter', + serverEntrypoint: '@test/ssr-prerender-chunks-test-adapter/server.js', + exports: ['default'], + supportedAstroFeatures: { + serverOutput: 'stable', + }, + }); + }, + 'astro:build:setup': ({ vite, target }) => { + if (target === 'server') { + vite.resolve ||= {}; + vite.resolve.alias ||= {}; + + const aliases = [ + { + find: 'react-dom/server', + replacement: 'react-dom/server.browser', + }, + ]; + + if (Array.isArray(vite.resolve.alias)) { + vite.resolve.alias = [...vite.resolve.alias, ...aliases]; + } else { + for (const alias of aliases) { + (vite.resolve.alias)[alias.find] = alias.replacement; + } + } + + vite.resolve.conditions ||= []; + // We need those conditions, previous these conditions where applied at the esbuild step which we removed + // https://github.com/withastro/astro/pull/7092 + vite.resolve.conditions.push('workerd', 'worker'); + + vite.ssr ||= {}; + vite.ssr.target = 'webworker'; + vite.ssr.noExternal = true; + + vite.build ||= {}; + vite.build.rollupOptions ||= {}; + vite.build.rollupOptions.output ||= {}; + vite.build.rollupOptions.output.banner ||= + 'globalThis.process ??= {}; globalThis.process.env ??= {};'; + + // Cloudflare env is only available per request. This isn't feasible for code that access env vars + // in a global way, so we shim their access as `process.env.*`. This is not the recommended way for users to access environment variables. But we'll add this for compatibility for chosen variables. Mainly to support `@astrojs/db` + vite.define = { + 'process.env': 'process.env', + ...vite.define, + }; + } + // we thought that vite config inside `if (target === 'server')` would not apply for client + // but it seems like the same `vite` reference is used for both + // so we need to reset the previous conflicting setting + // in the future we should look into a more robust solution + if (target === 'client') { + vite.resolve ||= {}; + vite.resolve.conditions ||= []; + vite.resolve.conditions = vite.resolve.conditions.filter( + (c) => c !== 'workerd' && c !== 'worker' + ); + } + }, + }, + }; +} \ No newline at end of file diff --git a/packages/astro/test/fixtures/ssr-prerender-chunks/deps/test-adapter/package.json b/packages/astro/test/fixtures/ssr-prerender-chunks/deps/test-adapter/package.json new file mode 100644 index 000000000000..655ab8b54929 --- /dev/null +++ b/packages/astro/test/fixtures/ssr-prerender-chunks/deps/test-adapter/package.json @@ -0,0 +1,10 @@ +{ + "name": "@test/ssr-prerender-chunks-test-adapter", + "version": "0.0.0", + "private": true, + "type": "module", + "exports": { + ".": "./index.js", + "./server.js": "./server.js" + } +} diff --git a/packages/astro/test/fixtures/ssr-prerender-chunks/deps/test-adapter/server.js b/packages/astro/test/fixtures/ssr-prerender-chunks/deps/test-adapter/server.js new file mode 100644 index 000000000000..0921fe782245 --- /dev/null +++ b/packages/astro/test/fixtures/ssr-prerender-chunks/deps/test-adapter/server.js @@ -0,0 +1,65 @@ +import { App } from 'astro/app'; + + export function createExports(manifest) { + const app = new App(manifest); + + const fetch = async ( + request, + env, + context + ) => { + const { pathname } = new URL(request.url); + + // static assets fallback, in case default _routes.json is not used + if (manifest.assets.has(pathname)) { + return env.ASSETS.fetch(request.url.replace(/\.html$/, '')); + } + + const routeData = app.match(request); + if (!routeData) { + // https://developers.cloudflare.com/pages/functions/api-reference/#envassetsfetch + const asset = await env.ASSETS.fetch( + request.url.replace(/index.html$/, '').replace(/\.html$/, '') + ); + if (asset.status !== 404) { + return asset; + } + } + + Reflect.set( + request, + Symbol.for('astro.clientAddress'), + request.headers.get('cf-connecting-ip') + ); + + process.env.ASTRO_STUDIO_APP_TOKEN ??= (() => { + if (typeof env.ASTRO_STUDIO_APP_TOKEN === 'string') { + return env.ASTRO_STUDIO_APP_TOKEN; + } + })(); + + const locals = { + runtime: { + env: env, + cf: request.cf, + caches, + ctx: { + waitUntil: (promise) => context.waitUntil(promise), + passThroughOnException: () => context.passThroughOnException(), + }, + }, + }; + + const response = await app.render(request, { routeData, locals }); + + if (app.setCookieHeaders) { + for (const setCookieHeader of app.setCookieHeaders(response)) { + response.headers.append('Set-Cookie', setCookieHeader); + } + } + + return response; + }; + + return { default: { fetch } }; + } \ No newline at end of file diff --git a/packages/astro/test/fixtures/ssr-prerender-chunks/package.json b/packages/astro/test/fixtures/ssr-prerender-chunks/package.json new file mode 100644 index 000000000000..60896bdd67f9 --- /dev/null +++ b/packages/astro/test/fixtures/ssr-prerender-chunks/package.json @@ -0,0 +1,14 @@ +{ + "name": "@test/ssr-prerender-chunks", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/react": "workspace:*", + "@test/ssr-prerender-chunks-test-adapter": "link:./deps/test-adapter", + "@types/react": "^18.2.75", + "@types/react-dom": "^18.2.24", + "astro": "workspace:*", + "react": "^18.2.0", + "react-dom": "^18.2.0" + } +} diff --git a/packages/astro/test/fixtures/ssr-prerender-chunks/src/components/Counter.tsx b/packages/astro/test/fixtures/ssr-prerender-chunks/src/components/Counter.tsx new file mode 100644 index 000000000000..c9fdcc2d95d4 --- /dev/null +++ b/packages/astro/test/fixtures/ssr-prerender-chunks/src/components/Counter.tsx @@ -0,0 +1,26 @@ +import React, { useState } from "react"; + + const Counter: React.FC = () => { + const [count, setCount] = useState(0); + + const increment = () => { + setCount((prevCount) => prevCount + 1); + }; + + const decrement = () => { + setCount((prevCount) => prevCount - 1); + }; + + return ( +
+

Counter

+
+ + {count} + +
+
+ ); + }; + + export default Counter; \ No newline at end of file diff --git a/packages/astro/test/fixtures/ssr-prerender-chunks/src/pages/index.astro b/packages/astro/test/fixtures/ssr-prerender-chunks/src/pages/index.astro new file mode 100644 index 000000000000..21a503211bc0 --- /dev/null +++ b/packages/astro/test/fixtures/ssr-prerender-chunks/src/pages/index.astro @@ -0,0 +1,13 @@ +--- + export const prerender = true; + import Counter from "../components/Counter"; + --- + + + + Static Page + + + + + \ No newline at end of file diff --git a/packages/astro/test/fixtures/ssr-prerender-chunks/tsconfig.json b/packages/astro/test/fixtures/ssr-prerender-chunks/tsconfig.json new file mode 100644 index 000000000000..7fb90fafc062 --- /dev/null +++ b/packages/astro/test/fixtures/ssr-prerender-chunks/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "astro/tsconfigs/base", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react" + } +} \ No newline at end of file diff --git a/packages/astro/test/i18n-routing.test.js b/packages/astro/test/i18n-routing.test.js index 09581c899732..df8083b81c1a 100644 --- a/packages/astro/test/i18n-routing.test.js +++ b/packages/astro/test/i18n-routing.test.js @@ -1336,6 +1336,11 @@ describe('[SSR] i18n routing', () => { fixture = await loadFixture({ root: './fixtures/i18n-routing-prefix-always/', output: 'server', + outDir: './dist/pathname-prefix-always-no-redirect', + build: { + client: './dist/pathname-prefix-always-no-redirect/client', + server: './dist/pathname-prefix-always-no-redirect/server', + }, adapter: testAdapter(), i18n: { routing: { @@ -1622,6 +1627,11 @@ describe('[SSR] i18n routing', () => { fixture = await loadFixture({ root: './fixtures/i18n-routing/', output: 'server', + outDir: './dist/locales-underscore', + build: { + client: './dist/locales-underscore/client', + server: './dist/locales-underscore/server', + }, adapter: testAdapter(), i18n: { defaultLocale: 'en', @@ -1891,6 +1901,11 @@ describe('SSR fallback from missing locale index to default locale index', () => fixture = await loadFixture({ root: './fixtures/i18n-routing-prefix-other-locales/', output: 'server', + outDir: './dist/missing-locale-to-default', + build: { + client: './dist/missing-locale-to-default/client', + server: './dist/missing-locale-to-default/server', + }, adapter: testAdapter(), i18n: { defaultLocale: 'en', diff --git a/packages/astro/test/ssr-hoisted-script.test.js b/packages/astro/test/ssr-hoisted-script.test.js index 41bae1ef502c..70852f1db189 100644 --- a/packages/astro/test/ssr-hoisted-script.test.js +++ b/packages/astro/test/ssr-hoisted-script.test.js @@ -25,7 +25,14 @@ describe('Hoisted inline scripts in SSR', () => { describe('without base path', () => { before(async () => { - fixture = await loadFixture(defaultFixtureOptions); + fixture = await loadFixture({ + ...defaultFixtureOptions, + outDir: './dist/inline-scripts-without-base-path', + build: { + client: './dist/inline-scripts-without-base-path/client', + server: './dist/inline-scripts-without-base-path/server', + }, + }); await fixture.build(); }); @@ -42,6 +49,11 @@ describe('Hoisted inline scripts in SSR', () => { before(async () => { fixture = await loadFixture({ ...defaultFixtureOptions, + outDir: './dist/inline-scripts-with-base-path', + build: { + client: './dist/inline-scripts-with-base-path/client', + server: './dist/inline-scripts-with-base-path/server', + }, base, }); await fixture.build(); @@ -63,6 +75,11 @@ describe('Hoisted external scripts in SSR', () => { before(async () => { fixture = await loadFixture({ ...defaultFixtureOptions, + outDir: './dist/external-scripts-without-base-path', + build: { + client: './dist/external-scripts-without-base-path/client', + server: './dist/external-scripts-without-base-path/server', + }, vite: { build: { assetsInlineLimit: 0, @@ -83,6 +100,11 @@ describe('Hoisted external scripts in SSR', () => { before(async () => { fixture = await loadFixture({ ...defaultFixtureOptions, + outDir: './dist/external-scripts-with-base-path', + build: { + client: './dist/external-scripts-with-base-path/client', + server: './dist/external-scripts-with-base-path/server', + }, vite: { build: { assetsInlineLimit: 0, @@ -104,14 +126,17 @@ describe('Hoisted external scripts in SSR', () => { before(async () => { fixture = await loadFixture({ ...defaultFixtureOptions, + outDir: './dist/with-assets-prefix', + build: { + client: './dist/with-assets-prefix/client', + server: './dist/with-assets-prefix/server', + assetsPrefix: 'https://cdn.example.com', + }, vite: { build: { assetsInlineLimit: 0, }, }, - build: { - assetsPrefix: 'https://cdn.example.com', - }, }); await fixture.build(); }); @@ -130,6 +155,11 @@ describe('Hoisted external scripts in SSR', () => { before(async () => { fixture = await loadFixture({ ...defaultFixtureOptions, + outDir: './dist/with-rollup-output-file-names', + build: { + client: './dist/with-rollup-output-file-names/client', + server: './dist/with-rollup-output-file-names/server', + }, vite: { build: { assetsInlineLimit: 0, @@ -157,6 +187,11 @@ describe('Hoisted external scripts in SSR', () => { before(async () => { fixture = await loadFixture({ ...defaultFixtureOptions, + outDir: './dist/with-rollup-output-file-names-and-base', + build: { + client: './dist/with-rollup-output-file-names-and-base/client', + server: './dist/with-rollup-output-file-names-and-base/server', + }, vite: { build: { assetsInlineLimit: 0, @@ -185,6 +220,12 @@ describe('Hoisted external scripts in SSR', () => { before(async () => { fixture = await loadFixture({ ...defaultFixtureOptions, + outDir: './dist/with-rollup-output-file-names-and-assets-prefix', + build: { + client: './dist/with-rollup-output-file-names-and-assets-prefix/client', + server: './dist/with-rollup-output-file-names-and-assets-prefix/server', + assetsPrefix: 'https://cdn.example.com', + }, vite: { build: { assetsInlineLimit: 0, @@ -197,9 +238,6 @@ describe('Hoisted external scripts in SSR', () => { }, }, }, - build: { - assetsPrefix: 'https://cdn.example.com', - }, }); await fixture.build(); }); diff --git a/packages/astro/test/ssr-prerender-chunks.test.js b/packages/astro/test/ssr-prerender-chunks.test.js new file mode 100644 index 000000000000..7bd916814938 --- /dev/null +++ b/packages/astro/test/ssr-prerender-chunks.test.js @@ -0,0 +1,21 @@ +import assert from 'node:assert/strict'; +import { before, describe, it } from 'node:test'; +import { loadFixture } from './test-utils.js'; + +describe('Chunks', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/ssr-prerender-chunks/', + }); + await fixture.build(); + }); + + it('does not have wrong chunks', async () => { + const content = await fixture.readFile('_worker.js/renderers.mjs'); + const hasImportFromPrerender = !content.includes(`React } from './chunks/prerender`); + assert.ok(hasImportFromPrerender); + }); +}); diff --git a/packages/astro/test/ssr-prerender.test.js b/packages/astro/test/ssr-prerender.test.js index 2bf53cefafb2..a1620d752d60 100644 --- a/packages/astro/test/ssr-prerender.test.js +++ b/packages/astro/test/ssr-prerender.test.js @@ -12,6 +12,11 @@ describe('SSR: prerender', () => { fixture = await loadFixture({ root: './fixtures/ssr-prerender/', output: 'server', + outDir: './dist/normal', + build: { + client: './dist/normal/client', + server: './dist/normal/server', + }, adapter: testAdapter(), }); await fixture.build(); @@ -61,7 +66,11 @@ describe('SSR: prerender', () => { }); }); -describe('Integrations can hook into the prerendering decision', () => { +// NOTE: This test doesn't make sense as it relies on the fact that on the client build, +// you can change the prerender state of pages from the SSR build, however, the client build +// is not always guaranteed to run. If we want to support this feature, we may want to only allow +// editing `route.prerender` on the `astro:build:done` hook. +describe.skip('Integrations can hook into the prerendering decision', () => { /** @type {import('./test-utils').Fixture} */ let fixture; @@ -83,6 +92,11 @@ describe('Integrations can hook into the prerendering decision', () => { fixture = await loadFixture({ root: './fixtures/ssr-prerender/', output: 'server', + outDir: './dist/integration-prerender', + build: { + client: './dist/integration-prerender/client', + server: './dist/integration-prerender/server', + }, integrations: [testIntegration], adapter: testAdapter(), }); diff --git a/packages/astro/test/test-utils.js b/packages/astro/test/test-utils.js index 65968a0bce09..a4a4e22f8556 100644 --- a/packages/astro/test/test-utils.js +++ b/packages/astro/test/test-utils.js @@ -216,6 +216,10 @@ export async function loadFixture(inlineConfig) { }); } }, + loadAdapterEntryModule: async () => { + const url = new URL(`./server/entry.mjs?id=${fixtureId}`, config.outDir); + return await import(url); + }, loadNodeAdapterHandler: async () => { const url = new URL(`./server/entry.mjs?id=${fixtureId}`, config.outDir); const { handler } = await import(url); diff --git a/packages/integrations/node/test/node-middleware.test.js b/packages/integrations/node/test/node-middleware.test.js index fa496e871f46..d1b016a51350 100644 --- a/packages/integrations/node/test/node-middleware.test.js +++ b/packages/integrations/node/test/node-middleware.test.js @@ -9,13 +9,6 @@ import { loadFixture, waitServerListen } from './test-utils.js'; * @typedef {import('../../../astro/test/test-utils').Fixture} Fixture */ -async function load() { - const mod = await import( - `./fixtures/node-middleware/dist/server/entry.mjs?dropcache=${Date.now()}` - ); - return mod; -} - describe('behavior from middleware, standalone', () => { /** @type {import('./test-utils').Fixture} */ let fixture; @@ -29,7 +22,7 @@ describe('behavior from middleware, standalone', () => { adapter: nodejs({ mode: 'standalone' }), }); await fixture.build(); - const { startServer } = await load(); + const { startServer } = await fixture.loadAdapterEntryModule(); let res = startServer(); server = res.server; await waitServerListen(server.server); @@ -69,7 +62,7 @@ describe('behavior from middleware, middleware', () => { adapter: nodejs({ mode: 'middleware' }), }); await fixture.build(); - const { handler } = await load(); + const { handler } = await fixture.loadAdapterEntryModule(); const app = express(); app.use(handler); server = app.listen(8888); diff --git a/packages/integrations/node/test/prerender-404-500.test.js b/packages/integrations/node/test/prerender-404-500.test.js index afa23a151025..2535fcb35bf7 100644 --- a/packages/integrations/node/test/prerender-404-500.test.js +++ b/packages/integrations/node/test/prerender-404-500.test.js @@ -8,13 +8,6 @@ import { loadFixture, waitServerListen } from './test-utils.js'; * @typedef {import('../../../astro/test/test-utils').Fixture} Fixture */ -async function load() { - const mod = await import( - `./fixtures/prerender-404-500/dist/server/entry.mjs?dropcache=${Date.now()}` - ); - return mod; -} - describe('Prerender 404', () => { /** @type {import('./test-utils').Fixture} */ let fixture; @@ -32,10 +25,15 @@ describe('Prerender 404', () => { base: '/some-base', root: './fixtures/prerender-404-500/', output: 'server', + outDir: './dist/server-with-base', + build: { + client: './dist/server-with-base/client', + server: './dist/server-with-base/server', + }, adapter: nodejs({ mode: 'standalone' }), }); await fixture.build(); - const { startServer } = await load(); + const { startServer } = await fixture.loadAdapterEntryModule(); let res = startServer(); server = res.server; await waitServerListen(server.server); @@ -117,10 +115,15 @@ describe('Prerender 404', () => { site: 'https://test.info/', root: './fixtures/prerender-404-500/', output: 'server', + outDir: './dist/server-without-base', + build: { + client: './dist/server-without-base/client', + server: './dist/server-without-base/server', + }, adapter: nodejs({ mode: 'standalone' }), }); await fixture.build(); - const { startServer } = await load(); + const { startServer } = await fixture.loadAdapterEntryModule(); let res = startServer(); server = res.server; await waitServerListen(server.server); @@ -181,10 +184,15 @@ describe('Hybrid 404', () => { base: '/some-base', root: './fixtures/prerender-404-500/', output: 'hybrid', + outDir: './dist/hybrid-with-base', + build: { + client: './dist/hybrid-with-base/client', + server: './dist/hybrid-with-base/server', + }, adapter: nodejs({ mode: 'standalone' }), }); await fixture.build(); - const { startServer } = await load(); + const { startServer } = await fixture.loadAdapterEntryModule(); let res = startServer(); server = res.server; await waitServerListen(server.server); @@ -238,10 +246,15 @@ describe('Hybrid 404', () => { site: 'https://test.net/', root: './fixtures/prerender-404-500/', output: 'hybrid', + outDir: './dist/hybrid-without-base', + build: { + client: './dist/hybrid-without-base/client', + server: './dist/hybrid-without-base/server', + }, adapter: nodejs({ mode: 'standalone' }), }); await fixture.build(); - const { startServer } = await load(); + const { startServer } = await fixture.loadAdapterEntryModule(); let res = startServer(); server = res.server; await waitServerListen(server.server); diff --git a/packages/integrations/node/test/prerender.test.js b/packages/integrations/node/test/prerender.test.js index 29080981aba8..d856d9d3e752 100644 --- a/packages/integrations/node/test/prerender.test.js +++ b/packages/integrations/node/test/prerender.test.js @@ -8,10 +8,6 @@ import { loadFixture, waitServerListen } from './test-utils.js'; * @typedef {import('../../../astro/test/test-utils').Fixture} Fixture */ -async function load() { - const mod = await import(`./fixtures/prerender/dist/server/entry.mjs?dropcache=${Date.now()}`); - return mod; -} describe('Prerendering', () => { /** @type {import('./test-utils').Fixture} */ let fixture; @@ -25,10 +21,15 @@ describe('Prerendering', () => { base: '/some-base', root: './fixtures/prerender/', output: 'server', + outDir: './dist/with-base', + build: { + client: './dist/with-base/client', + server: './dist/with-base/server', + }, adapter: nodejs({ mode: 'standalone' }), }); await fixture.build(); - const { startServer } = await load(); + const { startServer } = await fixture.loadAdapterEntryModule(); let res = startServer(); server = res.server; await waitServerListen(server.server); @@ -94,10 +95,15 @@ describe('Prerendering', () => { fixture = await loadFixture({ root: './fixtures/prerender/', output: 'server', + outDir: './dist/without-base', + build: { + client: './dist/without-base/client', + server: './dist/without-base/server', + }, adapter: nodejs({ mode: 'standalone' }), }); await fixture.build(); - const { startServer } = await await load(); + const { startServer } = await fixture.loadAdapterEntryModule(); let res = startServer(); server = res.server; await waitServerListen(server.server); @@ -155,6 +161,11 @@ describe('Prerendering', () => { fixture = await loadFixture({ root: './fixtures/prerender/', output: 'server', + outDir: './dist/dev', + build: { + client: './dist/dev/client', + server: './dist/dev/server', + }, adapter: nodejs({ mode: 'standalone' }), }); devServer = await fixture.startDevServer(); @@ -197,10 +208,15 @@ describe('Hybrid rendering', () => { base: '/some-base', root: './fixtures/prerender/', output: 'hybrid', + outDir: './dist/hybrid-with-base', + build: { + client: './dist/hybrid-with-base/client', + server: './dist/hybrid-with-base/server', + }, adapter: nodejs({ mode: 'standalone' }), }); await fixture.build(); - const { startServer } = await load(); + const { startServer } = await fixture.loadAdapterEntryModule(); let res = startServer(); server = res.server; await waitServerListen(server.server); @@ -264,10 +280,15 @@ describe('Hybrid rendering', () => { fixture = await loadFixture({ root: './fixtures/prerender/', output: 'hybrid', + outDir: './dist/hybrid-without-base', + build: { + client: './dist/hybrid-without-base/client', + server: './dist/hybrid-without-base/server', + }, adapter: nodejs({ mode: 'standalone' }), }); await fixture.build(); - const { startServer } = await load(); + const { startServer } = await fixture.loadAdapterEntryModule(); let res = startServer(); server = res.server; await waitServerListen(server.server); @@ -323,10 +344,15 @@ describe('Hybrid rendering', () => { fixture = await loadFixture({ root: './fixtures/prerender/', output: 'hybrid', + outDir: './dist/hybrid-shared-modules', + build: { + client: './dist/hybrid-shared-modules/client', + server: './dist/hybrid-shared-modules/server', + }, adapter: nodejs({ mode: 'standalone' }), }); await fixture.build(); - const { startServer } = await load(); + const { startServer } = await fixture.loadAdapterEntryModule(); let res = startServer(); server = res.server; await waitServerListen(server.server); diff --git a/packages/integrations/node/test/trailing-slash.test.js b/packages/integrations/node/test/trailing-slash.test.js index ad91a275949e..9ea8fcdddc39 100644 --- a/packages/integrations/node/test/trailing-slash.test.js +++ b/packages/integrations/node/test/trailing-slash.test.js @@ -8,13 +8,6 @@ import { loadFixture, waitServerListen } from './test-utils.js'; * @typedef {import('../../../astro/test/test-utils').Fixture} Fixture */ -async function load() { - const mod = await import( - `./fixtures/trailing-slash/dist/server/entry.mjs?dropcache=${Date.now()}` - ); - return mod; -} - describe('Trailing slash', () => { /** @type {import('./test-utils').Fixture} */ let fixture; @@ -30,10 +23,15 @@ describe('Trailing slash', () => { base: '/some-base', output: 'hybrid', trailingSlash: 'always', + outDir: './dist/always-with-base', + build: { + client: './dist/always-with-base/client', + server: './dist/always-with-base/server', + }, adapter: nodejs({ mode: 'standalone' }), }); await fixture.build(); - const { startServer } = await load(); + const { startServer } = await fixture.loadAdapterEntryModule(); let res = startServer(); server = res.server; await waitServerListen(server.server); @@ -96,10 +94,15 @@ describe('Trailing slash', () => { root: './fixtures/trailing-slash/', output: 'hybrid', trailingSlash: 'always', + outDir: './dist/always-without-base', + build: { + client: './dist/always-without-base/client', + server: './dist/always-without-base/server', + }, adapter: nodejs({ mode: 'standalone' }), }); await fixture.build(); - const { startServer } = await load(); + const { startServer } = await fixture.loadAdapterEntryModule(); let res = startServer(); server = res.server; await waitServerListen(server.server); @@ -165,10 +168,15 @@ describe('Trailing slash', () => { base: '/some-base', output: 'hybrid', trailingSlash: 'never', + outDir: './dist/never-with-base', + build: { + client: './dist/never-with-base/client', + server: './dist/never-with-base/server', + }, adapter: nodejs({ mode: 'standalone' }), }); await fixture.build(); - const { startServer } = await load(); + const { startServer } = await fixture.loadAdapterEntryModule(); let res = startServer(); server = res.server; await waitServerListen(server.server); @@ -224,10 +232,15 @@ describe('Trailing slash', () => { root: './fixtures/trailing-slash/', output: 'hybrid', trailingSlash: 'never', + outDir: './dist/never-without-base', + build: { + client: './dist/never-without-base/client', + server: './dist/never-without-base/server', + }, adapter: nodejs({ mode: 'standalone' }), }); await fixture.build(); - const { startServer } = await load(); + const { startServer } = await fixture.loadAdapterEntryModule(); let res = startServer(); server = res.server; await waitServerListen(server.server); @@ -286,10 +299,15 @@ describe('Trailing slash', () => { base: '/some-base', output: 'hybrid', trailingSlash: 'ignore', + outDir: './dist/ignore-with-base', + build: { + client: './dist/ignore-with-base/client', + server: './dist/ignore-with-base/server', + }, adapter: nodejs({ mode: 'standalone' }), }); await fixture.build(); - const { startServer } = await load(); + const { startServer } = await fixture.loadAdapterEntryModule(); let res = startServer(); server = res.server; await waitServerListen(server.server); @@ -363,10 +381,15 @@ describe('Trailing slash', () => { root: './fixtures/trailing-slash/', output: 'hybrid', trailingSlash: 'ignore', + outDir: './dist/ignore-without-base', + build: { + client: './dist/ignore-without-base/client', + server: './dist/ignore-without-base/server', + }, adapter: nodejs({ mode: 'standalone' }), }); await fixture.build(); - const { startServer } = await load(); + const { startServer } = await fixture.loadAdapterEntryModule(); let res = startServer(); server = res.server; await waitServerListen(server.server); diff --git a/packages/integrations/vercel/test/serverless-prerender.test.js b/packages/integrations/vercel/test/serverless-prerender.test.js index 537eef77cd79..5b75418c9fd0 100644 --- a/packages/integrations/vercel/test/serverless-prerender.test.js +++ b/packages/integrations/vercel/test/serverless-prerender.test.js @@ -20,7 +20,7 @@ describe('Serverless prerender', () => { it('outDir is tree-shaken if not needed', async () => { const [file] = await fixture.glob( - '../.vercel/output/functions/_render.func/packages/integrations/vercel/test/fixtures/serverless-prerender/.vercel/output/_functions/chunks/pages/generic_*.mjs' + '../.vercel/output/functions/_render.func/packages/integrations/vercel/test/fixtures/serverless-prerender/.vercel/output/_functions/pages/_image.astro.mjs' ); const contents = await fixture.readFile(file); assert.ok(!contents.includes('const outDir ='), "outDir is tree-shaken if it's not imported"); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fea9345a6096..45074d26a9c0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3775,6 +3775,32 @@ importers: specifier: workspace:* version: link:../../.. + packages/astro/test/fixtures/ssr-prerender-chunks: + dependencies: + '@astrojs/react': + specifier: workspace:* + version: link:../../../../integrations/react + '@test/ssr-prerender-chunks-test-adapter': + specifier: link:./deps/test-adapter + version: link:deps/test-adapter + '@types/react': + specifier: ^18.2.75 + version: 18.3.3 + '@types/react-dom': + specifier: ^18.2.24 + version: 18.3.0 + astro: + specifier: workspace:* + version: link:../../.. + react: + specifier: ^18.2.0 + version: 18.3.1 + react-dom: + specifier: ^18.2.0 + version: 18.3.1(react@18.3.1) + + packages/astro/test/fixtures/ssr-prerender-chunks/deps/test-adapter: {} + packages/astro/test/fixtures/ssr-prerender-get-static-paths: dependencies: astro: