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 <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>
This commit is contained in:
parent
7cbe7f5623
commit
d2960984c5
41 changed files with 701 additions and 68 deletions
17
.changeset/funny-waves-worry.md
Normal file
17
.changeset/funny-waves-worry.md
Normal file
|
@ -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,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/',
|
||||
});
|
||||
|
||||
|
|
|
@ -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/',
|
||||
});
|
||||
|
||||
|
|
|
@ -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/',
|
||||
});
|
||||
|
||||
|
|
|
@ -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/',
|
||||
});
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -83,6 +83,7 @@ export interface CLIFlags {
|
|||
config?: string;
|
||||
drafts?: boolean;
|
||||
experimentalErrorOverlay?: boolean;
|
||||
experimentalPrerender?: boolean;
|
||||
}
|
||||
|
||||
export interface BuildConfig {
|
||||
|
@ -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
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
@ -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);
|
||||
|
|
|
@ -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';
|
||||
|
@ -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(
|
||||
|
@ -41,7 +46,7 @@ export function getOutFile(
|
|||
astroConfig: AstroConfig,
|
||||
outFolder: URL,
|
||||
pathname: string,
|
||||
routeType: RouteType
|
||||
routeType: RouteType,
|
||||
): URL {
|
||||
switch (routeType) {
|
||||
case 'endpoint':
|
||||
|
|
|
@ -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,
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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({
|
||||
|
@ -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(
|
||||
|
@ -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';
|
||||
|
|
|
@ -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 {
|
||||
/**
|
||||
|
@ -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)
|
||||
*/
|
||||
|
@ -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(),
|
||||
|
||||
|
@ -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.
|
||||
|
|
|
@ -4,7 +4,11 @@ import { bgGreen, bgMagenta, black, dim } from 'kleur/colors';
|
|||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import * as vite from 'vite';
|
||||
import { BuildInternals, createBuildInternals } from '../../core/build/internal.js';
|
||||
import {
|
||||
BuildInternals,
|
||||
createBuildInternals,
|
||||
eachPrerenderedPageData,
|
||||
} from '../../core/build/internal.js';
|
||||
import { emptyDir, removeDir } from '../../core/fs/index.js';
|
||||
import { prependForwardSlash } from '../../core/path.js';
|
||||
import { isModeServerWithNoAdapter } from '../../core/util.js';
|
||||
|
@ -18,11 +22,13 @@ import { trackPageData } from './internal.js';
|
|||
import type { PageBuildData, StaticBuildOptions } from './types';
|
||||
import { getTimeStat } from './util.js';
|
||||
import { vitePluginAnalyzer } from './vite-plugin-analyzer.js';
|
||||
import { vitePluginPrerender } from './vite-plugin-prerender.js';
|
||||
import { rollupPluginAstroBuildCSS } from './vite-plugin-css.js';
|
||||
import { vitePluginHoistedScripts } from './vite-plugin-hoisted-scripts.js';
|
||||
import { vitePluginInternals } from './vite-plugin-internals.js';
|
||||
import { vitePluginPages } from './vite-plugin-pages.js';
|
||||
import { injectManifest, vitePluginSSR } from './vite-plugin-ssr.js';
|
||||
import * as eslexer from 'es-module-lexer';
|
||||
|
||||
export async function staticBuild(opts: StaticBuildOptions) {
|
||||
const { allPages, settings } = opts;
|
||||
|
@ -88,15 +94,33 @@ export async function staticBuild(opts: StaticBuildOptions) {
|
|||
await clientBuild(opts, internals, clientInput);
|
||||
|
||||
timer.generate = performance.now();
|
||||
if (settings.config.output === 'static') {
|
||||
await generatePages(opts, internals);
|
||||
await cleanSsrOutput(opts);
|
||||
} else {
|
||||
// Inject the manifest
|
||||
await injectManifest(opts, internals);
|
||||
if (!settings.config.experimental.prerender) {
|
||||
if (settings.config.output === 'static') {
|
||||
await generatePages(opts, internals);
|
||||
await cleanServerOutput(opts);
|
||||
} else {
|
||||
// Inject the manifest
|
||||
await injectManifest(opts, internals);
|
||||
|
||||
info(opts.logging, null, `\n${bgMagenta(black(' finalizing server assets '))}\n`);
|
||||
await ssrMoveAssets(opts);
|
||||
info(opts.logging, null, `\n${bgMagenta(black(' finalizing server assets '))}\n`);
|
||||
await ssrMoveAssets(opts);
|
||||
}
|
||||
} else {
|
||||
switch (settings.config.output) {
|
||||
case 'static': {
|
||||
await generatePages(opts, internals);
|
||||
await cleanServerOutput(opts);
|
||||
return;
|
||||
}
|
||||
case 'server': {
|
||||
await injectManifest(opts, internals);
|
||||
await generatePages(opts, internals);
|
||||
await cleanStaticOutput(opts, internals);
|
||||
info(opts.logging, null, `\n${bgMagenta(black(' finalizing server assets '))}\n`);
|
||||
await ssrMoveAssets(opts);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -134,6 +158,7 @@ async function ssrBuild(opts: StaticBuildOptions, internals: BuildInternals, inp
|
|||
reportCompressedSize: false,
|
||||
},
|
||||
plugins: [
|
||||
vitePluginAnalyzer(internals),
|
||||
vitePluginInternals(input, internals),
|
||||
vitePluginPages(opts, internals),
|
||||
rollupPluginAstroBuildCSS({
|
||||
|
@ -141,10 +166,10 @@ async function ssrBuild(opts: StaticBuildOptions, internals: BuildInternals, inp
|
|||
internals,
|
||||
target: 'server',
|
||||
}),
|
||||
vitePluginPrerender(opts, internals),
|
||||
...(viteConfig.plugins || []),
|
||||
// SSR needs to be last
|
||||
settings.config.output === 'server' && vitePluginSSR(internals, settings.adapter!),
|
||||
vitePluginAnalyzer(internals),
|
||||
ssr && vitePluginSSR(internals, settings.adapter!),
|
||||
],
|
||||
envPrefix: 'PUBLIC_',
|
||||
base: settings.config.base,
|
||||
|
@ -169,7 +194,12 @@ async function clientBuild(
|
|||
const { settings, viteConfig } = opts;
|
||||
const timer = performance.now();
|
||||
const ssr = settings.config.output === 'server';
|
||||
const out = ssr ? opts.buildConfig.client : settings.config.outDir;
|
||||
let out;
|
||||
if (!opts.settings.config.experimental.prerender) {
|
||||
out = ssr ? opts.buildConfig.client : settings.config.outDir;
|
||||
} else {
|
||||
out = ssr ? opts.buildConfig.client : getOutDirWithinCwd(settings.config.outDir);
|
||||
}
|
||||
|
||||
// Nothing to do if there is no client-side JS.
|
||||
if (!input.size) {
|
||||
|
@ -232,7 +262,77 @@ async function clientBuild(
|
|||
return buildResult;
|
||||
}
|
||||
|
||||
async function cleanSsrOutput(opts: StaticBuildOptions) {
|
||||
/**
|
||||
* For each statically prerendered page, replace their SSR file with a noop.
|
||||
* This allows us to run the SSR build only once, but still remove dependencies for statically rendered routes.
|
||||
*/
|
||||
async function cleanStaticOutput(opts: StaticBuildOptions, internals: BuildInternals) {
|
||||
const allStaticFiles = new Set();
|
||||
for (const pageData of eachPrerenderedPageData(internals)) {
|
||||
allStaticFiles.add(internals.pageToBundleMap.get(pageData.moduleSpecifier));
|
||||
}
|
||||
const ssr = opts.settings.config.output === 'server';
|
||||
const out = ssr ? opts.buildConfig.server : getOutDirWithinCwd(opts.settings.config.outDir);
|
||||
// The SSR output is all .mjs files, the client output is not.
|
||||
const files = await glob('**/*.mjs', {
|
||||
cwd: fileURLToPath(out),
|
||||
});
|
||||
|
||||
if (files.length) {
|
||||
await eslexer.init;
|
||||
|
||||
// Cleanup prerendered chunks.
|
||||
// This has to happen AFTER the SSR build runs as a final step, because we need the code in order to generate the pages.
|
||||
// These chunks should only contain prerendering logic, so they are safe to modify.
|
||||
await Promise.all(
|
||||
files.map(async (filename) => {
|
||||
if (!allStaticFiles.has(filename)) {
|
||||
return;
|
||||
}
|
||||
const url = new URL(filename, out);
|
||||
const text = await fs.promises.readFile(url, { encoding: 'utf8' });
|
||||
const [, exports] = eslexer.parse(text);
|
||||
// Replace exports (only prerendered pages) with a noop
|
||||
let value = 'const noop = () => {};';
|
||||
for (const e of exports) {
|
||||
value += `\nexport const ${e.n} = noop;`;
|
||||
}
|
||||
await fs.promises.writeFile(url, value, { encoding: 'utf8' });
|
||||
})
|
||||
);
|
||||
// Map directories heads from the .mjs files
|
||||
const directories: Set<string> = new Set();
|
||||
files.forEach((i) => {
|
||||
const splitFilePath = i.split(path.sep);
|
||||
// If the path is more than just a .mjs filename itself
|
||||
if (splitFilePath.length > 1) {
|
||||
directories.add(splitFilePath[0]);
|
||||
}
|
||||
});
|
||||
// Attempt to remove only those folders which are empty
|
||||
await Promise.all(
|
||||
Array.from(directories).map(async (filename) => {
|
||||
const url = new URL(filename, out);
|
||||
const folder = await fs.promises.readdir(url);
|
||||
if (!folder.length) {
|
||||
await fs.promises.rm(url, { recursive: true, force: true });
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (!opts.settings.config.experimental.prerender) {
|
||||
// Clean out directly if the outDir is outside of root
|
||||
if (out.toString() !== opts.settings.config.outDir.toString()) {
|
||||
// Copy assets before cleaning directory if outside root
|
||||
copyFiles(out, opts.settings.config.outDir);
|
||||
await fs.promises.rm(out, { recursive: true });
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanServerOutput(opts: StaticBuildOptions) {
|
||||
const out = getOutDirWithinCwd(opts.settings.config.outDir);
|
||||
// The SSR output is all .mjs files, the client output is not.
|
||||
const files = await glob('**/*.mjs', {
|
||||
|
@ -259,8 +359,8 @@ async function cleanSsrOutput(opts: StaticBuildOptions) {
|
|||
await Promise.all(
|
||||
Array.from(directories).map(async (filename) => {
|
||||
const url = new URL(filename, out);
|
||||
const folder = await fs.promises.readdir(url);
|
||||
if (!folder.length) {
|
||||
const dir = await glob(fileURLToPath(url), { absolute: true });
|
||||
if (!dir.length) {
|
||||
await fs.promises.rm(url, { recursive: true, force: true });
|
||||
}
|
||||
})
|
||||
|
@ -303,16 +403,16 @@ async function ssrMoveAssets(opts: StaticBuildOptions) {
|
|||
cwd: fileURLToPath(serverRoot),
|
||||
});
|
||||
|
||||
// Make the directory
|
||||
await fs.promises.mkdir(clientAssets, { recursive: true });
|
||||
|
||||
await Promise.all(
|
||||
files.map(async (filename) => {
|
||||
const currentUrl = new URL(filename, serverRoot);
|
||||
const clientUrl = new URL(filename, clientRoot);
|
||||
return fs.promises.rename(currentUrl, clientUrl);
|
||||
})
|
||||
);
|
||||
|
||||
removeDir(serverAssets);
|
||||
if (files.length > 0) {
|
||||
// Make the directory
|
||||
await fs.promises.mkdir(clientAssets, { recursive: true });
|
||||
await Promise.all(
|
||||
files.map(async (filename) => {
|
||||
const currentUrl = new URL(filename, serverRoot);
|
||||
const clientUrl = new URL(filename, clientRoot);
|
||||
return fs.promises.rename(currentUrl, clientUrl);
|
||||
})
|
||||
);
|
||||
removeDir(serverAssets);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import type { InlineConfig } from 'vite';
|
||||
import type {
|
||||
AstroConfig,
|
||||
AstroSettings,
|
||||
BuildConfig,
|
||||
ComponentInstance,
|
||||
|
@ -13,6 +14,7 @@ import type { RouteCache } from '../render/route-cache';
|
|||
|
||||
export type ComponentPath = string;
|
||||
export type ViteID = string;
|
||||
export type PageOutput = AstroConfig['output']
|
||||
|
||||
export interface PageBuildData {
|
||||
component: ComponentPath;
|
||||
|
|
|
@ -74,12 +74,18 @@ export function vitePluginAnalyzer(internals: BuildInternals): VitePlugin {
|
|||
const hoistScanner = hoistedScriptScanner();
|
||||
|
||||
const ids = this.getModuleIds();
|
||||
|
||||
for (const id of ids) {
|
||||
const info = this.getModuleInfo(id);
|
||||
if (!info || !info.meta?.astro) continue;
|
||||
|
||||
const astro = info.meta.astro as AstroPluginMetadata['astro'];
|
||||
|
||||
const pageData = getPageDataByViteID(internals, id);
|
||||
if (pageData) {
|
||||
internals.pageOptionsByPage.set(id, astro.pageOptions);
|
||||
}
|
||||
|
||||
for (const c of astro.hydratedComponents) {
|
||||
const rid = c.resolvedPath ? decodeURI(c.resolvedPath) : c.specifier;
|
||||
internals.discoveredHydratedComponents.add(rid);
|
||||
|
@ -103,10 +109,10 @@ export function vitePluginAnalyzer(internals: BuildInternals): VitePlugin {
|
|||
}
|
||||
|
||||
for (const [pageInfo] of getTopLevelPages(id, this)) {
|
||||
const pageData = getPageDataByViteID(internals, pageInfo.id);
|
||||
if (!pageData) continue;
|
||||
const newPageData = getPageDataByViteID(internals, pageInfo.id);
|
||||
if (!newPageData) continue;
|
||||
|
||||
trackClientOnlyPageDatas(internals, pageData, clientOnlys);
|
||||
trackClientOnlyPageDatas(internals, newPageData, clientOnlys);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,6 +48,13 @@ export function vitePluginInternals(input: Set<string>, internals: BuildInternal
|
|||
if (chunk.type === 'chunk' && chunk.facadeModuleId) {
|
||||
const specifier = mapping.get(chunk.facadeModuleId) || chunk.facadeModuleId;
|
||||
internals.entrySpecifierToBundleMap.set(specifier, chunk.fileName);
|
||||
} else if (chunk.type === 'chunk') {
|
||||
for (const id of Object.keys(chunk.modules)) {
|
||||
const pageData = internals.pagesByViteID.get(id);
|
||||
if (pageData) {
|
||||
internals.pageToBundleMap.set(pageData.moduleSpecifier, chunk.fileName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import type { Plugin as VitePlugin } from 'vite';
|
||||
import { pagesVirtualModuleId, resolvedPagesVirtualModuleId } from '../app/index.js';
|
||||
import { addRollupInput } from './add-rollup-input.js';
|
||||
import type { BuildInternals } from './internal.js';
|
||||
import { BuildInternals, hasPrerenderedPages } from './internal.js';
|
||||
import { eachPageData } from './internal.js';
|
||||
import type { StaticBuildOptions } from './types';
|
||||
|
||||
|
@ -10,7 +10,7 @@ export function vitePluginPages(opts: StaticBuildOptions, internals: BuildIntern
|
|||
name: '@astro/plugin-build-pages',
|
||||
|
||||
options(options) {
|
||||
if (opts.settings.config.output === 'static') {
|
||||
if (opts.settings.config.output === 'static' || hasPrerenderedPages(internals)) {
|
||||
return addRollupInput(options, [pagesVirtualModuleId]);
|
||||
}
|
||||
},
|
||||
|
|
43
packages/astro/src/core/build/vite-plugin-prerender.ts
Normal file
43
packages/astro/src/core/build/vite-plugin-prerender.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
import type { Plugin as VitePlugin } from 'vite';
|
||||
import type { BuildInternals } from './internal.js';
|
||||
import type { StaticBuildOptions } from './types';
|
||||
|
||||
export function vitePluginPrerender(opts: StaticBuildOptions, internals: BuildInternals): VitePlugin {
|
||||
return {
|
||||
name: 'astro:rollup-plugin-prerender',
|
||||
|
||||
outputOptions(outputOptions) {
|
||||
// No-op if `prerender` is not enabled
|
||||
if (!opts.settings.config.experimental.prerender) return;
|
||||
|
||||
const manualChunks = outputOptions.manualChunks || Function.prototype;
|
||||
outputOptions.manualChunks = function (id, api, ...args) {
|
||||
// Defer to user-provided `manualChunks`, if it was provided.
|
||||
if (typeof manualChunks == 'object') {
|
||||
if (id in manualChunks) {
|
||||
return manualChunks[id];
|
||||
}
|
||||
} else if (typeof manualChunks === 'function') {
|
||||
const outid = manualChunks.call(this, id, api, ...args);
|
||||
if (outid) {
|
||||
return outid;
|
||||
}
|
||||
}
|
||||
// Split the Astro runtime into a separate chunk for readability
|
||||
if (id.includes('astro/dist')) {
|
||||
return 'astro';
|
||||
}
|
||||
const pageInfo = internals.pagesByViteID.get(id);
|
||||
if (pageInfo) {
|
||||
// prerendered pages should be split into their own chunk
|
||||
// Important: this can't be in the `pages/` directory!
|
||||
if (api.getModuleInfo(id)?.meta.astro?.pageOptions?.prerender) {
|
||||
return `prerender`;
|
||||
}
|
||||
// pages should go in their own chunks/pages/* directory
|
||||
return `pages${pageInfo.route.route.replace(/\/$/, '/index')}`;
|
||||
}
|
||||
};
|
||||
},
|
||||
}
|
||||
}
|
|
@ -13,7 +13,8 @@ import { pagesVirtualModuleId } from '../app/index.js';
|
|||
import { removeLeadingForwardSlash, removeTrailingForwardSlash } from '../path.js';
|
||||
import { serializeRouteData } from '../routing/index.js';
|
||||
import { addRollupInput } from './add-rollup-input.js';
|
||||
import { eachPageData, sortedCSS } from './internal.js';
|
||||
import { eachServerPageData, eachPrerenderedPageData, sortedCSS } from './internal.js';
|
||||
import { getOutFile, getOutFolder } from './common.js';
|
||||
|
||||
export const virtualModuleId = '@astrojs-ssr-virtual-entry';
|
||||
const resolvedVirtualModuleId = '\0' + virtualModuleId;
|
||||
|
@ -43,6 +44,8 @@ const _manifest = Object.assign(_deserializeManifest('${manifestReplace}'), {
|
|||
});
|
||||
const _args = ${adapter.args ? JSON.stringify(adapter.args) : 'undefined'};
|
||||
|
||||
export * from '${pagesVirtualModuleId}';
|
||||
|
||||
${
|
||||
adapter.exports
|
||||
? `const _exports = adapter.createExports(_manifest, _args);
|
||||
|
@ -136,7 +139,20 @@ function buildManifest(
|
|||
const bareBase = removeTrailingForwardSlash(removeLeadingForwardSlash(settings.config.base));
|
||||
const joinBase = (pth: string) => (bareBase ? bareBase + '/' + pth : pth);
|
||||
|
||||
for (const pageData of eachPageData(internals)) {
|
||||
for (const pageData of eachPrerenderedPageData(internals)) {
|
||||
const outFolder = getOutFolder(opts.settings.config, pageData.route.pathname!, pageData.route.type);
|
||||
const outFile = getOutFile(opts.settings.config, outFolder, pageData.route.pathname!, pageData.route.type);
|
||||
const file = outFile.toString().replace(opts.settings.config.build.client.toString(), '');
|
||||
routes.push({
|
||||
file,
|
||||
links: [],
|
||||
scripts: [],
|
||||
routeData: serializeRouteData(pageData.route, settings.config.trailingSlash),
|
||||
});
|
||||
staticFiles.push(file);
|
||||
}
|
||||
|
||||
for (const pageData of eachServerPageData(internals)) {
|
||||
const scripts: SerializedRouteInfo['scripts'] = [];
|
||||
if (pageData.hoistedScript) {
|
||||
scripts.unshift(
|
||||
|
|
|
@ -104,6 +104,10 @@ export function resolveFlags(flags: Partial<Flags>): CLIFlags {
|
|||
typeof flags.experimentalErrorOverlay === 'boolean'
|
||||
? flags.experimentalErrorOverlay
|
||||
: undefined,
|
||||
experimentalPrerender:
|
||||
typeof flags.experimentalPrerender === 'boolean'
|
||||
? flags.experimentalPrerender
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -118,6 +122,7 @@ export function resolveRoot(cwd?: string | URL): string {
|
|||
function mergeCLIFlags(astroConfig: AstroUserConfig, flags: CLIFlags, cmd: string) {
|
||||
astroConfig.server = astroConfig.server || {};
|
||||
astroConfig.markdown = astroConfig.markdown || {};
|
||||
astroConfig.experimental = astroConfig.experimental || {};
|
||||
if (typeof flags.site === 'string') astroConfig.site = flags.site;
|
||||
if (typeof flags.base === 'string') astroConfig.base = flags.base;
|
||||
if (typeof flags.drafts === 'boolean') astroConfig.markdown.drafts = flags.drafts;
|
||||
|
@ -131,7 +136,8 @@ function mergeCLIFlags(astroConfig: AstroUserConfig, flags: CLIFlags, cmd: strin
|
|||
// TODO: Come back here and refactor to remove this expected error.
|
||||
astroConfig.server.host = flags.host;
|
||||
}
|
||||
astroConfig.experimentalErrorOverlay = flags.experimentalErrorOverlay ?? false;
|
||||
if (flags.experimentalErrorOverlay) astroConfig.experimental.errorOverlay = true;
|
||||
if (flags.experimentalPrerender) astroConfig.experimental.prerender = true;
|
||||
return astroConfig;
|
||||
}
|
||||
|
||||
|
|
|
@ -47,7 +47,10 @@ const ASTRO_CONFIG_DEFAULTS: AstroUserConfig & any = {
|
|||
legacy: {
|
||||
astroFlavoredMarkdown: false,
|
||||
},
|
||||
experimentalErrorOverlay: false,
|
||||
experimental: {
|
||||
errorOverlay: false,
|
||||
prerender: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const AstroConfigSchema = z.object({
|
||||
|
@ -188,6 +191,13 @@ export const AstroConfigSchema = z.object({
|
|||
vite: z
|
||||
.custom<ViteUserConfig>((data) => data instanceof Object && !Array.isArray(data))
|
||||
.default(ASTRO_CONFIG_DEFAULTS.vite),
|
||||
experimental: z
|
||||
.object({
|
||||
errorOverlay: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.errorOverlay),
|
||||
prerender: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.prerender),
|
||||
})
|
||||
.optional()
|
||||
.default({}),
|
||||
legacy: z
|
||||
.object({
|
||||
astroFlavoredMarkdown: z
|
||||
|
@ -197,7 +207,6 @@ export const AstroConfigSchema = z.object({
|
|||
})
|
||||
.optional()
|
||||
.default({}),
|
||||
experimentalErrorOverlay: z.boolean().optional().default(false),
|
||||
});
|
||||
|
||||
interface PostCSSConfigResult {
|
||||
|
|
|
@ -20,6 +20,7 @@ import markdownVitePlugin from '../vite-plugin-markdown/index.js';
|
|||
import astroScriptsPlugin from '../vite-plugin-scripts/index.js';
|
||||
import astroScriptsPageSSRPlugin from '../vite-plugin-scripts/page-ssr.js';
|
||||
import { createCustomViteLogger } from './errors/dev/index.js';
|
||||
import astroScannerPlugin from '../vite-plugin-scanner/index.js';
|
||||
import { resolveDependency } from './util.js';
|
||||
|
||||
interface CreateViteOptions {
|
||||
|
@ -114,6 +115,7 @@ export async function createVite(
|
|||
astroIntegrationsContainerPlugin({ settings, logging }),
|
||||
astroScriptsPageSSRPlugin({ settings }),
|
||||
astroHeadPropagationPlugin({ settings }),
|
||||
settings.config.experimental.prerender && astroScannerPlugin({ settings, logging }),
|
||||
],
|
||||
publicDir: fileURLToPath(settings.config.publicDir),
|
||||
root: fileURLToPath(settings.config.root),
|
||||
|
|
|
@ -393,6 +393,22 @@ See https://docs.astro.build/en/guides/server-side-rendering/ for more informati
|
|||
`Could not render \`${componentName}\`. No matching import has been found for \`${componentName}\`.`,
|
||||
hint: 'Please make sure the component is properly imported.',
|
||||
},
|
||||
/**
|
||||
* @docs
|
||||
* @description
|
||||
* A `prerender` export was detected, but the value was not statically analyzable. Values computed at runtime are not supported, so `export const prerender` can only be set to `true` or `false`. Variables are not supported.
|
||||
*/
|
||||
InvalidPrerenderExport: {
|
||||
title: 'Invalid prerender export.',
|
||||
code: 3019,
|
||||
message: (prefix: string, suffix: string) => {
|
||||
let msg = `A \`prerender\` export has been detected, but its value cannot be statically analyzed.`;
|
||||
if (prefix !== 'const') msg += `\nExpected \`const\` declaration but got \`${prefix}\`.`
|
||||
if (suffix !== 'true') msg += `\nExpected \`true\` value but got \`${suffix}\`.`
|
||||
return msg;
|
||||
},
|
||||
hint: 'Mutable values declared at runtime are not supported. Please make sure to use exactly `export const prerender = true`.',
|
||||
},
|
||||
// Vite Errors - 4xxx
|
||||
UnknownViteError: {
|
||||
title: 'Unknown Vite Error.',
|
||||
|
|
|
@ -561,7 +561,7 @@ function getOverlayCode() {
|
|||
}
|
||||
|
||||
export function patchOverlay(code: string, config: AstroConfig) {
|
||||
if (config.experimentalErrorOverlay) {
|
||||
if (config.experimental.errorOverlay) {
|
||||
return code.replace('class ErrorOverlay', getOverlayCode() + '\nclass ViteErrorOverlay');
|
||||
} else {
|
||||
// Legacy overlay
|
||||
|
|
|
@ -5,6 +5,19 @@ export function matchRoute(pathname: string, manifest: ManifestData): RouteData
|
|||
return manifest.routes.find((route) => route.pattern.test(pathname));
|
||||
}
|
||||
|
||||
/** Find matching static asset from pathname */
|
||||
export function matchAssets(route: RouteData, assets: Set<string>): string | undefined {
|
||||
for (const asset of assets) {
|
||||
if (!asset.endsWith('.html')) continue;
|
||||
if (route.pattern.test(asset)) {
|
||||
return asset;
|
||||
}
|
||||
if (route.pattern.test(asset.replace(/index\.html$/, ''))) {
|
||||
return asset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Finds all matching routes from pathname */
|
||||
export function matchAllRoutes(pathname: string, manifest: ManifestData): RouteData[] {
|
||||
return manifest.routes.filter((route) => route.pattern.test(pathname));
|
||||
|
|
|
@ -140,6 +140,12 @@ export function isPage(file: URL, settings: AstroSettings): boolean {
|
|||
return endsWithPageExt(file, settings);
|
||||
}
|
||||
|
||||
export function isEndpoint(file: URL, settings: AstroSettings): boolean {
|
||||
if (!isInPagesDir(file, settings.config)) return false;
|
||||
if (!isPublicRoute(file, settings.config)) return false;
|
||||
return !endsWithPageExt(file, settings);
|
||||
}
|
||||
|
||||
export function isModeServerWithNoAdapter(settings: AstroSettings): boolean {
|
||||
return settings.config.output === 'server' && !settings.adapter;
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import fs from 'node:fs';
|
||||
import { bold } from 'kleur/colors';
|
||||
import type { AddressInfo } from 'net';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
@ -345,6 +346,7 @@ export async function runHookBuildDone({
|
|||
logging: LogOptions;
|
||||
}) {
|
||||
const dir = config.output === 'server' ? buildConfig.client : config.outDir;
|
||||
await fs.promises.mkdir(dir, { recursive: true });
|
||||
|
||||
for (const integration of config.integrations) {
|
||||
if (integration?.hooks?.['astro:build:done']) {
|
||||
|
|
|
@ -146,6 +146,7 @@ export default function astroJSX(): PluginObj {
|
|||
hydratedComponents: [],
|
||||
scripts: [],
|
||||
propagation: 'none',
|
||||
pageOptions: {},
|
||||
};
|
||||
}
|
||||
path.node.body.splice(
|
||||
|
|
|
@ -220,6 +220,7 @@ export default function astro({ settings, logging }: AstroPluginOptions): vite.P
|
|||
hydratedComponents: transformResult.hydratedComponents,
|
||||
scripts: transformResult.scripts,
|
||||
propagation: 'none',
|
||||
pageOptions: {},
|
||||
};
|
||||
|
||||
return {
|
||||
|
|
|
@ -1,11 +1,16 @@
|
|||
import type { TransformResult } from '@astrojs/compiler';
|
||||
import type { PropagationHint } from '../@types/astro';
|
||||
|
||||
export interface PageOptions {
|
||||
prerender?: boolean;
|
||||
}
|
||||
|
||||
export interface PluginMetadata {
|
||||
astro: {
|
||||
hydratedComponents: TransformResult['hydratedComponents'];
|
||||
clientOnlyComponents: TransformResult['clientOnlyComponents'];
|
||||
scripts: TransformResult['scripts'];
|
||||
propagation: PropagationHint;
|
||||
pageOptions: PageOptions;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -234,6 +234,7 @@ ${tsResult}`;
|
|||
hydratedComponents: transformResult.hydratedComponents,
|
||||
scripts: transformResult.scripts,
|
||||
propagation: 'none',
|
||||
pageOptions: {},
|
||||
};
|
||||
|
||||
return {
|
||||
|
|
|
@ -158,6 +158,7 @@ export default function markdown({ settings, logging }: AstroPluginOptions): Plu
|
|||
clientOnlyComponents: [],
|
||||
scripts: [],
|
||||
propagation: 'none',
|
||||
pageOptions: {},
|
||||
} as PluginMetadata['astro'],
|
||||
vite: {
|
||||
lang: 'ts',
|
||||
|
|
44
packages/astro/src/vite-plugin-scanner/index.ts
Normal file
44
packages/astro/src/vite-plugin-scanner/index.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
import { Plugin as VitePlugin } from 'vite';
|
||||
import { AstroSettings } from '../@types/astro.js';
|
||||
import { isPage, isEndpoint } from '../core/util.js';
|
||||
import type { LogOptions } from '../core/logger/core.js';
|
||||
import { normalizeFilename } from '../vite-plugin-utils/index.js';
|
||||
|
||||
import { scan } from './scan.js';
|
||||
|
||||
export default function astroScannerPlugin({ settings, logging }: { settings: AstroSettings, logging: LogOptions }): VitePlugin {
|
||||
return {
|
||||
name: 'astro:scanner',
|
||||
enforce: 'post',
|
||||
|
||||
async transform(this, code, id, options) {
|
||||
if (!options?.ssr) return;
|
||||
|
||||
const filename = normalizeFilename(id, settings.config);
|
||||
let fileURL: URL;
|
||||
try {
|
||||
fileURL = new URL(`file://${filename}`);
|
||||
} catch (e) {
|
||||
// If we can't construct a valid URL, exit early
|
||||
return;
|
||||
}
|
||||
|
||||
const fileIsPage = isPage(fileURL, settings);
|
||||
const fileIsEndpoint = isEndpoint(fileURL, settings);
|
||||
if (!(fileIsPage || fileIsEndpoint)) return;
|
||||
const pageOptions = await scan(code, id)
|
||||
|
||||
const { meta = {} } = this.getModuleInfo(id) ?? {};
|
||||
return {
|
||||
code,
|
||||
meta: {
|
||||
...meta,
|
||||
astro: {
|
||||
...(meta.astro ?? { hydratedComponents: [], clientOnlyComponents: [], scripts: [] }),
|
||||
pageOptions,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
48
packages/astro/src/vite-plugin-scanner/scan.ts
Normal file
48
packages/astro/src/vite-plugin-scanner/scan.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
import * as eslexer from 'es-module-lexer';
|
||||
import { PageOptions } from '../vite-plugin-astro/types.js';
|
||||
import { AstroError, AstroErrorCodes, AstroErrorData } from '../core/errors/index.js'
|
||||
|
||||
const BOOLEAN_EXPORTS = new Set(['prerender']);
|
||||
|
||||
// Quick scan to determine if code includes recognized export
|
||||
// False positives are not a problem, so be forgiving!
|
||||
function includesExport(code: string) {
|
||||
for (const name of BOOLEAN_EXPORTS) {
|
||||
if (code.includes(name)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
let didInit = false;
|
||||
|
||||
export async function scan(code: string, id: string): Promise<PageOptions> {
|
||||
if (!includesExport(code)) return {};
|
||||
if (!didInit) {
|
||||
await eslexer.init;
|
||||
didInit = true;
|
||||
}
|
||||
|
||||
const [_, exports] = eslexer.parse(code, id);
|
||||
let pageOptions: PageOptions = {};
|
||||
for (const _export of exports) {
|
||||
const { n: name, le: endOfLocalName } = _export;
|
||||
if (BOOLEAN_EXPORTS.has(name)) {
|
||||
// For a given export, check the value of the local declaration
|
||||
// Basically extract the `const` from the statement `export const prerender = true`
|
||||
const prefix = code.slice(0, endOfLocalName).split('export').pop()!.trim().replace('prerender', '').trim();
|
||||
// For a given export, check the value of the first non-whitespace token.
|
||||
// Basically extract the `true` from the statement `export const prerender = true`
|
||||
const suffix = code.slice(endOfLocalName).trim().replace(/\=/, '').trim().split(/[;\n]/)[0];
|
||||
if (prefix !== 'const' || !(suffix === 'true' || suffix === 'false')) {
|
||||
throw new AstroError({
|
||||
...AstroErrorData.InvalidPrerenderExport,
|
||||
message: AstroErrorData.InvalidPrerenderExport.message(prefix, suffix),
|
||||
location: { file: id }
|
||||
});
|
||||
} else {
|
||||
pageOptions[name as keyof PageOptions] = suffix === 'true';
|
||||
}
|
||||
}
|
||||
}
|
||||
return pageOptions;
|
||||
}
|
8
packages/astro/test/fixtures/ssr-prerender/package.json
vendored
Normal file
8
packages/astro/test/fixtures/ssr-prerender/package.json
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"name": "@test/ssr-prerender",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"astro": "workspace:*"
|
||||
}
|
||||
}
|
18
packages/astro/test/fixtures/ssr-prerender/src/pages/static.astro
vendored
Normal file
18
packages/astro/test/fixtures/ssr-prerender/src/pages/static.astro
vendored
Normal file
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
export const prerender = true;
|
||||
|
||||
const { searchParams } = Astro.url;
|
||||
---
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title>Static Page</title>
|
||||
<script>
|
||||
console.log('hello world');
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<h1 id="greeting">Hello world!</h1>
|
||||
<div id="searchparams">{searchParams.get('q')}</div>
|
||||
</body>
|
||||
</html>
|
12
packages/astro/test/fixtures/ssr-prerender/src/pages/users/[id].astro
vendored
Normal file
12
packages/astro/test/fixtures/ssr-prerender/src/pages/users/[id].astro
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
const { id } = Astro.params;
|
||||
---
|
||||
<html>
|
||||
<head>
|
||||
<title>Testing</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Testing</h1>
|
||||
<h2 class="user">{ id }</h2>
|
||||
</body>
|
||||
</html>
|
52
packages/astro/test/ssr-prerender.test.js
Normal file
52
packages/astro/test/ssr-prerender.test.js
Normal file
|
@ -0,0 +1,52 @@
|
|||
import { expect } from 'chai';
|
||||
import * as cheerio from 'cheerio';
|
||||
import { loadFixture } from './test-utils.js';
|
||||
import testAdapter from './test-adapter.js';
|
||||
|
||||
describe('SSR: prerender', () => {
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
let fixture;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/ssr-prerender/',
|
||||
output: 'server',
|
||||
adapter: testAdapter(),
|
||||
experimental: {
|
||||
prerender: true,
|
||||
},
|
||||
});
|
||||
await fixture.build();
|
||||
});
|
||||
|
||||
describe('Prerendering', () => {
|
||||
// Prerendered assets are not served directly by `app`,
|
||||
// they're served _in front of_ the app as static assets!
|
||||
it('Does not render static page', async () => {
|
||||
const app = await fixture.loadTestAdapterApp();
|
||||
const request = new Request('http://example.com/static');
|
||||
const response = await app.render(request);
|
||||
expect(response.status).to.equal(404);
|
||||
});
|
||||
|
||||
it('includes prerendered pages in the asset manifest', async () => {
|
||||
const app = await fixture.loadTestAdapterApp();
|
||||
/** @type {Set<string>} */
|
||||
const assets = app.manifest.assets;
|
||||
expect(assets.size).to.equal(1);
|
||||
expect(Array.from(assets)[0].endsWith('static/index.html')).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe('Astro.params in SSR', () => {
|
||||
it('Params are passed to component', async () => {
|
||||
const app = await fixture.loadTestAdapterApp();
|
||||
const request = new Request('http://example.com/users/houston');
|
||||
const response = await app.render(request);
|
||||
expect(response.status).to.equal(200);
|
||||
const html = await response.text();
|
||||
const $ = cheerio.load(html);
|
||||
expect($('.user').text()).to.equal('houston');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -70,8 +70,9 @@ const name = 'world
|
|||
const result = await compile(`<h1>Hello World</h1>`, '/src/components/index.astro');
|
||||
await init;
|
||||
const [, exports] = parse(result.code);
|
||||
expect(exports).to.include('default');
|
||||
expect(exports).to.include('file');
|
||||
expect(exports).to.include('url');
|
||||
const names = exports.map(e => e.n);
|
||||
expect(names).to.include('default');
|
||||
expect(names).to.include('file');
|
||||
expect(names).to.include('url');
|
||||
});
|
||||
});
|
||||
|
|
79
packages/astro/test/units/vite-plugin-scanner/scan.test.js
Normal file
79
packages/astro/test/units/vite-plugin-scanner/scan.test.js
Normal file
|
@ -0,0 +1,79 @@
|
|||
import { expect } from 'chai';
|
||||
import { scan } from '../../../dist/vite-plugin-scanner/scan.js';
|
||||
|
||||
describe('astro scan', () => {
|
||||
it('should return empty object', async () => {
|
||||
const result = await scan(`export {}`, '/src/components/index.astro');
|
||||
expect(Object.keys(result).length).to.equal(0);
|
||||
});
|
||||
|
||||
it('recognizes constant boolean literal (false)', async () => {
|
||||
const result = await scan(`export const prerender = true;`, '/src/components/index.astro');
|
||||
expect(result.prerender).to.equal(true);
|
||||
});
|
||||
|
||||
it('recognizes constant boolean literal (false)', async () => {
|
||||
const result = await scan(`export const prerender = false;`, '/src/components/index.astro');
|
||||
expect(result.prerender).to.equal(false);
|
||||
});
|
||||
|
||||
it('throws on let boolean literal', async () => {
|
||||
try {
|
||||
const result = await scan(`export let prerender = true;`, '/src/components/index.astro');
|
||||
expect(false).to.be.true;
|
||||
} catch (e) {
|
||||
expect(e.errorCode).to.equal(3019);
|
||||
expect(e.message).to.contain(`A \`prerender\` export has been detected, but its value cannot be statically analyzed.`);
|
||||
}
|
||||
});
|
||||
|
||||
it('throws on var boolean literal', async () => {
|
||||
try {
|
||||
const result = await scan(`export var prerender = true;`, '/src/components/index.astro');
|
||||
expect(false).to.be.true;
|
||||
} catch (e) {
|
||||
expect(e.errorCode).to.equal(3019);
|
||||
expect(e.message).to.contain(`A \`prerender\` export has been detected, but its value cannot be statically analyzed.`);
|
||||
}
|
||||
});
|
||||
|
||||
it('throws on unknown values I', async () => {
|
||||
try {
|
||||
const result = await scan(`export const prerender = !!value;`, '/src/components/index.astro');
|
||||
expect(false).to.be.true;
|
||||
} catch (e) {
|
||||
expect(e.errorCode).to.equal(3019);
|
||||
expect(e.message).to.contain(`A \`prerender\` export has been detected, but its value cannot be statically analyzed.`);
|
||||
}
|
||||
});
|
||||
|
||||
it('throws on unknown values II', async () => {
|
||||
try {
|
||||
const result = await scan(`export const prerender = value;`, '/src/components/index.astro');
|
||||
expect(false).to.be.true;
|
||||
} catch (e) {
|
||||
expect(e.errorCode).to.equal(3019);
|
||||
expect(e.message).to.contain(`A \`prerender\` export has been detected, but its value cannot be statically analyzed.`);
|
||||
}
|
||||
});
|
||||
|
||||
it('throws on unknown values III', async () => {
|
||||
try {
|
||||
const result = await scan(`export let prerender = undefined; prerender = true;`, '/src/components/index.astro');
|
||||
expect(false).to.be.true;
|
||||
} catch (e) {
|
||||
expect(e.errorCode).to.equal(3019);
|
||||
expect(e.message).to.contain(`A \`prerender\` export has been detected, but its value cannot be statically analyzed.`);
|
||||
}
|
||||
});
|
||||
|
||||
it('throws on unknown values IV', async () => {
|
||||
try {
|
||||
const result = await scan(`let prerender = true; export { prerender }`, '/src/components/index.astro');
|
||||
expect(false).to.be.true;
|
||||
} catch (e) {
|
||||
expect(e.errorCode).to.equal(3019);
|
||||
expect(e.message).to.contain(`A \`prerender\` export has been detected, but its value cannot be statically analyzed.`);
|
||||
}
|
||||
});
|
||||
});
|
|
@ -14,18 +14,28 @@ export async function createRedirects(
|
|||
let _redirects = '';
|
||||
for (const route of routes) {
|
||||
if (route.pathname) {
|
||||
_redirects += `
|
||||
if (route.distURL) {
|
||||
_redirects += `
|
||||
${route.pathname} /${route.distURL.toString().replace(dir.toString(), '')} 200`;
|
||||
} else {
|
||||
_redirects += `
|
||||
${route.pathname} /.netlify/${kind}/${entryFile} 200`;
|
||||
|
||||
if (route.route === '/404') {
|
||||
_redirects += `
|
||||
if (route.route === '/404') {
|
||||
_redirects += `
|
||||
/* /.netlify/${kind}/${entryFile} 404`;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const pattern =
|
||||
'/' + route.segments.map(([part]) => (part.dynamic ? '*' : part.content)).join('/');
|
||||
_redirects += `
|
||||
if (route.distURL) {
|
||||
_redirects += `
|
||||
${pattern} /${route.distURL.toString().replace(dir.toString(), '')} 200`;
|
||||
} else {
|
||||
_redirects += `
|
||||
${pattern} /.netlify/${kind}/${entryFile} 200`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -422,7 +422,7 @@ importers:
|
|||
deepmerge-ts: ^4.2.2
|
||||
diff: ^5.1.0
|
||||
eol: ^0.9.1
|
||||
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
|
||||
|
@ -494,7 +494,7 @@ importers:
|
|||
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.12
|
||||
github-slugger: 1.5.0
|
||||
|
@ -1115,6 +1115,9 @@ importers:
|
|||
'@astrojs/node': link:../../../../integrations/node
|
||||
astro: link:../../..
|
||||
|
||||
packages/astro/test/benchmark/simple/dist/server:
|
||||
specifiers: {}
|
||||
|
||||
packages/astro/test/fixtures/0-css:
|
||||
specifiers:
|
||||
'@astrojs/react': workspace:*
|
||||
|
@ -1655,6 +1658,9 @@ importers:
|
|||
dependencies:
|
||||
astro: link:../../..
|
||||
|
||||
packages/astro/test/fixtures/config-vite/dist:
|
||||
specifiers: {}
|
||||
|
||||
packages/astro/test/fixtures/css-assets:
|
||||
specifiers:
|
||||
'@astrojs/test-font-awesome-package': file:packages/font-awesome
|
||||
|
@ -2308,6 +2314,12 @@ importers:
|
|||
'@astrojs/partytown': link:../../../../integrations/partytown
|
||||
astro: link:../../..
|
||||
|
||||
packages/astro/test/fixtures/ssr-prerender:
|
||||
specifiers:
|
||||
astro: workspace:*
|
||||
dependencies:
|
||||
astro: link:../../..
|
||||
|
||||
packages/astro/test/fixtures/ssr-preview:
|
||||
specifiers:
|
||||
astro: workspace:*
|
||||
|
@ -11819,6 +11831,10 @@ packages:
|
|||
resolution: {integrity: sha512-+7IwY/kiGAacQfY+YBhKMvEmyAJnw5grTUgjG85Pe7vcUI/6b7pZjZG8nQ7+48YhzEAEqrEgD2dCz/JIK+AYvw==}
|
||||
dev: false
|
||||
|
||||
/es-module-lexer/1.1.0:
|
||||
resolution: {integrity: sha512-fJg+1tiyEeS8figV+fPcPpm8WqJEflG3yPU0NOm5xMvrNkuiy7HzX/Ljng4Y0hAoiw4/3hQTCFYw+ub8+a2pRA==}
|
||||
dev: false
|
||||
|
||||
/es-shim-unscopables/1.0.0:
|
||||
resolution: {integrity: sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==}
|
||||
dependencies:
|
||||
|
|
Loading…
Reference in a new issue