From 8b041bf57c76830c4070330270521e05d8e58474 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Thu, 25 May 2023 14:28:40 +0100 Subject: [PATCH] refactor: emit pages as physical entry points (#7193) --- .changeset/dry-taxis-suffer.md | 6 + packages/astro/src/@types/astro.ts | 2 +- packages/astro/src/core/app/index.ts | 8 +- packages/astro/src/core/app/types.ts | 3 +- packages/astro/src/core/build/generate.ts | 32 ++-- packages/astro/src/core/build/graph.ts | 6 +- packages/astro/src/core/build/internal.ts | 23 ++- .../astro/src/core/build/plugins/README.md | 144 ++++++++++++++++++ .../core/build/plugins/plugin-middleware.ts | 20 +-- .../src/core/build/plugins/plugin-pages.ts | 79 +++++++--- .../src/core/build/plugins/plugin-ssr.ts | 71 ++++++--- packages/astro/src/core/build/static-build.ts | 46 +++++- packages/astro/src/core/build/types.ts | 4 +- 13 files changed, 352 insertions(+), 92 deletions(-) create mode 100644 .changeset/dry-taxis-suffer.md create mode 100644 packages/astro/src/core/build/plugins/README.md diff --git a/.changeset/dry-taxis-suffer.md b/.changeset/dry-taxis-suffer.md new file mode 100644 index 000000000000..b0cb68b24f98 --- /dev/null +++ b/.changeset/dry-taxis-suffer.md @@ -0,0 +1,6 @@ +--- +'astro': patch +--- + +Refactor how pages are emitted during the internal bundling. Now each +page is emitted as a separate entry point. diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index c7b1c4f596f1..c494cf12759a 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -1691,7 +1691,7 @@ export interface APIContext = Record { * context.locals.greeting = "Hello!"; - * next(); + * return next(); * }); * ``` * Inside a `.astro` file: diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts index 1e2dd1d24164..90e17f438cc6 100644 --- a/packages/astro/src/core/app/index.ts +++ b/packages/astro/src/core/app/index.ts @@ -32,8 +32,6 @@ export { deserializeManifest } from './common.js'; const clientLocalsSymbol = Symbol.for('astro.locals'); -export const pagesVirtualModuleId = '@astrojs-pages-virtual-entry'; -export const resolvedPagesVirtualModuleId = '\0' + pagesVirtualModuleId; const responseSentSymbol = Symbol.for('astro.responseSent'); export interface MatchOptions { @@ -139,7 +137,8 @@ export class App { defaultStatus = 404; } - let mod = await this.#manifest.pageMap.get(routeData.component)!(); + let page = await this.#manifest.pageMap.get(routeData.component)!(); + let mod = await page.page(); if (routeData.type === 'page') { let response = await this.#renderPage(request, routeData, mod, defaultStatus); @@ -148,7 +147,8 @@ export class App { if (response.status === 500 || response.status === 404) { const errorPageData = matchRoute('/' + response.status, this.#manifestData); if (errorPageData && errorPageData.route !== routeData.route) { - mod = await this.#manifest.pageMap.get(errorPageData.component)!(); + page = await this.#manifest.pageMap.get(errorPageData.component)!(); + mod = await page.page(); try { let errorResponse = await this.#renderPage( request, diff --git a/packages/astro/src/core/app/types.ts b/packages/astro/src/core/app/types.ts index 3747e96e3247..0fa2c034bd1e 100644 --- a/packages/astro/src/core/app/types.ts +++ b/packages/astro/src/core/app/types.ts @@ -8,6 +8,7 @@ import type { SSRLoadedRenderer, SSRResult, } from '../../@types/astro'; +import type { SinglePageBuiltModule } from '../build/types'; export type ComponentPath = string; @@ -31,7 +32,7 @@ export interface RouteInfo { export type SerializedRouteInfo = Omit & { routeData: SerializedRouteData; }; -type ImportComponentInstance = () => Promise; +type ImportComponentInstance = () => Promise; export interface SSRManifest { adapterName: string; diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index 5a9f075c4c8e..3c24aa4bca7a 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -20,7 +20,11 @@ import { generateImage as generateImageInternal, getStaticImageList, } from '../../assets/generate.js'; -import { hasPrerenderedPages, type BuildInternals } from '../../core/build/internal.js'; +import { + hasPrerenderedPages, + type BuildInternals, + eachPageDataFromEntryPoint, +} from '../../core/build/internal.js'; import { prependForwardSlash, removeLeadingForwardSlash, @@ -47,11 +51,12 @@ import { getOutDirWithinCwd, getOutFile, getOutFolder } from './common.js'; import { cssOrder, eachPageData, getPageDataByComponent, mergeInlineCss } from './internal.js'; import type { PageBuildData, - SingleFileBuiltModule, + SinglePageBuiltModule, StaticBuildOptions, StylesheetAsset, } from './types'; import { getTimeStat } from './util.js'; +import { ASTRO_PAGE_MODULE_ID } from './plugins/plugin-pages'; function shouldSkipDraft(pageModule: ComponentInstance, settings: AstroSettings): boolean { return ( @@ -99,18 +104,23 @@ export async function generatePages(opts: StaticBuildOptions, internals: BuildIn const verb = ssr ? 'prerendering' : 'generating'; info(opts.logging, null, `\n${bgGreen(black(` ${verb} static routes `))}`); - const ssrEntryURL = new URL('./' + serverEntry + `?time=${Date.now()}`, outFolder); - const ssrEntry = await import(ssrEntryURL.toString()); const builtPaths = new Set(); if (ssr) { - for (const pageData of eachPageData(internals)) { - if (pageData.route.prerender) - await generatePage(opts, internals, pageData, ssrEntry, builtPaths); + for (const [pageData, filePath] of eachPageDataFromEntryPoint(internals)) { + if (pageData.route.prerender) { + const ssrEntryURLPage = new URL('./' + filePath + `?time=${Date.now()}`, outFolder); + const ssrEntryPage = await import(ssrEntryURLPage.toString()); + + await generatePage(opts, internals, pageData, ssrEntryPage, builtPaths); + } } } else { - for (const pageData of eachPageData(internals)) { - await generatePage(opts, internals, pageData, ssrEntry, builtPaths); + for (const [pageData, filePath] of eachPageDataFromEntryPoint(internals)) { + const ssrEntryURLPage = new URL('./' + filePath + `?time=${Date.now()}`, outFolder); + const ssrEntryPage = await import(ssrEntryURLPage.toString()); + + await generatePage(opts, internals, pageData, ssrEntryPage, builtPaths); } } @@ -153,7 +163,7 @@ async function generatePage( opts: StaticBuildOptions, internals: BuildInternals, pageData: PageBuildData, - ssrEntry: SingleFileBuiltModule, + ssrEntry: SinglePageBuiltModule, builtPaths: Set ) { let timeStart = performance.now(); @@ -169,7 +179,7 @@ async function generatePage( .map(({ sheet }) => sheet) .reduce(mergeInlineCss, []); - const pageModulePromise = ssrEntry.pageMap?.get(pageData.component); + const pageModulePromise = ssrEntry.page; const middleware = ssrEntry.middleware; if (!pageModulePromise) { diff --git a/packages/astro/src/core/build/graph.ts b/packages/astro/src/core/build/graph.ts index 68d264b10304..3ce325309f40 100644 --- a/packages/astro/src/core/build/graph.ts +++ b/packages/astro/src/core/build/graph.ts @@ -1,6 +1,6 @@ import type { GetModuleInfo, ModuleInfo } from 'rollup'; -import { resolvedPagesVirtualModuleId } from '../app/index.js'; +import { ASTRO_PAGE_RESOLVED_MODULE_ID } from './plugins/plugin-pages.js'; // This walks up the dependency graph and yields out each ModuleInfo object. export function* walkParentInfos( @@ -43,8 +43,8 @@ export function* walkParentInfos( // it is imported by the top-level virtual module. export function moduleIsTopLevelPage(info: ModuleInfo): boolean { return ( - info.importers[0] === resolvedPagesVirtualModuleId || - info.dynamicImporters[0] == resolvedPagesVirtualModuleId + info.importers[0]?.includes(ASTRO_PAGE_RESOLVED_MODULE_ID) || + info.dynamicImporters[0]?.includes(ASTRO_PAGE_RESOLVED_MODULE_ID) ); } diff --git a/packages/astro/src/core/build/internal.ts b/packages/astro/src/core/build/internal.ts index 1d69849c90c8..f6025238a3f8 100644 --- a/packages/astro/src/core/build/internal.ts +++ b/packages/astro/src/core/build/internal.ts @@ -1,10 +1,10 @@ import type { Rollup } from 'vite'; import type { PageBuildData, StylesheetAsset, ViteID } from './types'; - import type { SSRResult } from '../../@types/astro'; import type { PageOptions } from '../../vite-plugin-astro/types'; import { prependForwardSlash, removeFileExtension } from '../path.js'; import { viteID } from '../util.js'; +import { ASTRO_PAGE_EXTENSION_POST_PATTERN, ASTRO_PAGE_MODULE_ID } from './plugins/plugin-pages.js'; export interface BuildInternals { /** @@ -97,7 +97,6 @@ export function createBuildInternals(): BuildInternals { hoistedScriptIdToPagesMap, entrySpecifierToBundleMap: new Map(), pageToBundleMap: new Map(), - pagesByComponent: new Map(), pageOptionsByPage: new Map(), pagesByViteID: new Map(), @@ -215,6 +214,26 @@ export function* eachPageData(internals: BuildInternals) { yield* internals.pagesByComponent.values(); } +export function* eachPageDataFromEntryPoint( + internals: BuildInternals +): Generator<[PageBuildData, string]> { + for (const [entryPoint, filePath] of internals.entrySpecifierToBundleMap) { + if (entryPoint.includes(ASTRO_PAGE_MODULE_ID)) { + const [, pageName] = entryPoint.split(':'); + const pageData = internals.pagesByComponent.get( + `${pageName.replace(ASTRO_PAGE_EXTENSION_POST_PATTERN, '.')}` + ); + if (!pageData) { + throw new Error( + "Build failed. Astro couldn't find the emitted page from " + pageName + ' pattern' + ); + } + + yield [pageData, filePath]; + } + } +} + export function hasPrerenderedPages(internals: BuildInternals) { for (const pageData of eachPageData(internals)) { if (pageData.route.prerender) { diff --git a/packages/astro/src/core/build/plugins/README.md b/packages/astro/src/core/build/plugins/README.md new file mode 100644 index 000000000000..32ac8c448f6f --- /dev/null +++ b/packages/astro/src/core/build/plugins/README.md @@ -0,0 +1,144 @@ +# Plugin directory (WIP) + +This file serves as developer documentation to explain how the internal plugins work + + +## `plugin-middleware` + +This plugin is responsible to retrieve the `src/middleware.{ts.js}` file and emit an entry point during the SSR build. + +The final file is emitted only if the user has the middleware file. The final name of the file is `middleware.mjs`. + +This is **not** a virtual module. The plugin will try to resolve the physical file. + +## `plugin-renderers` + +This plugin is responsible to collect all the renderers inside an Astro application and emit them in a single file. + +The emitted file is called `renderers.mjs`. + +The emitted file has content similar to: + +```js +const renderers = [Object.assign({"name":"astro:jsx","serverEntrypoint":"astro/jsx/server.js","jsxImportSource":"astro"}, { ssr: server_default }),]; + +export { renderers }; +``` + +## `plugin-pages` + +This plugin is responsible to collect all pages inside an Astro application, and emit a single entry point file for each page. + +This plugin **will emit code** only when building a static site. + +In order to achieve that, the plugin emits these pages as **virtual modules**. Doing so allows us to bypass: +- rollup resolution of the files +- possible plugins that get triggered when the name of the module has an extension e.g. `.astro` + +The plugin does the following operations: +- loop through all the pages and collects their paths; +- with each path, we create a new [string](#plugin-pages-mapping-resolution) that will serve and virtual module for that particular page +- when resolving the page, we check if the `id` of the module starts with `@astro-page` +- once the module is resolved, we emit [the code of the module](#plugin-pages-code-generation) + + +### `plugin pages` mapping resolution + +The mapping is as follows: + +``` +src/pages/index.astro => @astro-page:src/pages/index@_@astro +``` + +1. We add a fixed prefix, which is used as virtual module naming convention; +2. We replace the dot that belongs extension with an arbitrary string. + +This kind of patterns will then allow us to retrieve the path physical path of the +file back from that string. This is important for the [code generation](#plugin-pages-code-generation) + + + +### `plugin pages` code generation + +When generating the code of the page, we will import and export the following modules: +- the `renderers.mjs` +- the `middleware.mjs` +- the page, via dynamic import + +The emitted code of each entry point will look like this: + +```js +export { renderers } from '../renderers.mjs'; +import { _ as _middleware } from '../middleware.mjs'; +import '../chunks/astro.540fbe4e.mjs'; + +const page = () => import('../chunks/pages/index.astro.8aad0438.mjs'); +const middleware = _middleware; + +export { middleware, page }; +``` + +If we have a `pages/` folder that looks like this: +``` +├── blog +│ ├── first.astro +│ └── post.astro +├── first.astro +├── index.astro +├── issue.md +└── second.astro +``` + +The emitted entry points will be stored inside a `pages/` folder, and they +will look like this: +``` +├── _astro +│ ├── first.132e69e0.css +│ ├── first.49cbf029.css +│ ├── post.a3e86c58.css +│ └── second.d178d0b2.css +├── chunks +│ ├── astro.540fbe4e.mjs +│ └── pages +│ ├── first.astro.493fa853.mjs +│ ├── index.astro.8aad0438.mjs +│ ├── issue.md.535b7d3b.mjs +│ ├── post.astro.26e892d9.mjs +│ └── second.astro.76540694.mjs +├── middleware.mjs +├── pages +│ ├── blog +│ │ ├── first.astro.mjs +│ │ └── post.astro.mjs +│ ├── first.astro.mjs +│ ├── index.astro.mjs +│ ├── issue.md.mjs +│ └── second.astro.mjs +└── renderers.mjs +``` + +Of course, all these files will be deleted by Astro at the end build. + +## `plugin-ssr` (WIP) + +This plugin is responsible to create a single `entry.mjs` file that will be used +in SSR. + +This plugin **will emit code** only when building an **SSR** site. + +The plugin will collect all the [virtual pages](#plugin-pages) and create +a JavaScript `Map`. These map will look like this: + +```js +const _page$0 = () => import("../chunks/.mjs") +const _page$1 = () => import("../chunks/.mjs") + +const pageMap = new Map([ + ["src/pages/index.astro", _page$0], + ["src/pages/about.astro", _page$1], +]) +``` + +It will also import the [`renderers`](#plugin-renderers) virtual module +and the [`middleware`](#plugin-middleware) virtual module. + diff --git a/packages/astro/src/core/build/plugins/plugin-middleware.ts b/packages/astro/src/core/build/plugins/plugin-middleware.ts index 507c4ae710a0..dd9872da3db4 100644 --- a/packages/astro/src/core/build/plugins/plugin-middleware.ts +++ b/packages/astro/src/core/build/plugins/plugin-middleware.ts @@ -6,9 +6,7 @@ import type { AstroBuildPlugin } from '../plugin'; import type { StaticBuildOptions } from '../types'; export const MIDDLEWARE_MODULE_ID = '@astro-middleware'; -export const RESOLVED_MIDDLEWARE_MODULE_ID = '\0@astro-middleware'; -let inputs: Set = new Set(); export function vitePluginMiddleware( opts: StaticBuildOptions, _internals: BuildInternals @@ -21,26 +19,14 @@ export function vitePluginMiddleware( } }, - resolveId(id) { + async resolveId(id) { if (id === MIDDLEWARE_MODULE_ID && opts.settings.config.experimental.middleware) { - return RESOLVED_MIDDLEWARE_MODULE_ID; - } - }, - - async load(id) { - if (id === RESOLVED_MIDDLEWARE_MODULE_ID && opts.settings.config.experimental.middleware) { - const imports: string[] = []; - const exports: string[] = []; - let middlewareId = await this.resolve( + const middlewareId = await this.resolve( `${opts.settings.config.srcDir.pathname}/${MIDDLEWARE_PATH_SEGMENT_NAME}` ); if (middlewareId) { - imports.push(`import { onRequest } from "${middlewareId.id}"`); - exports.push(`export { onRequest }`); + return middlewareId.id; } - const result = [imports.join('\n'), exports.join('\n')]; - - return result.join('\n'); } }, }; diff --git a/packages/astro/src/core/build/plugins/plugin-pages.ts b/packages/astro/src/core/build/plugins/plugin-pages.ts index 051c85583b2d..3ea0a61c851a 100644 --- a/packages/astro/src/core/build/plugins/plugin-pages.ts +++ b/packages/astro/src/core/build/plugins/plugin-pages.ts @@ -1,11 +1,33 @@ import type { Plugin as VitePlugin } from 'vite'; -import { pagesVirtualModuleId, resolvedPagesVirtualModuleId } from '../../app/index.js'; import { addRollupInput } from '../add-rollup-input.js'; -import { eachPageData, type BuildInternals } from '../internal.js'; +import { type BuildInternals } from '../internal.js'; import type { AstroBuildPlugin } from '../plugin'; import type { StaticBuildOptions } from '../types'; import { MIDDLEWARE_MODULE_ID } from './plugin-middleware.js'; import { RENDERERS_MODULE_ID } from './plugin-renderers.js'; +import { extname } from 'node:path'; + +export const ASTRO_PAGE_MODULE_ID = '@astro-page:'; +export const ASTRO_PAGE_RESOLVED_MODULE_ID = '\0@astro-page:'; + +// This is an arbitrary string that we are going to replace the dot of the extension +export const ASTRO_PAGE_EXTENSION_POST_PATTERN = '@_@'; + +/** + * 1. We add a fixed prefix, which is used as virtual module naming convention; + * 2. We replace the dot that belongs extension with an arbitrary string. + * + * @param path + */ +export function getVirtualModulePageNameFromPath(path: string) { + // we mask the extension, so this virtual file + // so rollup won't trigger other plugins in the process + const extension = extname(path); + return `${ASTRO_PAGE_MODULE_ID}${path.replace( + extension, + extension.replace('.', ASTRO_PAGE_EXTENSION_POST_PATTERN) + )}`; +} function vitePluginPages(opts: StaticBuildOptions, internals: BuildInternals): VitePlugin { return { @@ -13,42 +35,49 @@ function vitePluginPages(opts: StaticBuildOptions, internals: BuildInternals): V options(options) { if (opts.settings.config.output === 'static') { - return addRollupInput(options, [pagesVirtualModuleId]); + const inputs: Set = new Set(); + + for (const path of Object.keys(opts.allPages)) { + inputs.add(getVirtualModulePageNameFromPath(path)); + } + + return addRollupInput(options, Array.from(inputs)); } }, resolveId(id) { - if (id === pagesVirtualModuleId) { - return resolvedPagesVirtualModuleId; + if (id.startsWith(ASTRO_PAGE_MODULE_ID)) { + return '\0' + id; } }, async load(id) { - if (id === resolvedPagesVirtualModuleId) { - let importMap = ''; + if (id.startsWith(ASTRO_PAGE_RESOLVED_MODULE_ID)) { const imports: string[] = []; const exports: string[] = []; - const content: string[] = []; - let i = 0; - imports.push(`import { renderers } from "${RENDERERS_MODULE_ID}";`); - exports.push(`export { renderers };`); - for (const pageData of eachPageData(internals)) { - const variable = `_page${i}`; - imports.push( - `const ${variable} = () => import(${JSON.stringify(pageData.moduleSpecifier)});` - ); - importMap += `[${JSON.stringify(pageData.component)}, ${variable}],`; - i++; - } + // we remove the module name prefix from id, this will result into a string that will start with "src/..." + const pageName = id.slice(ASTRO_PAGE_RESOLVED_MODULE_ID.length); + // We replaced the `.` of the extension with ASTRO_PAGE_EXTENSION_POST_PATTERN, let's replace it back + const pageData = internals.pagesByComponent.get( + `${pageName.replace(ASTRO_PAGE_EXTENSION_POST_PATTERN, '.')}` + ); + if (pageData) { + const resolvedPage = await this.resolve(pageData.moduleSpecifier); + if (resolvedPage) { + imports.push(`const page = () => import(${JSON.stringify(pageData.moduleSpecifier)});`); + exports.push(`export { page }`); - if (opts.settings.config.experimental.middleware) { - imports.push(`import * as _middleware from "${MIDDLEWARE_MODULE_ID}";`); - exports.push(`export const middleware = _middleware;`); - } + imports.push(`import { renderers } from "${RENDERERS_MODULE_ID}";`); + exports.push(`export { renderers };`); - content.push(`export const pageMap = new Map([${importMap}]);`); + if (opts.settings.config.experimental.middleware) { + imports.push(`import * as _middleware from "${MIDDLEWARE_MODULE_ID}";`); + exports.push(`export const middleware = _middleware;`); + } - return `${imports.join('\n')}${content.join('\n')}${exports.join('\n')}`; + return `${imports.join('\n')}${exports.join('\n')}`; + } + } } }, }; diff --git a/packages/astro/src/core/build/plugins/plugin-ssr.ts b/packages/astro/src/core/build/plugins/plugin-ssr.ts index a6d02b7927c5..b40faf20dc4a 100644 --- a/packages/astro/src/core/build/plugins/plugin-ssr.ts +++ b/packages/astro/src/core/build/plugins/plugin-ssr.ts @@ -1,62 +1,90 @@ import type { Plugin as VitePlugin } from 'vite'; -import type { AstroAdapter, AstroConfig } from '../../../@types/astro'; +import type { AstroAdapter } from '../../../@types/astro'; import type { SerializedRouteInfo, SerializedSSRManifest } from '../../app/types'; import type { StaticBuildOptions } from '../types'; - +import type { AstroBuildPlugin } from '../plugin'; import glob from 'fast-glob'; import { fileURLToPath } from 'url'; import { runHookBuildSsr } from '../../../integrations/index.js'; import { isHybridOutput } from '../../../prerender/utils.js'; import { BEFORE_HYDRATION_SCRIPT_ID, PAGE_SCRIPT_ID } from '../../../vite-plugin-scripts/index.js'; -import { pagesVirtualModuleId } from '../../app/index.js'; import { joinPaths, prependForwardSlash } from '../../path.js'; import { serializeRouteData } from '../../routing/index.js'; import { addRollupInput } from '../add-rollup-input.js'; import { getOutFile, getOutFolder } from '../common.js'; import { cssOrder, mergeInlineCss, type BuildInternals } from '../internal.js'; -import type { AstroBuildPlugin } from '../plugin'; +import { MIDDLEWARE_MODULE_ID } from './plugin-middleware.js'; import { RENDERERS_MODULE_ID } from './plugin-renderers.js'; +import { getVirtualModulePageNameFromPath } from './plugin-pages.js'; -export const virtualModuleId = '@astrojs-ssr-virtual-entry'; -const resolvedVirtualModuleId = '\0' + virtualModuleId; +export const SSR_VIRTUAL_MODULE_ID = '@astrojs-ssr-virtual-entry'; +const RESOLVED_SSR_VIRTUAL_MODULE_ID = '\0' + SSR_VIRTUAL_MODULE_ID; const manifestReplace = '@@ASTRO_MANIFEST_REPLACE@@'; const replaceExp = new RegExp(`['"](${manifestReplace})['"]`, 'g'); function vitePluginSSR( internals: BuildInternals, adapter: AstroAdapter, - config: AstroConfig + options: StaticBuildOptions ): VitePlugin { return { name: '@astrojs/vite-plugin-astro-ssr', enforce: 'post', options(opts) { - return addRollupInput(opts, [virtualModuleId]); + return addRollupInput(opts, [SSR_VIRTUAL_MODULE_ID]); }, resolveId(id) { - if (id === virtualModuleId) { - return resolvedVirtualModuleId; + if (id === SSR_VIRTUAL_MODULE_ID) { + return RESOLVED_SSR_VIRTUAL_MODULE_ID; } }, - load(id) { - if (id === resolvedVirtualModuleId) { - let middleware = ''; + async load(id) { + if (id === RESOLVED_SSR_VIRTUAL_MODULE_ID) { + const { + settings: { config }, + allPages, + } = options; + const imports: string[] = []; + const contents: string[] = []; + const exports: string[] = []; + let middleware; if (config.experimental?.middleware === true) { - middleware = 'middleware: _main.middleware'; + imports.push(`import * as _middleware from "${MIDDLEWARE_MODULE_ID}"`); + middleware = 'middleware: _middleware'; + } + let i = 0; + const pageMap: string[] = []; + + for (const path of Object.keys(allPages)) { + const virtualModuleName = getVirtualModulePageNameFromPath(path); + let module = await this.resolve(virtualModuleName); + if (module) { + const variable = `_page${i}`; + // we need to use the non-resolved ID in order to resolve correctly the virtual module + imports.push(`const ${variable} = () => import("${virtualModuleName}");`); + + const pageData = internals.pagesByComponent.get(path); + if (pageData) { + pageMap.push(`[${JSON.stringify(pageData.component)}, ${variable}]`); + } + i++; + } } - return `import * as adapter from '${adapter.serverEntrypoint}'; + + contents.push(`const pageMap = new Map([${pageMap.join(',')}]);`); + exports.push(`export { pageMap }`); + const content = `import * as adapter from '${adapter.serverEntrypoint}'; import { renderers } from '${RENDERERS_MODULE_ID}'; -import * as _main from '${pagesVirtualModuleId}'; import { deserializeManifest as _deserializeManifest } from 'astro/app'; import { _privateSetManifestDontUseThis } from 'astro:ssr-manifest'; const _manifest = Object.assign(_deserializeManifest('${manifestReplace}'), { - pageMap: _main.pageMap, - renderers: _main.renderers, + pageMap, + renderers, ${middleware} }); _privateSetManifestDontUseThis(_manifest); const _args = ${adapter.args ? JSON.stringify(adapter.args) : 'undefined'}; -export * from '${pagesVirtualModuleId}'; + ${ adapter.exports ? `const _exports = adapter.createExports(_manifest, _args); @@ -77,6 +105,7 @@ const _start = 'start'; if(_start in adapter) { adapter[_start](_manifest, _args); }`; + return `${imports.join('\n')}${contents.join('\n')}${content}${exports.join('\n')}`; } return void 0; }, @@ -92,7 +121,7 @@ if(_start in adapter) { if (chunk.type === 'asset') { continue; } - if (chunk.modules[resolvedVirtualModuleId]) { + if (chunk.modules[RESOLVED_SSR_VIRTUAL_MODULE_ID]) { internals.ssrEntryChunk = chunk; delete bundle[chunkName]; } @@ -250,7 +279,7 @@ export function pluginSSR( hooks: { 'build:before': () => { let vitePlugin = ssr - ? vitePluginSSR(internals, options.settings.adapter!, options.settings.config) + ? vitePluginSSR(internals, options.settings.adapter!, options) : undefined; return { diff --git a/packages/astro/src/core/build/static-build.ts b/packages/astro/src/core/build/static-build.ts index ef6541b308eb..54e99e0f4efa 100644 --- a/packages/astro/src/core/build/static-build.ts +++ b/packages/astro/src/core/build/static-build.ts @@ -17,7 +17,6 @@ import { isModeServerWithNoAdapter } from '../../core/util.js'; import { runHookBuildSetup } from '../../integrations/index.js'; import { isHybridOutput } from '../../prerender/utils.js'; import { PAGE_SCRIPT_ID } from '../../vite-plugin-scripts/index.js'; -import { resolvedPagesVirtualModuleId } from '../app/index.js'; import { AstroError, AstroErrorData } from '../errors/index.js'; import { info } from '../logger/core.js'; import { getOutDirWithinCwd } from './common.js'; @@ -25,10 +24,14 @@ import { generatePages } from './generate.js'; import { trackPageData } from './internal.js'; import { createPluginContainer, type AstroBuildPluginContainer } from './plugin.js'; import { registerAllPlugins } from './plugins/index.js'; -import { RESOLVED_MIDDLEWARE_MODULE_ID } from './plugins/plugin-middleware.js'; import { RESOLVED_RENDERERS_MODULE_ID } from './plugins/plugin-renderers.js'; import type { PageBuildData, StaticBuildOptions } from './types'; import { getTimeStat } from './util.js'; +import { + ASTRO_PAGE_EXTENSION_POST_PATTERN, + ASTRO_PAGE_RESOLVED_MODULE_ID, +} from './plugins/plugin-pages.js'; +import { SSR_VIRTUAL_MODULE_ID } from './plugins/plugin-ssr.js'; export async function viteBuild(opts: StaticBuildOptions) { const { allPages, settings } = opts; @@ -172,10 +175,17 @@ async function ssrBuild( assetFileNames: `${settings.config.build.assets}/[name].[hash][extname]`, ...viteConfig.build?.rollupOptions?.output, entryFileNames(chunkInfo) { - if (chunkInfo.facadeModuleId === resolvedPagesVirtualModuleId) { - return opts.buildConfig.serverEntry; - } else if (chunkInfo.facadeModuleId === RESOLVED_MIDDLEWARE_MODULE_ID) { + if (chunkInfo.facadeModuleId?.startsWith(ASTRO_PAGE_RESOLVED_MODULE_ID)) { + return makeAstroPageEntryPointFileName(chunkInfo.facadeModuleId); + } else if ( + // checks if the path of the module we have middleware, e.g. middleware.js / middleware/index.js + chunkInfo.facadeModuleId?.includes('middleware') && + // checks if the file actually export the `onRequest` function + chunkInfo.exports.includes('onRequest') + ) { return 'middleware.mjs'; + } else if (chunkInfo.facadeModuleId === SSR_VIRTUAL_MODULE_ID) { + return opts.settings.config.build.serverEntry; } else if (chunkInfo.facadeModuleId === RESOLVED_RENDERERS_MODULE_ID) { return 'renderers.mjs'; } else { @@ -408,3 +418,29 @@ async function ssrMoveAssets(opts: StaticBuildOptions) { removeEmptyDirs(serverAssets); } } + +/** + * This function takes as input the virtual module name of an astro page and transform + * to generate an `.mjs` file: + * + * Input: `@astro-page:src/pages/index@_@astro` + * + * Output: `pages/index.astro.mjs` + * + * 1. We remove the module id prefix, `@astro-page:` + * 2. We remove `src/` + * 3. We replace square brackets with underscore, for example `[slug]` + * 4. At last, we replace the extension pattern with a simple dot + * 5. We append the `.mjs` string, so the file will always be a JS file + * + * @param facadeModuleId + */ +function makeAstroPageEntryPointFileName(facadeModuleId: string) { + return `${facadeModuleId + .replace(ASTRO_PAGE_RESOLVED_MODULE_ID, '') + .replace('src/', '') + .replaceAll('[', '_') + .replaceAll(']', '_') + // this must be last + .replace(ASTRO_PAGE_EXTENSION_POST_PATTERN, '.')}.mjs`; +} diff --git a/packages/astro/src/core/build/types.ts b/packages/astro/src/core/build/types.ts index c0f38de45b43..772235697751 100644 --- a/packages/astro/src/core/build/types.ts +++ b/packages/astro/src/core/build/types.ts @@ -49,8 +49,8 @@ export interface StaticBuildOptions { type ImportComponentInstance = () => Promise; -export interface SingleFileBuiltModule { - pageMap: Map; +export interface SinglePageBuiltModule { + page: ImportComponentInstance; middleware: AstroMiddlewareInstance; renderers: SSRLoadedRenderer[]; }