refactor: emit pages as physical entry points (#7193)

This commit is contained in:
Emanuele Stoppa 2023-05-25 14:28:40 +01:00 committed by GitHub
parent f5a8cffac2
commit 8b041bf57c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 354 additions and 94 deletions

View file

@ -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.

View file

@ -1691,7 +1691,7 @@ export interface APIContext<Props extends Record<string, any> = Record<string, a
* *
* export const onRequest = defineMiddleware((context, next) => { * export const onRequest = defineMiddleware((context, next) => {
* context.locals.greeting = "Hello!"; * context.locals.greeting = "Hello!";
* next(); * return next();
* }); * });
* ``` * ```
* Inside a `.astro` file: * Inside a `.astro` file:

View file

@ -32,8 +32,6 @@ export { deserializeManifest } from './common.js';
const clientLocalsSymbol = Symbol.for('astro.locals'); const clientLocalsSymbol = Symbol.for('astro.locals');
export const pagesVirtualModuleId = '@astrojs-pages-virtual-entry';
export const resolvedPagesVirtualModuleId = '\0' + pagesVirtualModuleId;
const responseSentSymbol = Symbol.for('astro.responseSent'); const responseSentSymbol = Symbol.for('astro.responseSent');
export interface MatchOptions { export interface MatchOptions {
@ -139,7 +137,8 @@ export class App {
defaultStatus = 404; 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') { if (routeData.type === 'page') {
let response = await this.#renderPage(request, routeData, mod, defaultStatus); let response = await this.#renderPage(request, routeData, mod, defaultStatus);
@ -148,7 +147,8 @@ export class App {
if (response.status === 500 || response.status === 404) { if (response.status === 500 || response.status === 404) {
const errorPageData = matchRoute('/' + response.status, this.#manifestData); const errorPageData = matchRoute('/' + response.status, this.#manifestData);
if (errorPageData && errorPageData.route !== routeData.route) { 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 { try {
let errorResponse = await this.#renderPage( let errorResponse = await this.#renderPage(
request, request,

View file

@ -8,6 +8,7 @@ import type {
SSRLoadedRenderer, SSRLoadedRenderer,
SSRResult, SSRResult,
} from '../../@types/astro'; } from '../../@types/astro';
import type { SinglePageBuiltModule } from '../build/types';
export type ComponentPath = string; export type ComponentPath = string;
@ -31,7 +32,7 @@ export interface RouteInfo {
export type SerializedRouteInfo = Omit<RouteInfo, 'routeData'> & { export type SerializedRouteInfo = Omit<RouteInfo, 'routeData'> & {
routeData: SerializedRouteData; routeData: SerializedRouteData;
}; };
type ImportComponentInstance = () => Promise<ComponentInstance>; type ImportComponentInstance = () => Promise<SinglePageBuiltModule>;
export interface SSRManifest { export interface SSRManifest {
adapterName: string; adapterName: string;

View file

@ -20,7 +20,11 @@ import {
generateImage as generateImageInternal, generateImage as generateImageInternal,
getStaticImageList, getStaticImageList,
} from '../../assets/generate.js'; } from '../../assets/generate.js';
import { hasPrerenderedPages, type BuildInternals } from '../../core/build/internal.js'; import {
hasPrerenderedPages,
type BuildInternals,
eachPageDataFromEntryPoint,
} from '../../core/build/internal.js';
import { import {
prependForwardSlash, prependForwardSlash,
removeLeadingForwardSlash, removeLeadingForwardSlash,
@ -47,11 +51,12 @@ import { getOutDirWithinCwd, getOutFile, getOutFolder } from './common.js';
import { cssOrder, eachPageData, getPageDataByComponent, mergeInlineCss } from './internal.js'; import { cssOrder, eachPageData, getPageDataByComponent, mergeInlineCss } from './internal.js';
import type { import type {
PageBuildData, PageBuildData,
SingleFileBuiltModule, SinglePageBuiltModule,
StaticBuildOptions, StaticBuildOptions,
StylesheetAsset, StylesheetAsset,
} from './types'; } from './types';
import { getTimeStat } from './util.js'; import { getTimeStat } from './util.js';
import { ASTRO_PAGE_MODULE_ID } from './plugins/plugin-pages';
function shouldSkipDraft(pageModule: ComponentInstance, settings: AstroSettings): boolean { function shouldSkipDraft(pageModule: ComponentInstance, settings: AstroSettings): boolean {
return ( return (
@ -99,18 +104,23 @@ export async function generatePages(opts: StaticBuildOptions, internals: BuildIn
const verb = ssr ? 'prerendering' : 'generating'; const verb = ssr ? 'prerendering' : 'generating';
info(opts.logging, null, `\n${bgGreen(black(` ${verb} static routes `))}`); 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<string>(); const builtPaths = new Set<string>();
if (ssr) { if (ssr) {
for (const pageData of eachPageData(internals)) { for (const [pageData, filePath] of eachPageDataFromEntryPoint(internals)) {
if (pageData.route.prerender) if (pageData.route.prerender) {
await generatePage(opts, internals, pageData, ssrEntry, builtPaths); const ssrEntryURLPage = new URL('./' + filePath + `?time=${Date.now()}`, outFolder);
const ssrEntryPage = await import(ssrEntryURLPage.toString());
await generatePage(opts, internals, pageData, ssrEntryPage, builtPaths);
}
} }
} else { } else {
for (const pageData of eachPageData(internals)) { for (const [pageData, filePath] of eachPageDataFromEntryPoint(internals)) {
await generatePage(opts, internals, pageData, ssrEntry, builtPaths); 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, opts: StaticBuildOptions,
internals: BuildInternals, internals: BuildInternals,
pageData: PageBuildData, pageData: PageBuildData,
ssrEntry: SingleFileBuiltModule, ssrEntry: SinglePageBuiltModule,
builtPaths: Set<string> builtPaths: Set<string>
) { ) {
let timeStart = performance.now(); let timeStart = performance.now();
@ -169,7 +179,7 @@ async function generatePage(
.map(({ sheet }) => sheet) .map(({ sheet }) => sheet)
.reduce(mergeInlineCss, []); .reduce(mergeInlineCss, []);
const pageModulePromise = ssrEntry.pageMap?.get(pageData.component); const pageModulePromise = ssrEntry.page;
const middleware = ssrEntry.middleware; const middleware = ssrEntry.middleware;
if (!pageModulePromise) { if (!pageModulePromise) {

View file

@ -1,6 +1,6 @@
import type { GetModuleInfo, ModuleInfo } from 'rollup'; 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. // This walks up the dependency graph and yields out each ModuleInfo object.
export function* walkParentInfos( export function* walkParentInfos(
@ -43,8 +43,8 @@ export function* walkParentInfos(
// it is imported by the top-level virtual module. // it is imported by the top-level virtual module.
export function moduleIsTopLevelPage(info: ModuleInfo): boolean { export function moduleIsTopLevelPage(info: ModuleInfo): boolean {
return ( return (
info.importers[0] === resolvedPagesVirtualModuleId || info.importers[0]?.includes(ASTRO_PAGE_RESOLVED_MODULE_ID) ||
info.dynamicImporters[0] == resolvedPagesVirtualModuleId info.dynamicImporters[0]?.includes(ASTRO_PAGE_RESOLVED_MODULE_ID)
); );
} }

View file

@ -1,10 +1,10 @@
import type { Rollup } from 'vite'; import type { Rollup } from 'vite';
import type { PageBuildData, StylesheetAsset, ViteID } from './types'; import type { PageBuildData, StylesheetAsset, ViteID } from './types';
import type { SSRResult } from '../../@types/astro'; import type { SSRResult } from '../../@types/astro';
import type { PageOptions } from '../../vite-plugin-astro/types'; import type { PageOptions } from '../../vite-plugin-astro/types';
import { prependForwardSlash, removeFileExtension } from '../path.js'; import { prependForwardSlash, removeFileExtension } from '../path.js';
import { viteID } from '../util.js'; import { viteID } from '../util.js';
import { ASTRO_PAGE_EXTENSION_POST_PATTERN, ASTRO_PAGE_MODULE_ID } from './plugins/plugin-pages.js';
export interface BuildInternals { export interface BuildInternals {
/** /**
@ -97,7 +97,6 @@ export function createBuildInternals(): BuildInternals {
hoistedScriptIdToPagesMap, hoistedScriptIdToPagesMap,
entrySpecifierToBundleMap: new Map<string, string>(), entrySpecifierToBundleMap: new Map<string, string>(),
pageToBundleMap: new Map<string, string>(), pageToBundleMap: new Map<string, string>(),
pagesByComponent: new Map(), pagesByComponent: new Map(),
pageOptionsByPage: new Map(), pageOptionsByPage: new Map(),
pagesByViteID: new Map(), pagesByViteID: new Map(),
@ -215,6 +214,26 @@ export function* eachPageData(internals: BuildInternals) {
yield* internals.pagesByComponent.values(); 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) { export function hasPrerenderedPages(internals: BuildInternals) {
for (const pageData of eachPageData(internals)) { for (const pageData of eachPageData(internals)) {
if (pageData.route.prerender) { if (pageData.route.prerender) {

View file

@ -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/<INDEX.ASTRO_CHUNK>.mjs")
const _page$1 = () => import("../chunks/<ABOUT.ASTRO_CHUNK>.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.

View file

@ -6,9 +6,7 @@ import type { AstroBuildPlugin } from '../plugin';
import type { StaticBuildOptions } from '../types'; import type { StaticBuildOptions } from '../types';
export const MIDDLEWARE_MODULE_ID = '@astro-middleware'; export const MIDDLEWARE_MODULE_ID = '@astro-middleware';
export const RESOLVED_MIDDLEWARE_MODULE_ID = '\0@astro-middleware';
let inputs: Set<string> = new Set();
export function vitePluginMiddleware( export function vitePluginMiddleware(
opts: StaticBuildOptions, opts: StaticBuildOptions,
_internals: BuildInternals _internals: BuildInternals
@ -21,26 +19,14 @@ export function vitePluginMiddleware(
} }
}, },
resolveId(id) { async resolveId(id) {
if (id === MIDDLEWARE_MODULE_ID && opts.settings.config.experimental.middleware) { if (id === MIDDLEWARE_MODULE_ID && opts.settings.config.experimental.middleware) {
return RESOLVED_MIDDLEWARE_MODULE_ID; const middlewareId = await this.resolve(
}
},
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(
`${opts.settings.config.srcDir.pathname}/${MIDDLEWARE_PATH_SEGMENT_NAME}` `${opts.settings.config.srcDir.pathname}/${MIDDLEWARE_PATH_SEGMENT_NAME}`
); );
if (middlewareId) { if (middlewareId) {
imports.push(`import { onRequest } from "${middlewareId.id}"`); return middlewareId.id;
exports.push(`export { onRequest }`);
} }
const result = [imports.join('\n'), exports.join('\n')];
return result.join('\n');
} }
}, },
}; };

View file

@ -1,11 +1,33 @@
import type { Plugin as VitePlugin } from 'vite'; import type { Plugin as VitePlugin } from 'vite';
import { pagesVirtualModuleId, resolvedPagesVirtualModuleId } from '../../app/index.js';
import { addRollupInput } from '../add-rollup-input.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 { AstroBuildPlugin } from '../plugin';
import type { StaticBuildOptions } from '../types'; import type { StaticBuildOptions } from '../types';
import { MIDDLEWARE_MODULE_ID } from './plugin-middleware.js'; import { MIDDLEWARE_MODULE_ID } from './plugin-middleware.js';
import { RENDERERS_MODULE_ID } from './plugin-renderers.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 { function vitePluginPages(opts: StaticBuildOptions, internals: BuildInternals): VitePlugin {
return { return {
@ -13,42 +35,49 @@ function vitePluginPages(opts: StaticBuildOptions, internals: BuildInternals): V
options(options) { options(options) {
if (opts.settings.config.output === 'static') { if (opts.settings.config.output === 'static') {
return addRollupInput(options, [pagesVirtualModuleId]); const inputs: Set<string> = new Set();
for (const path of Object.keys(opts.allPages)) {
inputs.add(getVirtualModulePageNameFromPath(path));
}
return addRollupInput(options, Array.from(inputs));
} }
}, },
resolveId(id) { resolveId(id) {
if (id === pagesVirtualModuleId) { if (id.startsWith(ASTRO_PAGE_MODULE_ID)) {
return resolvedPagesVirtualModuleId; return '\0' + id;
} }
}, },
async load(id) { async load(id) {
if (id === resolvedPagesVirtualModuleId) { if (id.startsWith(ASTRO_PAGE_RESOLVED_MODULE_ID)) {
let importMap = '';
const imports: string[] = []; const imports: string[] = [];
const exports: string[] = []; const exports: string[] = [];
const content: string[] = []; // we remove the module name prefix from id, this will result into a string that will start with "src/..."
let i = 0; const pageName = id.slice(ASTRO_PAGE_RESOLVED_MODULE_ID.length);
imports.push(`import { renderers } from "${RENDERERS_MODULE_ID}";`); // We replaced the `.` of the extension with ASTRO_PAGE_EXTENSION_POST_PATTERN, let's replace it back
exports.push(`export { renderers };`); const pageData = internals.pagesByComponent.get(
for (const pageData of eachPageData(internals)) { `${pageName.replace(ASTRO_PAGE_EXTENSION_POST_PATTERN, '.')}`
const variable = `_page${i}`; );
imports.push( if (pageData) {
`const ${variable} = () => import(${JSON.stringify(pageData.moduleSpecifier)});` const resolvedPage = await this.resolve(pageData.moduleSpecifier);
); if (resolvedPage) {
importMap += `[${JSON.stringify(pageData.component)}, ${variable}],`; imports.push(`const page = () => import(${JSON.stringify(pageData.moduleSpecifier)});`);
i++; exports.push(`export { page }`);
imports.push(`import { renderers } from "${RENDERERS_MODULE_ID}";`);
exports.push(`export { renderers };`);
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')}${exports.join('\n')}`;
}
} }
if (opts.settings.config.experimental.middleware) {
imports.push(`import * as _middleware from "${MIDDLEWARE_MODULE_ID}";`);
exports.push(`export const middleware = _middleware;`);
}
content.push(`export const pageMap = new Map([${importMap}]);`);
return `${imports.join('\n')}${content.join('\n')}${exports.join('\n')}`;
} }
}, },
}; };

View file

@ -1,62 +1,90 @@
import type { Plugin as VitePlugin } from 'vite'; 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 { SerializedRouteInfo, SerializedSSRManifest } from '../../app/types';
import type { StaticBuildOptions } from '../types'; import type { StaticBuildOptions } from '../types';
import type { AstroBuildPlugin } from '../plugin';
import glob from 'fast-glob'; import glob from 'fast-glob';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { runHookBuildSsr } from '../../../integrations/index.js'; import { runHookBuildSsr } from '../../../integrations/index.js';
import { isHybridOutput } from '../../../prerender/utils.js'; import { isHybridOutput } from '../../../prerender/utils.js';
import { BEFORE_HYDRATION_SCRIPT_ID, PAGE_SCRIPT_ID } from '../../../vite-plugin-scripts/index.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 { joinPaths, prependForwardSlash } from '../../path.js';
import { serializeRouteData } from '../../routing/index.js'; import { serializeRouteData } from '../../routing/index.js';
import { addRollupInput } from '../add-rollup-input.js'; import { addRollupInput } from '../add-rollup-input.js';
import { getOutFile, getOutFolder } from '../common.js'; import { getOutFile, getOutFolder } from '../common.js';
import { cssOrder, mergeInlineCss, type BuildInternals } from '../internal.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 { RENDERERS_MODULE_ID } from './plugin-renderers.js';
import { getVirtualModulePageNameFromPath } from './plugin-pages.js';
export const virtualModuleId = '@astrojs-ssr-virtual-entry'; export const SSR_VIRTUAL_MODULE_ID = '@astrojs-ssr-virtual-entry';
const resolvedVirtualModuleId = '\0' + virtualModuleId; const RESOLVED_SSR_VIRTUAL_MODULE_ID = '\0' + SSR_VIRTUAL_MODULE_ID;
const manifestReplace = '@@ASTRO_MANIFEST_REPLACE@@'; const manifestReplace = '@@ASTRO_MANIFEST_REPLACE@@';
const replaceExp = new RegExp(`['"](${manifestReplace})['"]`, 'g'); const replaceExp = new RegExp(`['"](${manifestReplace})['"]`, 'g');
function vitePluginSSR( function vitePluginSSR(
internals: BuildInternals, internals: BuildInternals,
adapter: AstroAdapter, adapter: AstroAdapter,
config: AstroConfig options: StaticBuildOptions
): VitePlugin { ): VitePlugin {
return { return {
name: '@astrojs/vite-plugin-astro-ssr', name: '@astrojs/vite-plugin-astro-ssr',
enforce: 'post', enforce: 'post',
options(opts) { options(opts) {
return addRollupInput(opts, [virtualModuleId]); return addRollupInput(opts, [SSR_VIRTUAL_MODULE_ID]);
}, },
resolveId(id) { resolveId(id) {
if (id === virtualModuleId) { if (id === SSR_VIRTUAL_MODULE_ID) {
return resolvedVirtualModuleId; return RESOLVED_SSR_VIRTUAL_MODULE_ID;
} }
}, },
load(id) { async load(id) {
if (id === resolvedVirtualModuleId) { if (id === RESOLVED_SSR_VIRTUAL_MODULE_ID) {
let middleware = ''; const {
settings: { config },
allPages,
} = options;
const imports: string[] = [];
const contents: string[] = [];
const exports: string[] = [];
let middleware;
if (config.experimental?.middleware === true) { if (config.experimental?.middleware === true) {
middleware = 'middleware: _main.middleware'; imports.push(`import * as _middleware from "${MIDDLEWARE_MODULE_ID}"`);
middleware = 'middleware: _middleware';
} }
return `import * as adapter from '${adapter.serverEntrypoint}'; 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++;
}
}
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 { renderers } from '${RENDERERS_MODULE_ID}';
import * as _main from '${pagesVirtualModuleId}';
import { deserializeManifest as _deserializeManifest } from 'astro/app'; import { deserializeManifest as _deserializeManifest } from 'astro/app';
import { _privateSetManifestDontUseThis } from 'astro:ssr-manifest'; import { _privateSetManifestDontUseThis } from 'astro:ssr-manifest';
const _manifest = Object.assign(_deserializeManifest('${manifestReplace}'), { const _manifest = Object.assign(_deserializeManifest('${manifestReplace}'), {
pageMap: _main.pageMap, pageMap,
renderers: _main.renderers, renderers,
${middleware} ${middleware}
}); });
_privateSetManifestDontUseThis(_manifest); _privateSetManifestDontUseThis(_manifest);
const _args = ${adapter.args ? JSON.stringify(adapter.args) : 'undefined'}; const _args = ${adapter.args ? JSON.stringify(adapter.args) : 'undefined'};
export * from '${pagesVirtualModuleId}';
${ ${
adapter.exports adapter.exports
? `const _exports = adapter.createExports(_manifest, _args); ? `const _exports = adapter.createExports(_manifest, _args);
@ -77,6 +105,7 @@ const _start = 'start';
if(_start in adapter) { if(_start in adapter) {
adapter[_start](_manifest, _args); adapter[_start](_manifest, _args);
}`; }`;
return `${imports.join('\n')}${contents.join('\n')}${content}${exports.join('\n')}`;
} }
return void 0; return void 0;
}, },
@ -92,7 +121,7 @@ if(_start in adapter) {
if (chunk.type === 'asset') { if (chunk.type === 'asset') {
continue; continue;
} }
if (chunk.modules[resolvedVirtualModuleId]) { if (chunk.modules[RESOLVED_SSR_VIRTUAL_MODULE_ID]) {
internals.ssrEntryChunk = chunk; internals.ssrEntryChunk = chunk;
delete bundle[chunkName]; delete bundle[chunkName];
} }
@ -250,7 +279,7 @@ export function pluginSSR(
hooks: { hooks: {
'build:before': () => { 'build:before': () => {
let vitePlugin = ssr let vitePlugin = ssr
? vitePluginSSR(internals, options.settings.adapter!, options.settings.config) ? vitePluginSSR(internals, options.settings.adapter!, options)
: undefined; : undefined;
return { return {

View file

@ -17,7 +17,6 @@ import { isModeServerWithNoAdapter } from '../../core/util.js';
import { runHookBuildSetup } from '../../integrations/index.js'; import { runHookBuildSetup } from '../../integrations/index.js';
import { isHybridOutput } from '../../prerender/utils.js'; import { isHybridOutput } from '../../prerender/utils.js';
import { PAGE_SCRIPT_ID } from '../../vite-plugin-scripts/index.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 { AstroError, AstroErrorData } from '../errors/index.js';
import { info } from '../logger/core.js'; import { info } from '../logger/core.js';
import { getOutDirWithinCwd } from './common.js'; import { getOutDirWithinCwd } from './common.js';
@ -25,10 +24,14 @@ import { generatePages } from './generate.js';
import { trackPageData } from './internal.js'; import { trackPageData } from './internal.js';
import { createPluginContainer, type AstroBuildPluginContainer } from './plugin.js'; import { createPluginContainer, type AstroBuildPluginContainer } from './plugin.js';
import { registerAllPlugins } from './plugins/index.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 { RESOLVED_RENDERERS_MODULE_ID } from './plugins/plugin-renderers.js';
import type { PageBuildData, StaticBuildOptions } from './types'; import type { PageBuildData, StaticBuildOptions } from './types';
import { getTimeStat } from './util.js'; 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) { export async function viteBuild(opts: StaticBuildOptions) {
const { allPages, settings } = opts; const { allPages, settings } = opts;
@ -172,10 +175,17 @@ async function ssrBuild(
assetFileNames: `${settings.config.build.assets}/[name].[hash][extname]`, assetFileNames: `${settings.config.build.assets}/[name].[hash][extname]`,
...viteConfig.build?.rollupOptions?.output, ...viteConfig.build?.rollupOptions?.output,
entryFileNames(chunkInfo) { entryFileNames(chunkInfo) {
if (chunkInfo.facadeModuleId === resolvedPagesVirtualModuleId) { if (chunkInfo.facadeModuleId?.startsWith(ASTRO_PAGE_RESOLVED_MODULE_ID)) {
return opts.buildConfig.serverEntry; return makeAstroPageEntryPointFileName(chunkInfo.facadeModuleId);
} else if (chunkInfo.facadeModuleId === RESOLVED_MIDDLEWARE_MODULE_ID) { } 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'; 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) { } else if (chunkInfo.facadeModuleId === RESOLVED_RENDERERS_MODULE_ID) {
return 'renderers.mjs'; return 'renderers.mjs';
} else { } else {
@ -408,3 +418,29 @@ async function ssrMoveAssets(opts: StaticBuildOptions) {
removeEmptyDirs(serverAssets); 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`;
}

View file

@ -49,8 +49,8 @@ export interface StaticBuildOptions {
type ImportComponentInstance = () => Promise<ComponentInstance>; type ImportComponentInstance = () => Promise<ComponentInstance>;
export interface SingleFileBuiltModule { export interface SinglePageBuiltModule {
pageMap: Map<ComponentPath, ImportComponentInstance>; page: ImportComponentInstance;
middleware: AstroMiddlewareInstance<unknown>; middleware: AstroMiddlewareInstance<unknown>;
renderers: SSRLoadedRenderer[]; renderers: SSRLoadedRenderer[];
} }