refactor: emit pages as physical entry points (#7193)
This commit is contained in:
parent
f5a8cffac2
commit
8b041bf57c
13 changed files with 354 additions and 94 deletions
6
.changeset/dry-taxis-suffer.md
Normal file
6
.changeset/dry-taxis-suffer.md
Normal 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.
|
|
@ -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:
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
144
packages/astro/src/core/build/plugins/README.md
Normal file
144
packages/astro/src/core/build/plugins/README.md
Normal 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.
|
||||||
|
|
|
@ -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');
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -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')}`;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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`;
|
||||||
|
}
|
||||||
|
|
|
@ -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[];
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue