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';
|
import { testFactory, getErrorOverlayContent } from './test-utils.js';
|
||||||
|
|
||||||
const test = testFactory({
|
const test = testFactory({
|
||||||
experimentalErrorOverlay: true,
|
experimental: { errorOverlay: true },
|
||||||
root: './fixtures/error-cyclic/',
|
root: './fixtures/error-cyclic/',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { expect } from '@playwright/test';
|
||||||
import { testFactory, getErrorOverlayContent } from './test-utils.js';
|
import { testFactory, getErrorOverlayContent } from './test-utils.js';
|
||||||
|
|
||||||
const test = testFactory({
|
const test = testFactory({
|
||||||
experimentalErrorOverlay: true,
|
experimental: { errorOverlay: true },
|
||||||
root: './fixtures/error-react-spectrum/',
|
root: './fixtures/error-react-spectrum/',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { expect } from '@playwright/test';
|
||||||
import { testFactory, getErrorOverlayContent } from './test-utils.js';
|
import { testFactory, getErrorOverlayContent } from './test-utils.js';
|
||||||
|
|
||||||
const test = testFactory({
|
const test = testFactory({
|
||||||
experimentalErrorOverlay: true,
|
experimental: { errorOverlay: true },
|
||||||
root: './fixtures/error-sass/',
|
root: './fixtures/error-sass/',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { expect } from '@playwright/test';
|
||||||
import { getErrorOverlayContent, testFactory } from './test-utils.js';
|
import { getErrorOverlayContent, testFactory } from './test-utils.js';
|
||||||
|
|
||||||
const test = testFactory({
|
const test = testFactory({
|
||||||
experimentalErrorOverlay: true,
|
experimental: { errorOverlay: true },
|
||||||
root: './fixtures/errors/',
|
root: './fixtures/errors/',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -123,7 +123,7 @@
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"deepmerge-ts": "^4.2.2",
|
"deepmerge-ts": "^4.2.2",
|
||||||
"diff": "^5.1.0",
|
"diff": "^5.1.0",
|
||||||
"es-module-lexer": "^0.10.5",
|
"es-module-lexer": "^1.1.0",
|
||||||
"execa": "^6.1.0",
|
"execa": "^6.1.0",
|
||||||
"fast-glob": "^3.2.11",
|
"fast-glob": "^3.2.11",
|
||||||
"github-slugger": "^1.4.0",
|
"github-slugger": "^1.4.0",
|
||||||
|
|
|
@ -83,6 +83,7 @@ export interface CLIFlags {
|
||||||
config?: string;
|
config?: string;
|
||||||
drafts?: boolean;
|
drafts?: boolean;
|
||||||
experimentalErrorOverlay?: boolean;
|
experimentalErrorOverlay?: boolean;
|
||||||
|
experimentalPrerender?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BuildConfig {
|
export interface BuildConfig {
|
||||||
|
@ -895,11 +896,41 @@ export interface AstroUserConfig {
|
||||||
astroFlavoredMarkdown?: boolean;
|
astroFlavoredMarkdown?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @hidden
|
* @docs
|
||||||
* Turn on experimental support for the new error overlay component.
|
* @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
|
// Legacy options to be removed
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ import {
|
||||||
createLinkStylesheetElementSet,
|
createLinkStylesheetElementSet,
|
||||||
createModuleScriptElement,
|
createModuleScriptElement,
|
||||||
} from '../render/ssr-element.js';
|
} from '../render/ssr-element.js';
|
||||||
import { matchRoute } from '../routing/match.js';
|
import { matchAssets, matchRoute } from '../routing/match.js';
|
||||||
export { deserializeManifest } from './common.js';
|
export { deserializeManifest } from './common.js';
|
||||||
|
|
||||||
export const pagesVirtualModuleId = '@astrojs-pages-virtual-entry';
|
export const pagesVirtualModuleId = '@astrojs-pages-virtual-entry';
|
||||||
|
@ -100,6 +100,8 @@ export class App {
|
||||||
let routeData = matchRoute(pathname, this.#manifestData);
|
let routeData = matchRoute(pathname, this.#manifestData);
|
||||||
|
|
||||||
if (routeData) {
|
if (routeData) {
|
||||||
|
const asset = matchAssets(routeData, this.#manifest.assets);
|
||||||
|
if (asset) return undefined;
|
||||||
return routeData;
|
return routeData;
|
||||||
} else if (matchNotFound) {
|
} else if (matchNotFound) {
|
||||||
return matchRoute('/404', this.#manifestData);
|
return matchRoute('/404', this.#manifestData);
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import npath from 'path';
|
import npath from 'path';
|
||||||
|
import { createHash } from 'crypto'
|
||||||
import { fileURLToPath, pathToFileURL } from 'url';
|
import { fileURLToPath, pathToFileURL } from 'url';
|
||||||
import type { AstroConfig, RouteType } from '../../@types/astro';
|
import type { AstroConfig, RouteType } from '../../@types/astro';
|
||||||
import { appendForwardSlash } from '../../core/path.js';
|
import { appendForwardSlash } from '../../core/path.js';
|
||||||
|
@ -7,7 +8,11 @@ const STATUS_CODE_PAGES = new Set(['/404', '/500']);
|
||||||
const FALLBACK_OUT_DIR_NAME = './.astro/';
|
const FALLBACK_OUT_DIR_NAME = './.astro/';
|
||||||
|
|
||||||
function getOutRoot(astroConfig: AstroConfig): URL {
|
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(
|
export function getOutFolder(
|
||||||
|
@ -41,7 +46,7 @@ export function getOutFile(
|
||||||
astroConfig: AstroConfig,
|
astroConfig: AstroConfig,
|
||||||
outFolder: URL,
|
outFolder: URL,
|
||||||
pathname: string,
|
pathname: string,
|
||||||
routeType: RouteType
|
routeType: RouteType,
|
||||||
): URL {
|
): URL {
|
||||||
switch (routeType) {
|
switch (routeType) {
|
||||||
case 'endpoint':
|
case 'endpoint':
|
||||||
|
|
|
@ -12,7 +12,7 @@ import type {
|
||||||
RouteType,
|
RouteType,
|
||||||
SSRLoadedRenderer,
|
SSRLoadedRenderer,
|
||||||
} from '../../@types/astro';
|
} from '../../@types/astro';
|
||||||
import type { BuildInternals } from '../../core/build/internal.js';
|
import { BuildInternals, hasPrerenderedPages } from '../../core/build/internal.js';
|
||||||
import {
|
import {
|
||||||
prependForwardSlash,
|
prependForwardSlash,
|
||||||
removeLeadingForwardSlash,
|
removeLeadingForwardSlash,
|
||||||
|
@ -29,7 +29,12 @@ import { createRequest } from '../request.js';
|
||||||
import { matchRoute } from '../routing/match.js';
|
import { matchRoute } from '../routing/match.js';
|
||||||
import { getOutputFilename } from '../util.js';
|
import { getOutputFilename } from '../util.js';
|
||||||
import { getOutDirWithinCwd, getOutFile, getOutFolder } from './common.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 type { PageBuildData, SingleFileBuiltModule, StaticBuildOptions } from './types';
|
||||||
import { getTimeStat } from './util.js';
|
import { getTimeStat } from './util.js';
|
||||||
|
|
||||||
|
@ -70,17 +75,27 @@ export function chunkIsPage(
|
||||||
|
|
||||||
export async function generatePages(opts: StaticBuildOptions, internals: BuildInternals) {
|
export async function generatePages(opts: StaticBuildOptions, internals: BuildInternals) {
|
||||||
const timer = performance.now();
|
const timer = performance.now();
|
||||||
info(opts.logging, null, `\n${bgGreen(black(' generating static routes '))}`);
|
|
||||||
|
|
||||||
const ssr = opts.settings.config.output === 'server';
|
const ssr = opts.settings.config.output === 'server';
|
||||||
const serverEntry = opts.buildConfig.serverEntry;
|
const serverEntry = opts.buildConfig.serverEntry;
|
||||||
const outFolder = ssr ? opts.buildConfig.server : getOutDirWithinCwd(opts.settings.config.outDir);
|
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 ssrEntryURL = new URL('./' + serverEntry + `?time=${Date.now()}`, outFolder);
|
||||||
const ssrEntry = await import(ssrEntryURL.toString());
|
const ssrEntry = await import(ssrEntryURL.toString());
|
||||||
const builtPaths = new Set<string>();
|
const builtPaths = new Set<string>();
|
||||||
|
|
||||||
for (const pageData of eachPageData(internals)) {
|
if (opts.settings.config.experimental.prerender && opts.settings.config.output === 'server') {
|
||||||
await generatePage(opts, internals, pageData, ssrEntry, builtPaths);
|
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({
|
await runHookBuildGenerated({
|
||||||
|
@ -106,7 +121,7 @@ async function generatePage(
|
||||||
const linkIds: string[] = sortedCSS(pageData);
|
const linkIds: string[] = sortedCSS(pageData);
|
||||||
const scripts = pageInfo?.hoistedScript ?? null;
|
const scripts = pageInfo?.hoistedScript ?? null;
|
||||||
|
|
||||||
const pageModule = ssrEntry.pageMap.get(pageData.component);
|
const pageModule = ssrEntry.pageMap?.get(pageData.component);
|
||||||
|
|
||||||
if (!pageModule) {
|
if (!pageModule) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
@ -163,7 +178,7 @@ async function getPathsForRoute(
|
||||||
route: pageData.route,
|
route: pageData.route,
|
||||||
isValidate: false,
|
isValidate: false,
|
||||||
logging: opts.logging,
|
logging: opts.logging,
|
||||||
ssr: opts.settings.config.output === 'server',
|
ssr: false,
|
||||||
})
|
})
|
||||||
.then((_result) => {
|
.then((_result) => {
|
||||||
const label = _result.staticPaths.length === 1 ? 'page' : 'pages';
|
const label = _result.staticPaths.length === 1 ? 'page' : 'pages';
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import type { OutputChunk, RenderedChunk } from 'rollup';
|
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 { prependForwardSlash, removeFileExtension } from '../path.js';
|
||||||
import { viteID } from '../util.js';
|
import { viteID } from '../util.js';
|
||||||
|
import { PageOptions } from '../../vite-plugin-astro/types';
|
||||||
|
|
||||||
export interface BuildInternals {
|
export interface BuildInternals {
|
||||||
/**
|
/**
|
||||||
|
@ -20,11 +21,21 @@ export interface BuildInternals {
|
||||||
// Used to render pages with the correct specifiers.
|
// Used to render pages with the correct specifiers.
|
||||||
entrySpecifierToBundleMap: Map<string, string>;
|
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.
|
* A map for page-specific information.
|
||||||
*/
|
*/
|
||||||
pagesByComponent: Map<string, PageBuildData>;
|
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)
|
* A map for page-specific information by Vite ID (a path-like string)
|
||||||
*/
|
*/
|
||||||
|
@ -73,8 +84,10 @@ export function createBuildInternals(): BuildInternals {
|
||||||
hoistedScriptIdToHoistedMap,
|
hoistedScriptIdToHoistedMap,
|
||||||
hoistedScriptIdToPagesMap,
|
hoistedScriptIdToPagesMap,
|
||||||
entrySpecifierToBundleMap: new Map<string, string>(),
|
entrySpecifierToBundleMap: new Map<string, string>(),
|
||||||
|
pageToBundleMap: new Map<string, string>(),
|
||||||
|
|
||||||
pagesByComponent: new Map(),
|
pagesByComponent: new Map(),
|
||||||
|
pageOptionsByPage: new Map(),
|
||||||
pagesByViteID: new Map(),
|
pagesByViteID: new Map(),
|
||||||
pagesByClientOnly: new Map(),
|
pagesByClientOnly: new Map(),
|
||||||
|
|
||||||
|
@ -189,6 +202,31 @@ export function* eachPageData(internals: BuildInternals) {
|
||||||
yield* internals.pagesByComponent.values();
|
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.
|
* 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.
|
* 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 path from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import * as vite from 'vite';
|
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 { emptyDir, removeDir } from '../../core/fs/index.js';
|
||||||
import { prependForwardSlash } from '../../core/path.js';
|
import { prependForwardSlash } from '../../core/path.js';
|
||||||
import { isModeServerWithNoAdapter } from '../../core/util.js';
|
import { isModeServerWithNoAdapter } from '../../core/util.js';
|
||||||
|
@ -18,11 +22,13 @@ import { trackPageData } from './internal.js';
|
||||||
import type { PageBuildData, StaticBuildOptions } from './types';
|
import type { PageBuildData, StaticBuildOptions } from './types';
|
||||||
import { getTimeStat } from './util.js';
|
import { getTimeStat } from './util.js';
|
||||||
import { vitePluginAnalyzer } from './vite-plugin-analyzer.js';
|
import { vitePluginAnalyzer } from './vite-plugin-analyzer.js';
|
||||||
|
import { vitePluginPrerender } from './vite-plugin-prerender.js';
|
||||||
import { rollupPluginAstroBuildCSS } from './vite-plugin-css.js';
|
import { rollupPluginAstroBuildCSS } from './vite-plugin-css.js';
|
||||||
import { vitePluginHoistedScripts } from './vite-plugin-hoisted-scripts.js';
|
import { vitePluginHoistedScripts } from './vite-plugin-hoisted-scripts.js';
|
||||||
import { vitePluginInternals } from './vite-plugin-internals.js';
|
import { vitePluginInternals } from './vite-plugin-internals.js';
|
||||||
import { vitePluginPages } from './vite-plugin-pages.js';
|
import { vitePluginPages } from './vite-plugin-pages.js';
|
||||||
import { injectManifest, vitePluginSSR } from './vite-plugin-ssr.js';
|
import { injectManifest, vitePluginSSR } from './vite-plugin-ssr.js';
|
||||||
|
import * as eslexer from 'es-module-lexer';
|
||||||
|
|
||||||
export async function staticBuild(opts: StaticBuildOptions) {
|
export async function staticBuild(opts: StaticBuildOptions) {
|
||||||
const { allPages, settings } = opts;
|
const { allPages, settings } = opts;
|
||||||
|
@ -88,15 +94,33 @@ export async function staticBuild(opts: StaticBuildOptions) {
|
||||||
await clientBuild(opts, internals, clientInput);
|
await clientBuild(opts, internals, clientInput);
|
||||||
|
|
||||||
timer.generate = performance.now();
|
timer.generate = performance.now();
|
||||||
if (settings.config.output === 'static') {
|
if (!settings.config.experimental.prerender) {
|
||||||
await generatePages(opts, internals);
|
if (settings.config.output === 'static') {
|
||||||
await cleanSsrOutput(opts);
|
await generatePages(opts, internals);
|
||||||
} else {
|
await cleanServerOutput(opts);
|
||||||
// Inject the manifest
|
} else {
|
||||||
await injectManifest(opts, internals);
|
// Inject the manifest
|
||||||
|
await injectManifest(opts, internals);
|
||||||
|
|
||||||
info(opts.logging, null, `\n${bgMagenta(black(' finalizing server assets '))}\n`);
|
info(opts.logging, null, `\n${bgMagenta(black(' finalizing server assets '))}\n`);
|
||||||
await ssrMoveAssets(opts);
|
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,
|
reportCompressedSize: false,
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
|
vitePluginAnalyzer(internals),
|
||||||
vitePluginInternals(input, internals),
|
vitePluginInternals(input, internals),
|
||||||
vitePluginPages(opts, internals),
|
vitePluginPages(opts, internals),
|
||||||
rollupPluginAstroBuildCSS({
|
rollupPluginAstroBuildCSS({
|
||||||
|
@ -141,10 +166,10 @@ async function ssrBuild(opts: StaticBuildOptions, internals: BuildInternals, inp
|
||||||
internals,
|
internals,
|
||||||
target: 'server',
|
target: 'server',
|
||||||
}),
|
}),
|
||||||
|
vitePluginPrerender(opts, internals),
|
||||||
...(viteConfig.plugins || []),
|
...(viteConfig.plugins || []),
|
||||||
// SSR needs to be last
|
// SSR needs to be last
|
||||||
settings.config.output === 'server' && vitePluginSSR(internals, settings.adapter!),
|
ssr && vitePluginSSR(internals, settings.adapter!),
|
||||||
vitePluginAnalyzer(internals),
|
|
||||||
],
|
],
|
||||||
envPrefix: 'PUBLIC_',
|
envPrefix: 'PUBLIC_',
|
||||||
base: settings.config.base,
|
base: settings.config.base,
|
||||||
|
@ -169,7 +194,12 @@ async function clientBuild(
|
||||||
const { settings, viteConfig } = opts;
|
const { settings, viteConfig } = opts;
|
||||||
const timer = performance.now();
|
const timer = performance.now();
|
||||||
const ssr = settings.config.output === 'server';
|
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.
|
// Nothing to do if there is no client-side JS.
|
||||||
if (!input.size) {
|
if (!input.size) {
|
||||||
|
@ -232,7 +262,77 @@ async function clientBuild(
|
||||||
return buildResult;
|
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);
|
const out = getOutDirWithinCwd(opts.settings.config.outDir);
|
||||||
// The SSR output is all .mjs files, the client output is not.
|
// The SSR output is all .mjs files, the client output is not.
|
||||||
const files = await glob('**/*.mjs', {
|
const files = await glob('**/*.mjs', {
|
||||||
|
@ -259,8 +359,8 @@ async function cleanSsrOutput(opts: StaticBuildOptions) {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
Array.from(directories).map(async (filename) => {
|
Array.from(directories).map(async (filename) => {
|
||||||
const url = new URL(filename, out);
|
const url = new URL(filename, out);
|
||||||
const folder = await fs.promises.readdir(url);
|
const dir = await glob(fileURLToPath(url), { absolute: true });
|
||||||
if (!folder.length) {
|
if (!dir.length) {
|
||||||
await fs.promises.rm(url, { recursive: true, force: true });
|
await fs.promises.rm(url, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -303,16 +403,16 @@ async function ssrMoveAssets(opts: StaticBuildOptions) {
|
||||||
cwd: fileURLToPath(serverRoot),
|
cwd: fileURLToPath(serverRoot),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Make the directory
|
if (files.length > 0) {
|
||||||
await fs.promises.mkdir(clientAssets, { recursive: true });
|
// Make the directory
|
||||||
|
await fs.promises.mkdir(clientAssets, { recursive: true });
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
files.map(async (filename) => {
|
files.map(async (filename) => {
|
||||||
const currentUrl = new URL(filename, serverRoot);
|
const currentUrl = new URL(filename, serverRoot);
|
||||||
const clientUrl = new URL(filename, clientRoot);
|
const clientUrl = new URL(filename, clientRoot);
|
||||||
return fs.promises.rename(currentUrl, clientUrl);
|
return fs.promises.rename(currentUrl, clientUrl);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
removeDir(serverAssets);
|
||||||
removeDir(serverAssets);
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import type { InlineConfig } from 'vite';
|
import type { InlineConfig } from 'vite';
|
||||||
import type {
|
import type {
|
||||||
|
AstroConfig,
|
||||||
AstroSettings,
|
AstroSettings,
|
||||||
BuildConfig,
|
BuildConfig,
|
||||||
ComponentInstance,
|
ComponentInstance,
|
||||||
|
@ -13,6 +14,7 @@ import type { RouteCache } from '../render/route-cache';
|
||||||
|
|
||||||
export type ComponentPath = string;
|
export type ComponentPath = string;
|
||||||
export type ViteID = string;
|
export type ViteID = string;
|
||||||
|
export type PageOutput = AstroConfig['output']
|
||||||
|
|
||||||
export interface PageBuildData {
|
export interface PageBuildData {
|
||||||
component: ComponentPath;
|
component: ComponentPath;
|
||||||
|
|
|
@ -74,12 +74,18 @@ export function vitePluginAnalyzer(internals: BuildInternals): VitePlugin {
|
||||||
const hoistScanner = hoistedScriptScanner();
|
const hoistScanner = hoistedScriptScanner();
|
||||||
|
|
||||||
const ids = this.getModuleIds();
|
const ids = this.getModuleIds();
|
||||||
|
|
||||||
for (const id of ids) {
|
for (const id of ids) {
|
||||||
const info = this.getModuleInfo(id);
|
const info = this.getModuleInfo(id);
|
||||||
if (!info || !info.meta?.astro) continue;
|
if (!info || !info.meta?.astro) continue;
|
||||||
|
|
||||||
const astro = info.meta.astro as AstroPluginMetadata['astro'];
|
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) {
|
for (const c of astro.hydratedComponents) {
|
||||||
const rid = c.resolvedPath ? decodeURI(c.resolvedPath) : c.specifier;
|
const rid = c.resolvedPath ? decodeURI(c.resolvedPath) : c.specifier;
|
||||||
internals.discoveredHydratedComponents.add(rid);
|
internals.discoveredHydratedComponents.add(rid);
|
||||||
|
@ -103,10 +109,10 @@ export function vitePluginAnalyzer(internals: BuildInternals): VitePlugin {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [pageInfo] of getTopLevelPages(id, this)) {
|
for (const [pageInfo] of getTopLevelPages(id, this)) {
|
||||||
const pageData = getPageDataByViteID(internals, pageInfo.id);
|
const newPageData = getPageDataByViteID(internals, pageInfo.id);
|
||||||
if (!pageData) continue;
|
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) {
|
if (chunk.type === 'chunk' && chunk.facadeModuleId) {
|
||||||
const specifier = mapping.get(chunk.facadeModuleId) || chunk.facadeModuleId;
|
const specifier = mapping.get(chunk.facadeModuleId) || chunk.facadeModuleId;
|
||||||
internals.entrySpecifierToBundleMap.set(specifier, chunk.fileName);
|
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 type { Plugin as VitePlugin } from 'vite';
|
||||||
import { pagesVirtualModuleId, resolvedPagesVirtualModuleId } from '../app/index.js';
|
import { pagesVirtualModuleId, resolvedPagesVirtualModuleId } from '../app/index.js';
|
||||||
import { addRollupInput } from './add-rollup-input.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 { eachPageData } from './internal.js';
|
||||||
import type { StaticBuildOptions } from './types';
|
import type { StaticBuildOptions } from './types';
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ export function vitePluginPages(opts: StaticBuildOptions, internals: BuildIntern
|
||||||
name: '@astro/plugin-build-pages',
|
name: '@astro/plugin-build-pages',
|
||||||
|
|
||||||
options(options) {
|
options(options) {
|
||||||
if (opts.settings.config.output === 'static') {
|
if (opts.settings.config.output === 'static' || hasPrerenderedPages(internals)) {
|
||||||
return addRollupInput(options, [pagesVirtualModuleId]);
|
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 { removeLeadingForwardSlash, removeTrailingForwardSlash } 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 { 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';
|
export const virtualModuleId = '@astrojs-ssr-virtual-entry';
|
||||||
const resolvedVirtualModuleId = '\0' + virtualModuleId;
|
const resolvedVirtualModuleId = '\0' + virtualModuleId;
|
||||||
|
@ -43,6 +44,8 @@ const _manifest = Object.assign(_deserializeManifest('${manifestReplace}'), {
|
||||||
});
|
});
|
||||||
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);
|
||||||
|
@ -136,7 +139,20 @@ function buildManifest(
|
||||||
const bareBase = removeTrailingForwardSlash(removeLeadingForwardSlash(settings.config.base));
|
const bareBase = removeTrailingForwardSlash(removeLeadingForwardSlash(settings.config.base));
|
||||||
const joinBase = (pth: string) => (bareBase ? bareBase + '/' + pth : pth);
|
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'] = [];
|
const scripts: SerializedRouteInfo['scripts'] = [];
|
||||||
if (pageData.hoistedScript) {
|
if (pageData.hoistedScript) {
|
||||||
scripts.unshift(
|
scripts.unshift(
|
||||||
|
|
|
@ -104,6 +104,10 @@ export function resolveFlags(flags: Partial<Flags>): CLIFlags {
|
||||||
typeof flags.experimentalErrorOverlay === 'boolean'
|
typeof flags.experimentalErrorOverlay === 'boolean'
|
||||||
? flags.experimentalErrorOverlay
|
? flags.experimentalErrorOverlay
|
||||||
: undefined,
|
: 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) {
|
function mergeCLIFlags(astroConfig: AstroUserConfig, flags: CLIFlags, cmd: string) {
|
||||||
astroConfig.server = astroConfig.server || {};
|
astroConfig.server = astroConfig.server || {};
|
||||||
astroConfig.markdown = astroConfig.markdown || {};
|
astroConfig.markdown = astroConfig.markdown || {};
|
||||||
|
astroConfig.experimental = astroConfig.experimental || {};
|
||||||
if (typeof flags.site === 'string') astroConfig.site = flags.site;
|
if (typeof flags.site === 'string') astroConfig.site = flags.site;
|
||||||
if (typeof flags.base === 'string') astroConfig.base = flags.base;
|
if (typeof flags.base === 'string') astroConfig.base = flags.base;
|
||||||
if (typeof flags.drafts === 'boolean') astroConfig.markdown.drafts = flags.drafts;
|
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.
|
// TODO: Come back here and refactor to remove this expected error.
|
||||||
astroConfig.server.host = flags.host;
|
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;
|
return astroConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -47,7 +47,10 @@ const ASTRO_CONFIG_DEFAULTS: AstroUserConfig & any = {
|
||||||
legacy: {
|
legacy: {
|
||||||
astroFlavoredMarkdown: false,
|
astroFlavoredMarkdown: false,
|
||||||
},
|
},
|
||||||
experimentalErrorOverlay: false,
|
experimental: {
|
||||||
|
errorOverlay: false,
|
||||||
|
prerender: false,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AstroConfigSchema = z.object({
|
export const AstroConfigSchema = z.object({
|
||||||
|
@ -188,6 +191,13 @@ export const AstroConfigSchema = z.object({
|
||||||
vite: z
|
vite: z
|
||||||
.custom<ViteUserConfig>((data) => data instanceof Object && !Array.isArray(data))
|
.custom<ViteUserConfig>((data) => data instanceof Object && !Array.isArray(data))
|
||||||
.default(ASTRO_CONFIG_DEFAULTS.vite),
|
.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
|
legacy: z
|
||||||
.object({
|
.object({
|
||||||
astroFlavoredMarkdown: z
|
astroFlavoredMarkdown: z
|
||||||
|
@ -197,7 +207,6 @@ export const AstroConfigSchema = z.object({
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.default({}),
|
.default({}),
|
||||||
experimentalErrorOverlay: z.boolean().optional().default(false),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
interface PostCSSConfigResult {
|
interface PostCSSConfigResult {
|
||||||
|
|
|
@ -20,6 +20,7 @@ import markdownVitePlugin from '../vite-plugin-markdown/index.js';
|
||||||
import astroScriptsPlugin from '../vite-plugin-scripts/index.js';
|
import astroScriptsPlugin from '../vite-plugin-scripts/index.js';
|
||||||
import astroScriptsPageSSRPlugin from '../vite-plugin-scripts/page-ssr.js';
|
import astroScriptsPageSSRPlugin from '../vite-plugin-scripts/page-ssr.js';
|
||||||
import { createCustomViteLogger } from './errors/dev/index.js';
|
import { createCustomViteLogger } from './errors/dev/index.js';
|
||||||
|
import astroScannerPlugin from '../vite-plugin-scanner/index.js';
|
||||||
import { resolveDependency } from './util.js';
|
import { resolveDependency } from './util.js';
|
||||||
|
|
||||||
interface CreateViteOptions {
|
interface CreateViteOptions {
|
||||||
|
@ -114,6 +115,7 @@ export async function createVite(
|
||||||
astroIntegrationsContainerPlugin({ settings, logging }),
|
astroIntegrationsContainerPlugin({ settings, logging }),
|
||||||
astroScriptsPageSSRPlugin({ settings }),
|
astroScriptsPageSSRPlugin({ settings }),
|
||||||
astroHeadPropagationPlugin({ settings }),
|
astroHeadPropagationPlugin({ settings }),
|
||||||
|
settings.config.experimental.prerender && astroScannerPlugin({ settings, logging }),
|
||||||
],
|
],
|
||||||
publicDir: fileURLToPath(settings.config.publicDir),
|
publicDir: fileURLToPath(settings.config.publicDir),
|
||||||
root: fileURLToPath(settings.config.root),
|
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}\`.`,
|
`Could not render \`${componentName}\`. No matching import has been found for \`${componentName}\`.`,
|
||||||
hint: 'Please make sure the component is properly imported.',
|
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
|
// Vite Errors - 4xxx
|
||||||
UnknownViteError: {
|
UnknownViteError: {
|
||||||
title: 'Unknown Vite Error.',
|
title: 'Unknown Vite Error.',
|
||||||
|
|
|
@ -561,7 +561,7 @@ function getOverlayCode() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function patchOverlay(code: string, config: AstroConfig) {
|
export function patchOverlay(code: string, config: AstroConfig) {
|
||||||
if (config.experimentalErrorOverlay) {
|
if (config.experimental.errorOverlay) {
|
||||||
return code.replace('class ErrorOverlay', getOverlayCode() + '\nclass ViteErrorOverlay');
|
return code.replace('class ErrorOverlay', getOverlayCode() + '\nclass ViteErrorOverlay');
|
||||||
} else {
|
} else {
|
||||||
// Legacy overlay
|
// Legacy overlay
|
||||||
|
|
|
@ -5,6 +5,19 @@ export function matchRoute(pathname: string, manifest: ManifestData): RouteData
|
||||||
return manifest.routes.find((route) => route.pattern.test(pathname));
|
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 */
|
/** Finds all matching routes from pathname */
|
||||||
export function matchAllRoutes(pathname: string, manifest: ManifestData): RouteData[] {
|
export function matchAllRoutes(pathname: string, manifest: ManifestData): RouteData[] {
|
||||||
return manifest.routes.filter((route) => route.pattern.test(pathname));
|
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);
|
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 {
|
export function isModeServerWithNoAdapter(settings: AstroSettings): boolean {
|
||||||
return settings.config.output === 'server' && !settings.adapter;
|
return settings.config.output === 'server' && !settings.adapter;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import fs from 'node:fs';
|
||||||
import { bold } from 'kleur/colors';
|
import { bold } from 'kleur/colors';
|
||||||
import type { AddressInfo } from 'net';
|
import type { AddressInfo } from 'net';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
|
@ -345,6 +346,7 @@ export async function runHookBuildDone({
|
||||||
logging: LogOptions;
|
logging: LogOptions;
|
||||||
}) {
|
}) {
|
||||||
const dir = config.output === 'server' ? buildConfig.client : config.outDir;
|
const dir = config.output === 'server' ? buildConfig.client : config.outDir;
|
||||||
|
await fs.promises.mkdir(dir, { recursive: true });
|
||||||
|
|
||||||
for (const integration of config.integrations) {
|
for (const integration of config.integrations) {
|
||||||
if (integration?.hooks?.['astro:build:done']) {
|
if (integration?.hooks?.['astro:build:done']) {
|
||||||
|
|
|
@ -146,6 +146,7 @@ export default function astroJSX(): PluginObj {
|
||||||
hydratedComponents: [],
|
hydratedComponents: [],
|
||||||
scripts: [],
|
scripts: [],
|
||||||
propagation: 'none',
|
propagation: 'none',
|
||||||
|
pageOptions: {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
path.node.body.splice(
|
path.node.body.splice(
|
||||||
|
|
|
@ -220,6 +220,7 @@ export default function astro({ settings, logging }: AstroPluginOptions): vite.P
|
||||||
hydratedComponents: transformResult.hydratedComponents,
|
hydratedComponents: transformResult.hydratedComponents,
|
||||||
scripts: transformResult.scripts,
|
scripts: transformResult.scripts,
|
||||||
propagation: 'none',
|
propagation: 'none',
|
||||||
|
pageOptions: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1,11 +1,16 @@
|
||||||
import type { TransformResult } from '@astrojs/compiler';
|
import type { TransformResult } from '@astrojs/compiler';
|
||||||
import type { PropagationHint } from '../@types/astro';
|
import type { PropagationHint } from '../@types/astro';
|
||||||
|
|
||||||
|
export interface PageOptions {
|
||||||
|
prerender?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface PluginMetadata {
|
export interface PluginMetadata {
|
||||||
astro: {
|
astro: {
|
||||||
hydratedComponents: TransformResult['hydratedComponents'];
|
hydratedComponents: TransformResult['hydratedComponents'];
|
||||||
clientOnlyComponents: TransformResult['clientOnlyComponents'];
|
clientOnlyComponents: TransformResult['clientOnlyComponents'];
|
||||||
scripts: TransformResult['scripts'];
|
scripts: TransformResult['scripts'];
|
||||||
propagation: PropagationHint;
|
propagation: PropagationHint;
|
||||||
|
pageOptions: PageOptions;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -234,6 +234,7 @@ ${tsResult}`;
|
||||||
hydratedComponents: transformResult.hydratedComponents,
|
hydratedComponents: transformResult.hydratedComponents,
|
||||||
scripts: transformResult.scripts,
|
scripts: transformResult.scripts,
|
||||||
propagation: 'none',
|
propagation: 'none',
|
||||||
|
pageOptions: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -158,6 +158,7 @@ export default function markdown({ settings, logging }: AstroPluginOptions): Plu
|
||||||
clientOnlyComponents: [],
|
clientOnlyComponents: [],
|
||||||
scripts: [],
|
scripts: [],
|
||||||
propagation: 'none',
|
propagation: 'none',
|
||||||
|
pageOptions: {},
|
||||||
} as PluginMetadata['astro'],
|
} as PluginMetadata['astro'],
|
||||||
vite: {
|
vite: {
|
||||||
lang: 'ts',
|
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');
|
const result = await compile(`<h1>Hello World</h1>`, '/src/components/index.astro');
|
||||||
await init;
|
await init;
|
||||||
const [, exports] = parse(result.code);
|
const [, exports] = parse(result.code);
|
||||||
expect(exports).to.include('default');
|
const names = exports.map(e => e.n);
|
||||||
expect(exports).to.include('file');
|
expect(names).to.include('default');
|
||||||
expect(exports).to.include('url');
|
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 = '';
|
let _redirects = '';
|
||||||
for (const route of routes) {
|
for (const route of routes) {
|
||||||
if (route.pathname) {
|
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`;
|
${route.pathname} /.netlify/${kind}/${entryFile} 200`;
|
||||||
|
|
||||||
if (route.route === '/404') {
|
if (route.route === '/404') {
|
||||||
_redirects += `
|
_redirects += `
|
||||||
/* /.netlify/${kind}/${entryFile} 404`;
|
/* /.netlify/${kind}/${entryFile} 404`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const pattern =
|
const pattern =
|
||||||
'/' + route.segments.map(([part]) => (part.dynamic ? '*' : part.content)).join('/');
|
'/' + 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`;
|
${pattern} /.netlify/${kind}/${entryFile} 200`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -422,7 +422,7 @@ importers:
|
||||||
deepmerge-ts: ^4.2.2
|
deepmerge-ts: ^4.2.2
|
||||||
diff: ^5.1.0
|
diff: ^5.1.0
|
||||||
eol: ^0.9.1
|
eol: ^0.9.1
|
||||||
es-module-lexer: ^0.10.5
|
es-module-lexer: ^1.1.0
|
||||||
execa: ^6.1.0
|
execa: ^6.1.0
|
||||||
fast-glob: ^3.2.11
|
fast-glob: ^3.2.11
|
||||||
github-slugger: ^1.4.0
|
github-slugger: ^1.4.0
|
||||||
|
@ -494,7 +494,7 @@ importers:
|
||||||
debug: 4.3.4
|
debug: 4.3.4
|
||||||
deepmerge-ts: 4.2.2
|
deepmerge-ts: 4.2.2
|
||||||
diff: 5.1.0
|
diff: 5.1.0
|
||||||
es-module-lexer: 0.10.5
|
es-module-lexer: 1.1.0
|
||||||
execa: 6.1.0
|
execa: 6.1.0
|
||||||
fast-glob: 3.2.12
|
fast-glob: 3.2.12
|
||||||
github-slugger: 1.5.0
|
github-slugger: 1.5.0
|
||||||
|
@ -1115,6 +1115,9 @@ importers:
|
||||||
'@astrojs/node': link:../../../../integrations/node
|
'@astrojs/node': link:../../../../integrations/node
|
||||||
astro: link:../../..
|
astro: link:../../..
|
||||||
|
|
||||||
|
packages/astro/test/benchmark/simple/dist/server:
|
||||||
|
specifiers: {}
|
||||||
|
|
||||||
packages/astro/test/fixtures/0-css:
|
packages/astro/test/fixtures/0-css:
|
||||||
specifiers:
|
specifiers:
|
||||||
'@astrojs/react': workspace:*
|
'@astrojs/react': workspace:*
|
||||||
|
@ -1655,6 +1658,9 @@ importers:
|
||||||
dependencies:
|
dependencies:
|
||||||
astro: link:../../..
|
astro: link:../../..
|
||||||
|
|
||||||
|
packages/astro/test/fixtures/config-vite/dist:
|
||||||
|
specifiers: {}
|
||||||
|
|
||||||
packages/astro/test/fixtures/css-assets:
|
packages/astro/test/fixtures/css-assets:
|
||||||
specifiers:
|
specifiers:
|
||||||
'@astrojs/test-font-awesome-package': file:packages/font-awesome
|
'@astrojs/test-font-awesome-package': file:packages/font-awesome
|
||||||
|
@ -2308,6 +2314,12 @@ importers:
|
||||||
'@astrojs/partytown': link:../../../../integrations/partytown
|
'@astrojs/partytown': link:../../../../integrations/partytown
|
||||||
astro: link:../../..
|
astro: link:../../..
|
||||||
|
|
||||||
|
packages/astro/test/fixtures/ssr-prerender:
|
||||||
|
specifiers:
|
||||||
|
astro: workspace:*
|
||||||
|
dependencies:
|
||||||
|
astro: link:../../..
|
||||||
|
|
||||||
packages/astro/test/fixtures/ssr-preview:
|
packages/astro/test/fixtures/ssr-preview:
|
||||||
specifiers:
|
specifiers:
|
||||||
astro: workspace:*
|
astro: workspace:*
|
||||||
|
@ -11819,6 +11831,10 @@ packages:
|
||||||
resolution: {integrity: sha512-+7IwY/kiGAacQfY+YBhKMvEmyAJnw5grTUgjG85Pe7vcUI/6b7pZjZG8nQ7+48YhzEAEqrEgD2dCz/JIK+AYvw==}
|
resolution: {integrity: sha512-+7IwY/kiGAacQfY+YBhKMvEmyAJnw5grTUgjG85Pe7vcUI/6b7pZjZG8nQ7+48YhzEAEqrEgD2dCz/JIK+AYvw==}
|
||||||
dev: false
|
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:
|
/es-shim-unscopables/1.0.0:
|
||||||
resolution: {integrity: sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==}
|
resolution: {integrity: sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
Loading…
Reference in a new issue