Skip to content

Commit d296098

Browse files
natemoo-redelucis
andauthored
Experimental Prerender API (#5297)
* wip: hybrid output * wip: hybrid output mvp * refactor: move hybrid => server * wip: hybrid support for `output: 'server'` * feat(hybrid): overwrite static files * fix: update static build * feat(hybrid): skip page generation if no static entrypoints * feat: migrate from hybrid output => prerender flag * fix: appease typescript * fix: appease typescript * fix: appease typescript * fix: appease typescript * fix: improve static cleanup * attempt: avoid preprocess scanning * hack: force generated .js files to be treated as ESM * better handling for astro metadata * fix: update scanner plugin * fix: page name bug * fix: keep ssr false when generating pages * fix: force output to be treated as ESM * fix: client output should respect buildConfig * fix: ensure outDir is always created * fix: do not replace files with noop * fix(netlify): add support for `experimental_prerender` pages * feat: switch to `experimental_prerender` * chore: update es-module-lexer code in test * feat: improved code-splitting, cleanup * feat: move prerender behind flag * test: prerender * test: update prerender test * chore: update lockfile * fix: only match `.html` files when resolving assets * chore: update changeset * chore: remove ESM hack * chore: allow `--experimental-prerender` flag, move `--experimental-error-overlay` into subobject * chore: update changeset * test(vite-plugin-scanner): add proper unit tests for vite-plugin-scanner * chore: remove leftover code * chore: add comment on cleanup task * refactor: move manual chunks logic to vite-plugin-prerender * fix: do not support let declarations * test: add var test * refactor: prefer existing util * Update packages/astro/src/@types/astro.ts Co-authored-by: Chris Swithinbank <[email protected]> * Update packages/astro/src/core/errors/errors-data.ts Co-authored-by: Chris Swithinbank <[email protected]> * Update packages/astro/src/@types/astro.ts Co-authored-by: Chris Swithinbank <[email protected]> Co-authored-by: Nate Moore <[email protected]> Co-authored-by: Chris Swithinbank <[email protected]>
1 parent 7cbe7f5 commit d296098

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+701
-68
lines changed

.changeset/funny-waves-worry.md

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'astro': minor
3+
'@astrojs/netlify': minor
4+
---
5+
6+
Introduces the **experimental** Prerender API.
7+
8+
> **Note**
9+
> This API is not yet stable and is subject to possible breaking changes!
10+
11+
- Deploy an Astro server without sacrificing the speed or cacheability of static HTML.
12+
- The Prerender API allows you to statically prerender specific `pages/` at build time.
13+
14+
**Usage**
15+
16+
- First, run `astro build --experimental-prerender` or enable `experimental: { prerender: true }` in your `astro.config.mjs` file.
17+
- Then, include `export const prerender = true` in any file in the `pages/` directory that you wish to prerender.

packages/astro/e2e/error-cyclic.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { expect } from '@playwright/test';
22
import { testFactory, getErrorOverlayContent } from './test-utils.js';
33

44
const test = testFactory({
5-
experimentalErrorOverlay: true,
5+
experimental: { errorOverlay: true },
66
root: './fixtures/error-cyclic/',
77
});
88

packages/astro/e2e/error-react-spectrum.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { expect } from '@playwright/test';
22
import { testFactory, getErrorOverlayContent } from './test-utils.js';
33

44
const test = testFactory({
5-
experimentalErrorOverlay: true,
5+
experimental: { errorOverlay: true },
66
root: './fixtures/error-react-spectrum/',
77
});
88

packages/astro/e2e/error-sass.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { expect } from '@playwright/test';
22
import { testFactory, getErrorOverlayContent } from './test-utils.js';
33

44
const test = testFactory({
5-
experimentalErrorOverlay: true,
5+
experimental: { errorOverlay: true },
66
root: './fixtures/error-sass/',
77
});
88

packages/astro/e2e/errors.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { expect } from '@playwright/test';
22
import { getErrorOverlayContent, testFactory } from './test-utils.js';
33

44
const test = testFactory({
5-
experimentalErrorOverlay: true,
5+
experimental: { errorOverlay: true },
66
root: './fixtures/errors/',
77
});
88

packages/astro/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@
123123
"debug": "^4.3.4",
124124
"deepmerge-ts": "^4.2.2",
125125
"diff": "^5.1.0",
126-
"es-module-lexer": "^0.10.5",
126+
"es-module-lexer": "^1.1.0",
127127
"execa": "^6.1.0",
128128
"fast-glob": "^3.2.11",
129129
"github-slugger": "^1.4.0",

packages/astro/src/@types/astro.ts

+35-4
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ export interface CLIFlags {
8383
config?: string;
8484
drafts?: boolean;
8585
experimentalErrorOverlay?: boolean;
86+
experimentalPrerender?: boolean;
8687
}
8788

8889
export interface BuildConfig {
@@ -895,11 +896,41 @@ export interface AstroUserConfig {
895896
astroFlavoredMarkdown?: boolean;
896897
};
897898

898-
/**
899-
* @hidden
900-
* Turn on experimental support for the new error overlay component.
899+
/**
900+
* @docs
901+
* @kind heading
902+
* @name Experimental Flags
903+
* @description
904+
* Astro offers experimental flags to give users early access to new features.
905+
* These flags are not guaranteed to be stable.
901906
*/
902-
experimentalErrorOverlay?: boolean;
907+
experimental?: {
908+
/**
909+
* @hidden
910+
* Turn on experimental support for the new error overlay component.
911+
*/
912+
errorOverlay?: boolean;
913+
/**
914+
* @docs
915+
* @name experimental.prerender
916+
* @type {boolean}
917+
* @default `false`
918+
* @version 1.7.0
919+
* @description
920+
* Enable experimental support for prerendered pages when generating a server.
921+
*
922+
* To enable this feature, set `experimental.prerender` to `true` in your Astro config:
923+
*
924+
* ```js
925+
* {
926+
* experimental: {
927+
* prerender: true,
928+
* },
929+
* }
930+
* ```
931+
*/
932+
prerender?: boolean;
933+
};
903934

904935
// Legacy options to be removed
905936

packages/astro/src/core/app/index.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import {
2525
createLinkStylesheetElementSet,
2626
createModuleScriptElement,
2727
} from '../render/ssr-element.js';
28-
import { matchRoute } from '../routing/match.js';
28+
import { matchAssets, matchRoute } from '../routing/match.js';
2929
export { deserializeManifest } from './common.js';
3030

3131
export const pagesVirtualModuleId = '@astrojs-pages-virtual-entry';
@@ -100,6 +100,8 @@ export class App {
100100
let routeData = matchRoute(pathname, this.#manifestData);
101101

102102
if (routeData) {
103+
const asset = matchAssets(routeData, this.#manifest.assets);
104+
if (asset) return undefined;
103105
return routeData;
104106
} else if (matchNotFound) {
105107
return matchRoute('/404', this.#manifestData);

packages/astro/src/core/build/common.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import npath from 'path';
2+
import { createHash } from 'crypto'
23
import { fileURLToPath, pathToFileURL } from 'url';
34
import type { AstroConfig, RouteType } from '../../@types/astro';
45
import { appendForwardSlash } from '../../core/path.js';
@@ -7,7 +8,11 @@ const STATUS_CODE_PAGES = new Set(['/404', '/500']);
78
const FALLBACK_OUT_DIR_NAME = './.astro/';
89

910
function getOutRoot(astroConfig: AstroConfig): URL {
10-
return new URL('./', astroConfig.outDir);
11+
if (astroConfig.output === 'static') {
12+
return new URL('./', astroConfig.outDir);
13+
} else {
14+
return new URL('./', astroConfig.build.client);
15+
}
1116
}
1217

1318
export function getOutFolder(
@@ -41,7 +46,7 @@ export function getOutFile(
4146
astroConfig: AstroConfig,
4247
outFolder: URL,
4348
pathname: string,
44-
routeType: RouteType
49+
routeType: RouteType,
4550
): URL {
4651
switch (routeType) {
4752
case 'endpoint':

packages/astro/src/core/build/generate.ts

+23-8
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import type {
1212
RouteType,
1313
SSRLoadedRenderer,
1414
} from '../../@types/astro';
15-
import type { BuildInternals } from '../../core/build/internal.js';
15+
import { BuildInternals, hasPrerenderedPages } from '../../core/build/internal.js';
1616
import {
1717
prependForwardSlash,
1818
removeLeadingForwardSlash,
@@ -29,7 +29,12 @@ import { createRequest } from '../request.js';
2929
import { matchRoute } from '../routing/match.js';
3030
import { getOutputFilename } from '../util.js';
3131
import { getOutDirWithinCwd, getOutFile, getOutFolder } from './common.js';
32-
import { eachPageData, getPageDataByComponent, sortedCSS } from './internal.js';
32+
import {
33+
eachPrerenderedPageData,
34+
eachPageData,
35+
getPageDataByComponent,
36+
sortedCSS,
37+
} from './internal.js';
3338
import type { PageBuildData, SingleFileBuiltModule, StaticBuildOptions } from './types';
3439
import { getTimeStat } from './util.js';
3540

@@ -70,17 +75,27 @@ export function chunkIsPage(
7075

7176
export async function generatePages(opts: StaticBuildOptions, internals: BuildInternals) {
7277
const timer = performance.now();
73-
info(opts.logging, null, `\n${bgGreen(black(' generating static routes '))}`);
74-
7578
const ssr = opts.settings.config.output === 'server';
7679
const serverEntry = opts.buildConfig.serverEntry;
7780
const outFolder = ssr ? opts.buildConfig.server : getOutDirWithinCwd(opts.settings.config.outDir);
81+
82+
if (opts.settings.config.experimental.prerender && opts.settings.config.output === 'server' && !hasPrerenderedPages(internals)) return;
83+
84+
const verb = ssr ? 'prerendering' : 'generating';
85+
info(opts.logging, null, `\n${bgGreen(black(` ${verb} static routes `))}`);
86+
7887
const ssrEntryURL = new URL('./' + serverEntry + `?time=${Date.now()}`, outFolder);
7988
const ssrEntry = await import(ssrEntryURL.toString());
8089
const builtPaths = new Set<string>();
8190

82-
for (const pageData of eachPageData(internals)) {
83-
await generatePage(opts, internals, pageData, ssrEntry, builtPaths);
91+
if (opts.settings.config.experimental.prerender && opts.settings.config.output === 'server') {
92+
for (const pageData of eachPrerenderedPageData(internals)) {
93+
await generatePage(opts, internals, pageData, ssrEntry, builtPaths);
94+
}
95+
} else {
96+
for (const pageData of eachPageData(internals)) {
97+
await generatePage(opts, internals, pageData, ssrEntry, builtPaths);
98+
}
8499
}
85100

86101
await runHookBuildGenerated({
@@ -106,7 +121,7 @@ async function generatePage(
106121
const linkIds: string[] = sortedCSS(pageData);
107122
const scripts = pageInfo?.hoistedScript ?? null;
108123

109-
const pageModule = ssrEntry.pageMap.get(pageData.component);
124+
const pageModule = ssrEntry.pageMap?.get(pageData.component);
110125

111126
if (!pageModule) {
112127
throw new Error(
@@ -163,7 +178,7 @@ async function getPathsForRoute(
163178
route: pageData.route,
164179
isValidate: false,
165180
logging: opts.logging,
166-
ssr: opts.settings.config.output === 'server',
181+
ssr: false,
167182
})
168183
.then((_result) => {
169184
const label = _result.staticPaths.length === 1 ? 'page' : 'pages';

packages/astro/src/core/build/internal.ts

+39-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import type { OutputChunk, RenderedChunk } from 'rollup';
2-
import type { PageBuildData, ViteID } from './types';
2+
import type { PageBuildData, PageOutput, ViteID } from './types';
33

44
import { prependForwardSlash, removeFileExtension } from '../path.js';
55
import { viteID } from '../util.js';
6+
import { PageOptions } from '../../vite-plugin-astro/types';
67

78
export interface BuildInternals {
89
/**
@@ -20,11 +21,21 @@ export interface BuildInternals {
2021
// Used to render pages with the correct specifiers.
2122
entrySpecifierToBundleMap: Map<string, string>;
2223

24+
/**
25+
* A map to get a specific page's bundled output file.
26+
*/
27+
pageToBundleMap: Map<string, string>;
28+
2329
/**
2430
* A map for page-specific information.
2531
*/
2632
pagesByComponent: Map<string, PageBuildData>;
2733

34+
/**
35+
* A map for page-specific output.
36+
*/
37+
pageOptionsByPage: Map<string, PageOptions>;
38+
2839
/**
2940
* A map for page-specific information by Vite ID (a path-like string)
3041
*/
@@ -73,8 +84,10 @@ export function createBuildInternals(): BuildInternals {
7384
hoistedScriptIdToHoistedMap,
7485
hoistedScriptIdToPagesMap,
7586
entrySpecifierToBundleMap: new Map<string, string>(),
87+
pageToBundleMap: new Map<string, string>(),
7688

7789
pagesByComponent: new Map(),
90+
pageOptionsByPage: new Map(),
7891
pagesByViteID: new Map(),
7992
pagesByClientOnly: new Map(),
8093

@@ -189,6 +202,31 @@ export function* eachPageData(internals: BuildInternals) {
189202
yield* internals.pagesByComponent.values();
190203
}
191204

205+
export function hasPrerenderedPages(internals: BuildInternals) {
206+
for (const id of internals.pagesByViteID.keys()) {
207+
if (internals.pageOptionsByPage.get(id)?.prerender) {
208+
return true
209+
}
210+
}
211+
return false
212+
}
213+
214+
export function* eachPrerenderedPageData(internals: BuildInternals) {
215+
for (const [id, pageData] of internals.pagesByViteID.entries()) {
216+
if (internals.pageOptionsByPage.get(id)?.prerender) {
217+
yield pageData;
218+
}
219+
}
220+
}
221+
222+
export function* eachServerPageData(internals: BuildInternals) {
223+
for (const [id, pageData] of internals.pagesByViteID.entries()) {
224+
if (!internals.pageOptionsByPage.get(id)?.prerender) {
225+
yield pageData;
226+
}
227+
}
228+
}
229+
192230
/**
193231
* Sort a page's CSS by depth. A higher depth means that the CSS comes from shared subcomponents.
194232
* A lower depth means it comes directly from the top-level page.

0 commit comments

Comments
 (0)