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) => {
* context.locals.greeting = "Hello!";
* next();
* return next();
* });
* ```
* Inside a `.astro` file:

View file

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

View file

@ -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<RouteInfo, 'routeData'> & {
routeData: SerializedRouteData;
};
type ImportComponentInstance = () => Promise<ComponentInstance>;
type ImportComponentInstance = () => Promise<SinglePageBuiltModule>;
export interface SSRManifest {
adapterName: string;

View file

@ -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<string>();
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<string>
) {
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) {

View file

@ -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)
);
}

View file

@ -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<string, string>(),
pageToBundleMap: new Map<string, string>(),
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) {

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';
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(
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');
}
},
};

View file

@ -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<string> = 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 }`);
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 { 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';
}
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 * 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 {

View file

@ -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`;
}

View file

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