Skip to content

Commit

Permalink
Experimental Prerender API (#5297)
Browse files Browse the repository at this point in the history
* 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 <swithinbank@gmail.com>

* Update packages/astro/src/core/errors/errors-data.ts

Co-authored-by: Chris Swithinbank <swithinbank@gmail.com>

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

Co-authored-by: Chris Swithinbank <swithinbank@gmail.com>

Co-authored-by: Nate Moore <nate@astro.build>
Co-authored-by: Chris Swithinbank <swithinbank@gmail.com>
  • Loading branch information
3 people authored Dec 16, 2022
1 parent 7cbe7f5 commit d296098
Show file tree
Hide file tree
Showing 41 changed files with 701 additions and 68 deletions.
17 changes: 17 additions & 0 deletions .changeset/funny-waves-worry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
'astro': minor
'@astrojs/netlify': minor
---

Introduces the **experimental** Prerender API.

> **Note**
> This API is not yet stable and is subject to possible breaking changes!
- Deploy an Astro server without sacrificing the speed or cacheability of static HTML.
- The Prerender API allows you to statically prerender specific `pages/` at build time.

**Usage**

- First, run `astro build --experimental-prerender` or enable `experimental: { prerender: true }` in your `astro.config.mjs` file.
- Then, include `export const prerender = true` in any file in the `pages/` directory that you wish to prerender.
2 changes: 1 addition & 1 deletion packages/astro/e2e/error-cyclic.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { expect } from '@playwright/test';
import { testFactory, getErrorOverlayContent } from './test-utils.js';

const test = testFactory({
experimentalErrorOverlay: true,
experimental: { errorOverlay: true },
root: './fixtures/error-cyclic/',
});

Expand Down
2 changes: 1 addition & 1 deletion packages/astro/e2e/error-react-spectrum.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { expect } from '@playwright/test';
import { testFactory, getErrorOverlayContent } from './test-utils.js';

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

Expand Down
2 changes: 1 addition & 1 deletion packages/astro/e2e/error-sass.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { expect } from '@playwright/test';
import { testFactory, getErrorOverlayContent } from './test-utils.js';

const test = testFactory({
experimentalErrorOverlay: true,
experimental: { errorOverlay: true },
root: './fixtures/error-sass/',
});

Expand Down
2 changes: 1 addition & 1 deletion packages/astro/e2e/errors.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { expect } from '@playwright/test';
import { getErrorOverlayContent, testFactory } from './test-utils.js';

const test = testFactory({
experimentalErrorOverlay: true,
experimental: { errorOverlay: true },
root: './fixtures/errors/',
});

Expand Down
2 changes: 1 addition & 1 deletion packages/astro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@
"debug": "^4.3.4",
"deepmerge-ts": "^4.2.2",
"diff": "^5.1.0",
"es-module-lexer": "^0.10.5",
"es-module-lexer": "^1.1.0",
"execa": "^6.1.0",
"fast-glob": "^3.2.11",
"github-slugger": "^1.4.0",
Expand Down
39 changes: 35 additions & 4 deletions packages/astro/src/@types/astro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export interface CLIFlags {
config?: string;
drafts?: boolean;
experimentalErrorOverlay?: boolean;
experimentalPrerender?: boolean;
}

export interface BuildConfig {
Expand Down Expand Up @@ -895,11 +896,41 @@ export interface AstroUserConfig {
astroFlavoredMarkdown?: boolean;
};

/**
* @hidden
* Turn on experimental support for the new error overlay component.
/**
* @docs
* @kind heading
* @name Experimental Flags
* @description
* Astro offers experimental flags to give users early access to new features.
* These flags are not guaranteed to be stable.
*/
experimentalErrorOverlay?: boolean;
experimental?: {
/**
* @hidden
* Turn on experimental support for the new error overlay component.
*/
errorOverlay?: boolean;
/**
* @docs
* @name experimental.prerender
* @type {boolean}
* @default `false`
* @version 1.7.0
* @description
* Enable experimental support for prerendered pages when generating a server.
*
* To enable this feature, set `experimental.prerender` to `true` in your Astro config:
*
* ```js
* {
* experimental: {
* prerender: true,
* },
* }
* ```
*/
prerender?: boolean;
};

// Legacy options to be removed

Expand Down
4 changes: 3 additions & 1 deletion packages/astro/src/core/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
createLinkStylesheetElementSet,
createModuleScriptElement,
} from '../render/ssr-element.js';
import { matchRoute } from '../routing/match.js';
import { matchAssets, matchRoute } from '../routing/match.js';
export { deserializeManifest } from './common.js';

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

if (routeData) {
const asset = matchAssets(routeData, this.#manifest.assets);
if (asset) return undefined;
return routeData;
} else if (matchNotFound) {
return matchRoute('/404', this.#manifestData);
Expand Down
9 changes: 7 additions & 2 deletions packages/astro/src/core/build/common.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import npath from 'path';
import { createHash } from 'crypto'
import { fileURLToPath, pathToFileURL } from 'url';
import type { AstroConfig, RouteType } from '../../@types/astro';
import { appendForwardSlash } from '../../core/path.js';
Expand All @@ -7,7 +8,11 @@ const STATUS_CODE_PAGES = new Set(['/404', '/500']);
const FALLBACK_OUT_DIR_NAME = './.astro/';

function getOutRoot(astroConfig: AstroConfig): URL {
return new URL('./', astroConfig.outDir);
if (astroConfig.output === 'static') {
return new URL('./', astroConfig.outDir);
} else {
return new URL('./', astroConfig.build.client);
}
}

export function getOutFolder(
Expand Down Expand Up @@ -41,7 +46,7 @@ export function getOutFile(
astroConfig: AstroConfig,
outFolder: URL,
pathname: string,
routeType: RouteType
routeType: RouteType,
): URL {
switch (routeType) {
case 'endpoint':
Expand Down
31 changes: 23 additions & 8 deletions packages/astro/src/core/build/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import type {
RouteType,
SSRLoadedRenderer,
} from '../../@types/astro';
import type { BuildInternals } from '../../core/build/internal.js';
import { BuildInternals, hasPrerenderedPages } from '../../core/build/internal.js';
import {
prependForwardSlash,
removeLeadingForwardSlash,
Expand All @@ -29,7 +29,12 @@ import { createRequest } from '../request.js';
import { matchRoute } from '../routing/match.js';
import { getOutputFilename } from '../util.js';
import { getOutDirWithinCwd, getOutFile, getOutFolder } from './common.js';
import { eachPageData, getPageDataByComponent, sortedCSS } from './internal.js';
import {
eachPrerenderedPageData,
eachPageData,
getPageDataByComponent,
sortedCSS,
} from './internal.js';
import type { PageBuildData, SingleFileBuiltModule, StaticBuildOptions } from './types';
import { getTimeStat } from './util.js';

Expand Down Expand Up @@ -70,17 +75,27 @@ export function chunkIsPage(

export async function generatePages(opts: StaticBuildOptions, internals: BuildInternals) {
const timer = performance.now();
info(opts.logging, null, `\n${bgGreen(black(' generating static routes '))}`);

const ssr = opts.settings.config.output === 'server';
const serverEntry = opts.buildConfig.serverEntry;
const outFolder = ssr ? opts.buildConfig.server : getOutDirWithinCwd(opts.settings.config.outDir);

if (opts.settings.config.experimental.prerender && opts.settings.config.output === 'server' && !hasPrerenderedPages(internals)) return;

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>();

for (const pageData of eachPageData(internals)) {
await generatePage(opts, internals, pageData, ssrEntry, builtPaths);
if (opts.settings.config.experimental.prerender && opts.settings.config.output === 'server') {
for (const pageData of eachPrerenderedPageData(internals)) {
await generatePage(opts, internals, pageData, ssrEntry, builtPaths);
}
} else {
for (const pageData of eachPageData(internals)) {
await generatePage(opts, internals, pageData, ssrEntry, builtPaths);
}
}

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

const pageModule = ssrEntry.pageMap.get(pageData.component);
const pageModule = ssrEntry.pageMap?.get(pageData.component);

if (!pageModule) {
throw new Error(
Expand Down Expand Up @@ -163,7 +178,7 @@ async function getPathsForRoute(
route: pageData.route,
isValidate: false,
logging: opts.logging,
ssr: opts.settings.config.output === 'server',
ssr: false,
})
.then((_result) => {
const label = _result.staticPaths.length === 1 ? 'page' : 'pages';
Expand Down
40 changes: 39 additions & 1 deletion packages/astro/src/core/build/internal.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { OutputChunk, RenderedChunk } from 'rollup';
import type { PageBuildData, ViteID } from './types';
import type { PageBuildData, PageOutput, ViteID } from './types';

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

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

/**
* A map to get a specific page's bundled output file.
*/
pageToBundleMap: Map<string, string>;

/**
* A map for page-specific information.
*/
pagesByComponent: Map<string, PageBuildData>;

/**
* A map for page-specific output.
*/
pageOptionsByPage: Map<string, PageOptions>;

/**
* A map for page-specific information by Vite ID (a path-like string)
*/
Expand Down Expand Up @@ -73,8 +84,10 @@ export function createBuildInternals(): BuildInternals {
hoistedScriptIdToHoistedMap,
hoistedScriptIdToPagesMap,
entrySpecifierToBundleMap: new Map<string, string>(),
pageToBundleMap: new Map<string, string>(),

pagesByComponent: new Map(),
pageOptionsByPage: new Map(),
pagesByViteID: new Map(),
pagesByClientOnly: new Map(),

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

export function hasPrerenderedPages(internals: BuildInternals) {
for (const id of internals.pagesByViteID.keys()) {
if (internals.pageOptionsByPage.get(id)?.prerender) {
return true
}
}
return false
}

export function* eachPrerenderedPageData(internals: BuildInternals) {
for (const [id, pageData] of internals.pagesByViteID.entries()) {
if (internals.pageOptionsByPage.get(id)?.prerender) {
yield pageData;
}
}
}

export function* eachServerPageData(internals: BuildInternals) {
for (const [id, pageData] of internals.pagesByViteID.entries()) {
if (!internals.pageOptionsByPage.get(id)?.prerender) {
yield pageData;
}
}
}

/**
* Sort a page's CSS by depth. A higher depth means that the CSS comes from shared subcomponents.
* A lower depth means it comes directly from the top-level page.
Expand Down
Loading

0 comments on commit d296098

Please sign in to comment.