refactor the route cache and other build internals (#2503)
* refactor dev to use vite server * refactor the route cache and other build internals * use debug package for debug logs (#2504) Co-authored-by: Matthew Phillips <matthew@matthewphillips.info>
This commit is contained in:
parent
d7149f9b2f
commit
de9fadbaed
14 changed files with 191 additions and 197 deletions
|
@ -69,6 +69,7 @@
|
|||
"@proload/core": "^0.2.1",
|
||||
"@proload/plugin-tsm": "^0.1.0",
|
||||
"@types/babel__core": "^7.1.15",
|
||||
"@types/debug": "^4.1.7",
|
||||
"@web/parse5-utils": "^1.3.0",
|
||||
"astring": "^1.7.5",
|
||||
"ci-info": "^3.2.0",
|
||||
|
|
|
@ -341,8 +341,6 @@ export interface RouteData {
|
|||
type: 'page';
|
||||
}
|
||||
|
||||
export type RouteCache = Record<string, GetStaticPathsResultKeyed>;
|
||||
|
||||
export type RuntimeMode = 'development' | 'production';
|
||||
|
||||
/**
|
||||
|
@ -385,7 +383,7 @@ export interface RSS {
|
|||
}[];
|
||||
}
|
||||
|
||||
export type RSSFunction = (args: RSS) => void;
|
||||
export type RSSFunction = (args: RSS) => RSSResult;
|
||||
|
||||
export type FeedResult = { url: string; content?: string };
|
||||
export type RSSResult = { xml: FeedResult; xsl?: FeedResult };
|
||||
|
|
|
@ -90,12 +90,8 @@ export async function cli(args: string[]) {
|
|||
try {
|
||||
config = await loadConfig({ cwd: projectRoot, flags });
|
||||
} catch (err) {
|
||||
if (err instanceof z.ZodError) {
|
||||
console.error(formatConfigError(err));
|
||||
} else {
|
||||
console.error(colors.red((err as any).toString() || err));
|
||||
}
|
||||
process.exit(1);
|
||||
throwAndExit(err);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (cmd) {
|
||||
|
@ -143,6 +139,13 @@ export async function cli(args: string[]) {
|
|||
|
||||
/** Display error and exit */
|
||||
function throwAndExit(err: any) {
|
||||
console.error(colors.red(err.toString() || err));
|
||||
if (err instanceof z.ZodError) {
|
||||
console.error(formatConfigError(err));
|
||||
} else if (err.stack) {
|
||||
const [mainMsg, ...stackMsg] = err.stack.split('\n');
|
||||
console.error(colors.red(mainMsg) + '\n' + colors.dim(stackMsg.join('\n')));
|
||||
} else {
|
||||
console.error(colors.red(err.toString() || err));
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { AstroConfig, ManifestData, RouteCache } from '../../@types/astro';
|
||||
import type { AstroConfig, ManifestData } from '../../@types/astro';
|
||||
import type { LogOptions } from '../logger';
|
||||
|
||||
import fs from 'fs';
|
||||
|
@ -13,6 +13,7 @@ import { generateSitemap } from '../ssr/sitemap.js';
|
|||
import { collectPagesData } from './page-data.js';
|
||||
import { build as scanBasedBuild } from './scan-based-build.js';
|
||||
import { staticBuild } from './static-build.js';
|
||||
import { RouteCache } from '../ssr/route-cache.js';
|
||||
|
||||
export interface BuildOptions {
|
||||
mode?: string;
|
||||
|
@ -35,7 +36,7 @@ class AstroBuilder {
|
|||
private logging: LogOptions;
|
||||
private mode = 'production';
|
||||
private origin: string;
|
||||
private routeCache: RouteCache = {};
|
||||
private routeCache: RouteCache;
|
||||
private manifest: ManifestData;
|
||||
private viteServer?: ViteDevServer;
|
||||
private viteConfig?: ViteConfigWithSSR;
|
||||
|
@ -49,6 +50,7 @@ class AstroBuilder {
|
|||
this.config = config;
|
||||
const port = config.devOptions.port; // no need to save this (don’t rely on port in builder)
|
||||
this.logging = options.logging;
|
||||
this.routeCache = new RouteCache(this.logging);
|
||||
this.origin = config.buildOptions.site ? new URL(config.buildOptions.site).origin : `http://localhost:${port}`;
|
||||
this.manifest = createRouteManifest({ config }, this.logging);
|
||||
}
|
||||
|
@ -74,7 +76,7 @@ class AstroBuilder {
|
|||
this.viteConfig = viteConfig;
|
||||
const viteServer = await vite.createServer(viteConfig);
|
||||
this.viteServer = viteServer;
|
||||
debug(logging, 'build', timerMessage('Vite started', timer.viteStart));
|
||||
debug('build', timerMessage('Vite started', timer.viteStart));
|
||||
|
||||
timer.loadStart = performance.now();
|
||||
const { assets, allPages } = await collectPagesData({
|
||||
|
@ -92,13 +94,13 @@ class AstroBuilder {
|
|||
// TODO: add better type inference to data.preload[1]
|
||||
const frontmatter = (data.preload[1] as any).frontmatter;
|
||||
if (Boolean(frontmatter.draft) && !this.config.buildOptions.drafts) {
|
||||
debug(logging, 'build', timerMessage(`Skipping draft page ${page}`, timer.loadStart));
|
||||
debug('build', timerMessage(`Skipping draft page ${page}`, timer.loadStart));
|
||||
delete allPages[page];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
debug(logging, 'build', timerMessage('All pages loaded', timer.loadStart));
|
||||
debug('build', timerMessage('All pages loaded', timer.loadStart));
|
||||
|
||||
// The names of each pages
|
||||
const pageNames: string[] = [];
|
||||
|
@ -130,7 +132,7 @@ class AstroBuilder {
|
|||
viteServer: this.viteServer,
|
||||
});
|
||||
}
|
||||
debug(logging, 'build', timerMessage('Vite build finished', timer.buildStart));
|
||||
debug('build', timerMessage('Vite build finished', timer.buildStart));
|
||||
|
||||
// Write any additionally generated assets to disk.
|
||||
timer.assetsStart = performance.now();
|
||||
|
@ -141,7 +143,7 @@ class AstroBuilder {
|
|||
fs.writeFileSync(filePath, assets[k], 'utf8');
|
||||
delete assets[k]; // free up memory
|
||||
});
|
||||
debug(logging, 'build', timerMessage('Additional assets copied', timer.assetsStart));
|
||||
debug('build', timerMessage('Additional assets copied', timer.assetsStart));
|
||||
|
||||
// Build your final sitemap.
|
||||
timer.sitemapStart = performance.now();
|
||||
|
@ -151,7 +153,7 @@ class AstroBuilder {
|
|||
await fs.promises.mkdir(new URL('./', sitemapPath), { recursive: true });
|
||||
await fs.promises.writeFile(sitemapPath, sitemap, 'utf8');
|
||||
}
|
||||
debug(logging, 'build', timerMessage('Sitemap built', timer.sitemapStart));
|
||||
debug('build', timerMessage('Sitemap built', timer.sitemapStart));
|
||||
|
||||
// You're done! Time to clean up.
|
||||
await viteServer.close();
|
||||
|
@ -162,8 +164,6 @@ class AstroBuilder {
|
|||
|
||||
/** Stats */
|
||||
private async printStats({ logging, timeStart, pageCount }: { logging: LogOptions; timeStart: number; pageCount: number }) {
|
||||
/* eslint-disable no-console */
|
||||
debug(logging, ''); // empty line for debug
|
||||
const buildTime = performance.now() - timeStart;
|
||||
const total = buildTime < 750 ? `${Math.round(buildTime)}ms` : `${(buildTime / 1000).toFixed(2)}s`;
|
||||
const perPage = `${Math.round(buildTime / pageCount)}ms`;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { AstroConfig, ComponentInstance, GetStaticPathsResult, ManifestData, RouteCache, RouteData, RSSResult } from '../../@types/astro';
|
||||
import type { AstroConfig, ComponentInstance, ManifestData, RouteData, RSSResult } from '../../@types/astro';
|
||||
import type { AllPagesData } from './types';
|
||||
import type { LogOptions } from '../logger';
|
||||
import type { ViteDevServer } from '../vite.js';
|
||||
|
@ -7,10 +7,8 @@ import { fileURLToPath } from 'url';
|
|||
import * as colors from 'kleur/colors';
|
||||
import { debug } from '../logger.js';
|
||||
import { preload as ssrPreload } from '../ssr/index.js';
|
||||
import { validateGetStaticPathsModule, validateGetStaticPathsResult } from '../ssr/routing.js';
|
||||
import { generatePaginateFunction } from '../ssr/paginate.js';
|
||||
import { generateRssFunction } from '../ssr/rss.js';
|
||||
import { assignStaticPaths } from '../ssr/route-cache.js';
|
||||
import { callGetStaticPaths, RouteCache, RouteCacheEntry } from '../ssr/route-cache.js';
|
||||
|
||||
export interface CollectPagesDataOptions {
|
||||
astroConfig: AstroConfig;
|
||||
|
@ -57,11 +55,11 @@ export async function collectPagesData(opts: CollectPagesDataOptions): Promise<C
|
|||
})
|
||||
.then((routes) => {
|
||||
const html = `${route.pathname}`.replace(/\/?$/, '/index.html');
|
||||
debug(logging, 'build', `├── ${colors.bold(colors.green('✔'))} ${route.component} → ${colors.yellow(html)}`);
|
||||
debug('build', `├── ${colors.bold(colors.green('✔'))} ${route.component} → ${colors.yellow(html)}`);
|
||||
return routes;
|
||||
})
|
||||
.catch((err) => {
|
||||
debug(logging, 'build', `├── ${colors.bold(colors.red('✘'))} ${route.component}`);
|
||||
debug('build', `├── ${colors.bold(colors.red('✘'))} ${route.component}`);
|
||||
throw err;
|
||||
}),
|
||||
};
|
||||
|
@ -69,50 +67,50 @@ export async function collectPagesData(opts: CollectPagesDataOptions): Promise<C
|
|||
}
|
||||
// dynamic route:
|
||||
const result = await getStaticPathsForRoute(opts, route)
|
||||
.then((routes) => {
|
||||
const label = routes.paths.length === 1 ? 'page' : 'pages';
|
||||
debug(logging, 'build', `├── ${colors.bold(colors.green('✔'))} ${route.component} → ${colors.magenta(`[${routes.paths.length} ${label}]`)}`);
|
||||
return routes;
|
||||
.then((_result) => {
|
||||
const label = _result.staticPaths.length === 1 ? 'page' : 'pages';
|
||||
debug('build', `├── ${colors.bold(colors.green('✔'))} ${route.component} → ${colors.magenta(`[${_result.staticPaths.length} ${label}]`)}`);
|
||||
return _result;
|
||||
})
|
||||
.catch((err) => {
|
||||
debug(logging, 'build', `├── ${colors.bold(colors.red('✗'))} ${route.component}`);
|
||||
debug('build', `├── ${colors.bold(colors.red('✗'))} ${route.component}`);
|
||||
throw err;
|
||||
});
|
||||
if (result.rss?.length) {
|
||||
for (let i = 0; i < result.rss.length; i++) {
|
||||
const rss = result.rss[i];
|
||||
if (rss.xml) {
|
||||
const { url, content } = rss.xml;
|
||||
if (content) {
|
||||
const rssFile = new URL(url.replace(/^\/?/, './'), astroConfig.dist);
|
||||
if (assets[fileURLToPath(rssFile)]) {
|
||||
throw new Error(`[getStaticPaths] RSS feed ${url} already exists.\nUse \`rss(data, {url: '...'})\` to choose a unique, custom URL. (${route.component})`);
|
||||
}
|
||||
assets[fileURLToPath(rssFile)] = content;
|
||||
const rssFn = generateRssFunction(astroConfig.buildOptions.site, route);
|
||||
for (const rssCallArg of result.rss) {
|
||||
const rssResult = rssFn(rssCallArg);
|
||||
if (rssResult.xml) {
|
||||
const { url, content } = rssResult.xml;
|
||||
if (content) {
|
||||
const rssFile = new URL(url.replace(/^\/?/, './'), astroConfig.dist);
|
||||
if (assets[fileURLToPath(rssFile)]) {
|
||||
throw new Error(`[getStaticPaths] RSS feed ${url} already exists.\nUse \`rss(data, {url: '...'})\` to choose a unique, custom URL. (${route.component})`);
|
||||
}
|
||||
}
|
||||
if (rss.xsl?.content) {
|
||||
const { url, content } = rss.xsl;
|
||||
const stylesheetFile = new URL(url.replace(/^\/?/, './'), astroConfig.dist);
|
||||
if (assets[fileURLToPath(stylesheetFile)]) {
|
||||
throw new Error(
|
||||
`[getStaticPaths] RSS feed stylesheet ${url} already exists.\nUse \`rss(data, {stylesheet: '...'})\` to choose a unique, custom URL. (${route.component})`
|
||||
);
|
||||
}
|
||||
assets[fileURLToPath(stylesheetFile)] = content;
|
||||
assets[fileURLToPath(rssFile)] = content;
|
||||
}
|
||||
}
|
||||
if (rssResult.xsl?.content) {
|
||||
const { url, content } = rssResult.xsl;
|
||||
const stylesheetFile = new URL(url.replace(/^\/?/, './'), astroConfig.dist);
|
||||
if (assets[fileURLToPath(stylesheetFile)]) {
|
||||
throw new Error(
|
||||
`[getStaticPaths] RSS feed stylesheet ${url} already exists.\nUse \`rss(data, {stylesheet: '...'})\` to choose a unique, custom URL. (${route.component})`
|
||||
);
|
||||
}
|
||||
assets[fileURLToPath(stylesheetFile)] = content;
|
||||
}
|
||||
}
|
||||
const finalPaths = result.staticPaths.map((staticPath) => staticPath.params && route.generate(staticPath.params)).filter(Boolean);
|
||||
allPages[route.component] = {
|
||||
route,
|
||||
paths: result.paths,
|
||||
paths: finalPaths,
|
||||
preload: await ssrPreload({
|
||||
astroConfig,
|
||||
filePath: new URL(`./${route.component}`, astroConfig.projectRoot),
|
||||
logging,
|
||||
mode: 'production',
|
||||
origin,
|
||||
pathname: result.paths[0],
|
||||
pathname: finalPaths[0],
|
||||
route,
|
||||
routeCache,
|
||||
viteServer,
|
||||
|
@ -124,18 +122,12 @@ export async function collectPagesData(opts: CollectPagesDataOptions): Promise<C
|
|||
return { assets, allPages };
|
||||
}
|
||||
|
||||
async function getStaticPathsForRoute(opts: CollectPagesDataOptions, route: RouteData): Promise<{ paths: string[]; rss?: RSSResult[] }> {
|
||||
async function getStaticPathsForRoute(opts: CollectPagesDataOptions, route: RouteData): Promise<RouteCacheEntry> {
|
||||
const { astroConfig, logging, routeCache, viteServer } = opts;
|
||||
if (!viteServer) throw new Error(`vite.createServer() not called!`);
|
||||
const filePath = new URL(`./${route.component}`, astroConfig.projectRoot);
|
||||
const mod = (await viteServer.ssrLoadModule(fileURLToPath(filePath))) as ComponentInstance;
|
||||
validateGetStaticPathsModule(mod);
|
||||
const rss = generateRssFunction(astroConfig.buildOptions.site, route);
|
||||
await assignStaticPaths(routeCache, route, mod, rss.generator);
|
||||
const staticPaths = routeCache[route.component];
|
||||
validateGetStaticPathsResult(staticPaths, logging);
|
||||
return {
|
||||
paths: staticPaths.map((staticPath) => staticPath.params && route.generate(staticPath.params)).filter(Boolean),
|
||||
rss: rss.rss,
|
||||
};
|
||||
const result = await callGetStaticPaths(mod, route, false, logging);
|
||||
routeCache.set(route, result);
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import type { ViteDevServer } from '../vite.js';
|
||||
import type { AstroConfig, RouteCache } from '../../@types/astro';
|
||||
import type { AstroConfig } from '../../@types/astro';
|
||||
import type { AllPagesData } from './types';
|
||||
import type { LogOptions } from '../logger';
|
||||
import type { ViteConfigWithSSR } from '../create-vite.js';
|
||||
|
@ -9,6 +9,7 @@ import vite from '../vite.js';
|
|||
import { createBuildInternals } from '../../core/build/internal.js';
|
||||
import { rollupPluginAstroBuildHTML } from '../../vite-plugin-build-html/index.js';
|
||||
import { rollupPluginAstroBuildCSS } from '../../vite-plugin-build-css/index.js';
|
||||
import { RouteCache } from '../ssr/route-cache.js';
|
||||
|
||||
export interface ScanBasedBuildOptions {
|
||||
allPages: AllPagesData;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { OutputChunk, OutputAsset, PreRenderedChunk, RollupOutput } from 'rollup';
|
||||
import type { Plugin as VitePlugin, UserConfig } from '../vite';
|
||||
import type { AstroConfig, Renderer, RouteCache, SSRElement } from '../../@types/astro';
|
||||
import type { AstroConfig, Renderer, SSRElement } from '../../@types/astro';
|
||||
import type { AllPagesData } from './types';
|
||||
import type { LogOptions } from '../logger';
|
||||
import type { ViteConfigWithSSR } from '../create-vite';
|
||||
|
@ -22,6 +22,7 @@ import { createResult } from '../ssr/result.js';
|
|||
import { renderPage } from '../../runtime/server/index.js';
|
||||
import { prepareOutDir } from './fs.js';
|
||||
import { vitePluginHoistedScripts } from './vite-plugin-hoisted-scripts.js';
|
||||
import { RouteCache } from '../ssr/route-cache.js';
|
||||
|
||||
export interface StaticBuildOptions {
|
||||
allPages: AllPagesData;
|
||||
|
@ -182,7 +183,7 @@ async function ssrBuild(opts: StaticBuildOptions, internals: BuildInternals, inp
|
|||
root: viteConfig.root,
|
||||
envPrefix: 'PUBLIC_',
|
||||
server: viteConfig.server,
|
||||
base: astroConfig.buildOptions.site ? fileURLToPath(new URL(astroConfig.buildOptions.site)) : '/',
|
||||
base: astroConfig.buildOptions.site ? new URL(astroConfig.buildOptions.site).pathname : '/',
|
||||
ssr: viteConfig.ssr,
|
||||
} as ViteConfigWithSSR);
|
||||
}
|
||||
|
@ -223,7 +224,7 @@ async function clientBuild(opts: StaticBuildOptions, internals: BuildInternals,
|
|||
root: viteConfig.root,
|
||||
envPrefix: 'PUBLIC_',
|
||||
server: viteConfig.server,
|
||||
base: astroConfig.buildOptions.site ? fileURLToPath(new URL(astroConfig.buildOptions.site)) : '/',
|
||||
base: astroConfig.buildOptions.site ? new URL(astroConfig.buildOptions.site).pathname : '/',
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -255,7 +256,7 @@ async function collectRenderers(opts: StaticBuildOptions): Promise<Renderer[]> {
|
|||
}
|
||||
|
||||
async function generatePages(result: RollupOutput, opts: StaticBuildOptions, internals: BuildInternals, facadeIdToPageDataMap: Map<string, PageBuildData>) {
|
||||
debug(opts.logging, 'generate', 'End build step, now generating');
|
||||
debug('build', 'Finish build. Begin generating.');
|
||||
|
||||
// Get renderers to be shared for each page generation.
|
||||
const renderers = await collectRenderers(opts);
|
||||
|
@ -330,15 +331,10 @@ async function generatePath(pathname: string, opts: StaticBuildOptions, gopts: G
|
|||
const [params, pageProps] = await getParamsAndProps({
|
||||
route: pageData.route,
|
||||
routeCache,
|
||||
logging,
|
||||
pathname,
|
||||
mod,
|
||||
// Do not validate as validation already occurred for static routes
|
||||
// and validation is relatively expensive.
|
||||
validate: false,
|
||||
});
|
||||
|
||||
debug(logging, 'generate', `Generating: ${pathname}`);
|
||||
debug('build', `Generating: ${pathname}`);
|
||||
|
||||
const rootpath = new URL(astroConfig.buildOptions.site || 'http://localhost/').pathname;
|
||||
const links = new Set<SSRElement>(
|
||||
|
|
|
@ -4,6 +4,7 @@ import { bold, blue, dim, red, grey, underline, yellow } from 'kleur/colors';
|
|||
import { performance } from 'perf_hooks';
|
||||
import { Writable } from 'stream';
|
||||
import stringWidth from 'string-width';
|
||||
import debugPackage from 'debug';
|
||||
import { format as utilFormat } from 'util';
|
||||
|
||||
type ConsoleStream = Writable & {
|
||||
|
@ -61,7 +62,7 @@ interface LogWritable<T> extends Writable {
|
|||
}
|
||||
|
||||
export type LoggerLevel = 'debug' | 'info' | 'warn' | 'error' | 'silent'; // same as Pino
|
||||
export type LoggerEvent = 'debug' | 'info' | 'warn' | 'error';
|
||||
export type LoggerEvent = 'info' | 'warn' | 'error';
|
||||
|
||||
export interface LogOptions {
|
||||
dest?: LogWritable<LogMessage>;
|
||||
|
@ -107,27 +108,35 @@ export function log(opts: LogOptions = {}, level: LoggerLevel, type: string | nu
|
|||
dest.write(event);
|
||||
}
|
||||
|
||||
/** Emit a message only shown in debug mode */
|
||||
export function debug(opts: LogOptions, type: string | null, ...messages: Array<any>) {
|
||||
return log(opts, 'debug', type, ...messages);
|
||||
const debuggers: Record<string, debugPackage.Debugger['log']> = {};
|
||||
/**
|
||||
* Emit a message only shown in debug mode.
|
||||
* Astro (along with many of its dependencies) uses the `debug` package for debug logging.
|
||||
* You can enable these logs with the `DEBUG=astro:*` environment variable.
|
||||
* More info https://github.com/debug-js/debug#environment-variables
|
||||
*/
|
||||
export function debug(type: string, ...messages: Array<any>) {
|
||||
const namespace = `astro:${type}`;
|
||||
debuggers[namespace] = debuggers[namespace] || debugPackage(namespace);
|
||||
return debuggers[namespace](messages);
|
||||
}
|
||||
|
||||
/** Emit a general info message (be careful using this too much!) */
|
||||
/** Emit a user-facing message. Useful for UI and other console messages. */
|
||||
export function info(opts: LogOptions, type: string | null, ...messages: Array<any>) {
|
||||
return log(opts, 'info', type, ...messages);
|
||||
}
|
||||
|
||||
/** Emit a warning a user should be aware of */
|
||||
/** Emit a warning message. Useful for high-priority messages that aren't necessarily errors. */
|
||||
export function warn(opts: LogOptions, type: string | null, ...messages: Array<any>) {
|
||||
return log(opts, 'warn', type, ...messages);
|
||||
}
|
||||
|
||||
/** Emit a fatal error message the user should address. */
|
||||
/** Emit a error message, Useful when Astro can't recover from some error. */
|
||||
export function error(opts: LogOptions, type: string | null, ...messages: Array<any>) {
|
||||
return log(opts, 'error', type, ...messages);
|
||||
}
|
||||
|
||||
type LogFn = typeof debug | typeof info | typeof warn | typeof error;
|
||||
type LogFn = typeof info | typeof warn | typeof error;
|
||||
|
||||
export function table(opts: LogOptions, columns: number[]) {
|
||||
return function logTable(logFn: LogFn, ...input: Array<any>) {
|
||||
|
@ -163,7 +172,6 @@ ${frame}
|
|||
|
||||
// A default logger for when too lazy to pass LogOptions around.
|
||||
export const logger = {
|
||||
debug: debug.bind(null, defaultLogOptions),
|
||||
info: info.bind(null, defaultLogOptions),
|
||||
warn: warn.bind(null, defaultLogOptions),
|
||||
error: error.bind(null, defaultLogOptions),
|
||||
|
|
|
@ -1,20 +1,7 @@
|
|||
import type { BuildResult } from 'esbuild';
|
||||
import type vite from '../vite';
|
||||
import type {
|
||||
AstroConfig,
|
||||
ComponentInstance,
|
||||
GetStaticPathsResult,
|
||||
GetStaticPathsResultKeyed,
|
||||
Params,
|
||||
Props,
|
||||
Renderer,
|
||||
RouteCache,
|
||||
RouteData,
|
||||
RuntimeMode,
|
||||
SSRElement,
|
||||
SSRError,
|
||||
} from '../../@types/astro';
|
||||
import type { LogOptions } from '../logger';
|
||||
import type { AstroConfig, ComponentInstance, Params, Props, Renderer, RouteData, RuntimeMode, SSRElement, SSRError } from '../../@types/astro';
|
||||
import { LogOptions, warn } from '../logger.js';
|
||||
|
||||
import eol from 'eol';
|
||||
import fs from 'fs';
|
||||
|
@ -24,10 +11,9 @@ import { renderPage } from '../../runtime/server/index.js';
|
|||
import { codeFrame, resolveDependency } from '../util.js';
|
||||
import { getStylesForURL } from './css.js';
|
||||
import { injectTags } from './html.js';
|
||||
import { generatePaginateFunction } from './paginate.js';
|
||||
import { getParams, validateGetStaticPathsModule, validateGetStaticPathsResult } from './routing.js';
|
||||
import { getParams, validateGetStaticPathsResult } from './routing.js';
|
||||
import { createResult } from './result.js';
|
||||
import { assignStaticPaths, ensureRouteCached, findPathItemByKey } from './route-cache.js';
|
||||
import { callGetStaticPaths, findPathItemByKey, RouteCache } from './route-cache.js';
|
||||
|
||||
const svelteStylesRE = /svelte\?svelte&type=style/;
|
||||
|
||||
|
@ -139,21 +125,7 @@ export async function preload({ astroConfig, filePath, viteServer }: SSROptions)
|
|||
return [renderers, mod];
|
||||
}
|
||||
|
||||
export async function getParamsAndProps({
|
||||
route,
|
||||
routeCache,
|
||||
logging,
|
||||
pathname,
|
||||
mod,
|
||||
validate = true,
|
||||
}: {
|
||||
route: RouteData | undefined;
|
||||
routeCache: RouteCache;
|
||||
pathname: string;
|
||||
mod: ComponentInstance;
|
||||
logging: LogOptions;
|
||||
validate?: boolean;
|
||||
}): Promise<[Params, Props]> {
|
||||
export async function getParamsAndProps({ route, routeCache, pathname }: { route: RouteData | undefined; routeCache: RouteCache; pathname: string }): Promise<[Params, Props]> {
|
||||
// Handle dynamic routes
|
||||
let params: Params = {};
|
||||
let pageProps: Props;
|
||||
|
@ -164,19 +136,12 @@ export async function getParamsAndProps({
|
|||
params = getParams(route.params)(paramsMatch);
|
||||
}
|
||||
}
|
||||
if (validate) {
|
||||
validateGetStaticPathsModule(mod);
|
||||
const routeCacheEntry = routeCache.get(route);
|
||||
if (!routeCacheEntry) {
|
||||
throw new Error(`[${route.component}] Internal error: route cache was empty, but expected to be full.`);
|
||||
}
|
||||
if (!routeCache[route.component]) {
|
||||
await assignStaticPaths(routeCache, route, mod);
|
||||
}
|
||||
if (validate) {
|
||||
// This validation is expensive so we only want to do it in dev.
|
||||
validateGetStaticPathsResult(routeCache[route.component], logging);
|
||||
}
|
||||
const staticPaths: GetStaticPathsResultKeyed = routeCache[route.component];
|
||||
const paramsKey = JSON.stringify(params);
|
||||
const matchedStaticPath = findPathItemByKey(staticPaths, paramsKey, logging);
|
||||
const matchedStaticPath = findPathItemByKey(routeCacheEntry.staticPaths, paramsKey);
|
||||
if (!matchedStaticPath) {
|
||||
throw new Error(`[getStaticPaths] route pattern matched, but no matching static path found. (${pathname})`);
|
||||
}
|
||||
|
@ -203,11 +168,16 @@ export async function render(renderers: Renderer[], mod: ComponentInstance, ssrO
|
|||
params = getParams(route.params)(paramsMatch);
|
||||
}
|
||||
}
|
||||
validateGetStaticPathsModule(mod);
|
||||
await ensureRouteCached(routeCache, route, mod);
|
||||
validateGetStaticPathsResult(routeCache[route.component], logging);
|
||||
const routePathParams: GetStaticPathsResult = routeCache[route.component];
|
||||
const matchedStaticPath = routePathParams.find(({ params: _params }) => JSON.stringify(_params) === JSON.stringify(params));
|
||||
let routeCacheEntry = routeCache.get(route);
|
||||
// TODO(fks): All of our getStaticPaths logic should live in a single place,
|
||||
// to prevent duplicate runs during the build. This is not expected to run
|
||||
// anymore and we should change this check to thrown an internal error.
|
||||
if (!routeCacheEntry) {
|
||||
warn(logging, 'routeCache', `Internal Warning: getStaticPaths() called twice during the build. (${route.component})`);
|
||||
routeCacheEntry = await callGetStaticPaths(mod, route, true, logging);
|
||||
routeCache.set(route, routeCacheEntry);
|
||||
}
|
||||
const matchedStaticPath = routeCacheEntry.staticPaths.find(({ params: _params }) => JSON.stringify(_params) === JSON.stringify(params));
|
||||
if (!matchedStaticPath) {
|
||||
throw new Error(`[getStaticPaths] route pattern matched, but no matching static path found. (${pathname})`);
|
||||
}
|
||||
|
|
|
@ -1,20 +1,22 @@
|
|||
import type { ComponentInstance, GetStaticPathsItem, GetStaticPathsResult, GetStaticPathsResultKeyed, RouteCache, RouteData } from '../../@types/astro';
|
||||
import type { LogOptions } from '../logger';
|
||||
import type { ComponentInstance, GetStaticPathsItem, GetStaticPathsResult, GetStaticPathsResultKeyed, RouteData, RSS } from '../../@types/astro';
|
||||
import { LogOptions, warn, debug } from '../logger.js';
|
||||
|
||||
import { debug } from '../logger.js';
|
||||
import { generatePaginateFunction } from '../ssr/paginate.js';
|
||||
import { validateGetStaticPathsModule, validateGetStaticPathsResult } from './routing.js';
|
||||
|
||||
type RSSFn = (...args: any[]) => any;
|
||||
|
||||
export async function callGetStaticPaths(mod: ComponentInstance, route: RouteData, rssFn?: RSSFn): Promise<GetStaticPathsResultKeyed> {
|
||||
export async function callGetStaticPaths(mod: ComponentInstance, route: RouteData, isValidate: boolean, logging: LogOptions): Promise<RouteCacheEntry> {
|
||||
validateGetStaticPathsModule(mod);
|
||||
const resultInProgress = {
|
||||
rss: [] as RSS[],
|
||||
};
|
||||
const staticPaths: GetStaticPathsResult = await (
|
||||
await mod.getStaticPaths!({
|
||||
paginate: generatePaginateFunction(route),
|
||||
rss:
|
||||
rssFn ||
|
||||
(() => {
|
||||
/* noop */
|
||||
}),
|
||||
rss: (data) => {
|
||||
resultInProgress.rss.push(data);
|
||||
},
|
||||
})
|
||||
).flat();
|
||||
|
||||
|
@ -24,31 +26,59 @@ export async function callGetStaticPaths(mod: ComponentInstance, route: RouteDat
|
|||
const paramsKey = JSON.stringify(sp.params);
|
||||
keyedStaticPaths.keyed.set(paramsKey, sp);
|
||||
}
|
||||
|
||||
return keyedStaticPaths;
|
||||
if (isValidate) {
|
||||
validateGetStaticPathsResult(keyedStaticPaths, logging);
|
||||
}
|
||||
return {
|
||||
rss: resultInProgress.rss,
|
||||
staticPaths: keyedStaticPaths,
|
||||
};
|
||||
}
|
||||
|
||||
export async function assignStaticPaths(routeCache: RouteCache, route: RouteData, mod: ComponentInstance, rssFn?: RSSFn): Promise<void> {
|
||||
const staticPaths = await callGetStaticPaths(mod, route, rssFn);
|
||||
routeCache[route.component] = staticPaths;
|
||||
export interface RouteCacheEntry {
|
||||
staticPaths: GetStaticPathsResultKeyed;
|
||||
rss: RSS[];
|
||||
}
|
||||
|
||||
export async function ensureRouteCached(routeCache: RouteCache, route: RouteData, mod: ComponentInstance, rssFn?: RSSFn): Promise<GetStaticPathsResultKeyed> {
|
||||
if (!routeCache[route.component]) {
|
||||
const staticPaths = await callGetStaticPaths(mod, route, rssFn);
|
||||
routeCache[route.component] = staticPaths;
|
||||
return staticPaths;
|
||||
} else {
|
||||
return routeCache[route.component];
|
||||
/**
|
||||
* Manange the route cache, responsible for caching data related to each route,
|
||||
* including the result of calling getStaticPath() so that it can be reused across
|
||||
* responses during dev and only ever called once during build.
|
||||
*/
|
||||
export class RouteCache {
|
||||
private logging: LogOptions;
|
||||
private cache: Record<string, RouteCacheEntry> = {};
|
||||
|
||||
constructor(logging: LogOptions) {
|
||||
this.logging = logging;
|
||||
}
|
||||
|
||||
/** Clear the cache. */
|
||||
clearAll() {
|
||||
this.cache = {};
|
||||
}
|
||||
|
||||
set(route: RouteData, entry: RouteCacheEntry): void {
|
||||
// NOTE: This shouldn't be called on an already-cached component.
|
||||
// Warn here so that an unexpected double-call of getStaticPaths()
|
||||
// isn't invisible and developer can track down the issue.
|
||||
if (this.cache[route.component]) {
|
||||
warn(this.logging, 'routeCache', `Internal Warning: route cache overwritten. (${route.component})`);
|
||||
}
|
||||
this.cache[route.component] = entry;
|
||||
}
|
||||
|
||||
get(route: RouteData): RouteCacheEntry | undefined {
|
||||
return this.cache[route.component];
|
||||
}
|
||||
}
|
||||
|
||||
export function findPathItemByKey(staticPaths: GetStaticPathsResultKeyed, paramsKey: string, logging: LogOptions) {
|
||||
export function findPathItemByKey(staticPaths: GetStaticPathsResultKeyed, paramsKey: string) {
|
||||
let matchedStaticPath = staticPaths.keyed.get(paramsKey);
|
||||
if (matchedStaticPath) {
|
||||
return matchedStaticPath;
|
||||
}
|
||||
|
||||
debug(logging, 'findPathItemByKey', `Unexpected cache miss looking for ${paramsKey}`);
|
||||
debug('findPathItemByKey', `Unexpected cache miss looking for ${paramsKey}`);
|
||||
matchedStaticPath = staticPaths.find(({ params: _params }) => JSON.stringify(_params) === paramsKey);
|
||||
}
|
||||
|
|
|
@ -83,33 +83,29 @@ export function generateRSSStylesheet() {
|
|||
}
|
||||
|
||||
/** Generated function to be run */
|
||||
export function generateRssFunction(site: string | undefined, route: RouteData): { generator: RSSFunction; rss?: RSSResult[] } {
|
||||
let results: RSSResult[] = [];
|
||||
return {
|
||||
generator: function rssUtility(args: RSS) {
|
||||
if (!site) {
|
||||
throw new Error(`[${route.component}] rss() tried to generate RSS but "buildOptions.site" missing in astro.config.mjs`);
|
||||
}
|
||||
let result: RSSResult = {} as any;
|
||||
const { dest, ...rssData } = args;
|
||||
const feedURL = dest || '/rss.xml';
|
||||
if (rssData.stylesheet === true) {
|
||||
rssData.stylesheet = feedURL.replace(/\.xml$/, '.xsl');
|
||||
result.xsl = {
|
||||
url: rssData.stylesheet,
|
||||
content: generateRSSStylesheet(),
|
||||
};
|
||||
} else if (typeof rssData.stylesheet === 'string') {
|
||||
result.xsl = {
|
||||
url: rssData.stylesheet,
|
||||
};
|
||||
}
|
||||
result.xml = {
|
||||
url: feedURL,
|
||||
content: generateRSS({ rssData, site, srcFile: route.component }),
|
||||
export function generateRssFunction(site: string | undefined, route: RouteData): RSSFunction {
|
||||
return function rssUtility(args: RSS): RSSResult {
|
||||
if (!site) {
|
||||
throw new Error(`[${route.component}] rss() tried to generate RSS but "buildOptions.site" missing in astro.config.mjs`);
|
||||
}
|
||||
let result: RSSResult = {} as any;
|
||||
const { dest, ...rssData } = args;
|
||||
const feedURL = dest || '/rss.xml';
|
||||
if (rssData.stylesheet === true) {
|
||||
rssData.stylesheet = feedURL.replace(/\.xml$/, '.xsl');
|
||||
result.xsl = {
|
||||
url: rssData.stylesheet,
|
||||
content: generateRSSStylesheet(),
|
||||
};
|
||||
results.push(result);
|
||||
},
|
||||
rss: results,
|
||||
} else if (typeof rssData.stylesheet === 'string') {
|
||||
result.xsl = {
|
||||
url: rssData.stylesheet,
|
||||
};
|
||||
}
|
||||
result.xml = {
|
||||
url: feedURL,
|
||||
content: generateRSS({ rssData, site, srcFile: route.component }),
|
||||
};
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type vite from '../core/vite';
|
||||
import type http from 'http';
|
||||
import type { AstroConfig, ManifestData, RouteCache, RouteData } from '../@types/astro';
|
||||
import type { AstroConfig, ManifestData, RouteData } from '../@types/astro';
|
||||
import { info, LogOptions } from '../core/logger.js';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { createRouteManifest, matchRoute } from '../core/ssr/routing.js';
|
||||
|
@ -12,6 +12,7 @@ import * as msg from '../core/messages.js';
|
|||
|
||||
import notFoundTemplate, { subpathNotUsedTemplate } from '../template/4xx.js';
|
||||
import serverErrorTemplate from '../template/5xx.js';
|
||||
import { RouteCache } from '../core/ssr/route-cache.js';
|
||||
|
||||
interface AstroPluginOptions {
|
||||
config: AstroConfig;
|
||||
|
@ -126,22 +127,19 @@ export default function createPlugin({ config, logging }: AstroPluginOptions): v
|
|||
name: 'astro:server',
|
||||
configureServer(viteServer) {
|
||||
const pagesDirectory = fileURLToPath(config.pages);
|
||||
let routeCache: RouteCache = {};
|
||||
let routeCache = new RouteCache(logging);
|
||||
let manifest: ManifestData = createRouteManifest({ config: config }, logging);
|
||||
/** rebuild the route cache + manifest if the changed file impacts routing. */
|
||||
function rebuildManifestIfNeeded(file: string) {
|
||||
if (file.startsWith(pagesDirectory)) {
|
||||
routeCache = {};
|
||||
/** rebuild the route cache + manifest, as needed. */
|
||||
function rebuildManifest(needsManifestRebuild: boolean, file: string) {
|
||||
routeCache.clearAll();
|
||||
if (needsManifestRebuild) {
|
||||
manifest = createRouteManifest({ config: config }, logging);
|
||||
}
|
||||
}
|
||||
// Rebuild route manifest on file change, if needed.
|
||||
viteServer.watcher.on('add', rebuildManifestIfNeeded);
|
||||
viteServer.watcher.on('unlink', rebuildManifestIfNeeded);
|
||||
// No need to rebuild routes on content-only changes.
|
||||
// However, we DO want to clear the cache in case
|
||||
// the change caused a getStaticPaths() return to change.
|
||||
viteServer.watcher.on('change', () => (routeCache = {}));
|
||||
viteServer.watcher.on('add', rebuildManifest.bind(null, true));
|
||||
viteServer.watcher.on('unlink', rebuildManifest.bind(null, true));
|
||||
viteServer.watcher.on('change', rebuildManifest.bind(null, false));
|
||||
return () => {
|
||||
removeViteHttpMiddleware(viteServer.middlewares);
|
||||
viteServer.middlewares.use(async (req, res) => {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { AstroConfig, RouteCache } from '../@types/astro';
|
||||
import type { AstroConfig } from '../@types/astro';
|
||||
import type { LogOptions } from '../core/logger.js';
|
||||
import type { ViteDevServer, Plugin as VitePlugin } from '../core/vite';
|
||||
import type { OutputChunk, PreRenderedChunk } from 'rollup';
|
||||
|
@ -15,6 +15,7 @@ import { isBuildableImage, isBuildableLink, isHoistedScript, isInSrcDirectory, h
|
|||
import { render as ssrRender } from '../core/ssr/index.js';
|
||||
import { getAstroStyleId, getAstroPageStyleId } from '../vite-plugin-build-css/index.js';
|
||||
import { prependDotSlash, removeEndingForwardSlash } from '../core/path.js';
|
||||
import { RouteCache } from '../core/ssr/route-cache.js';
|
||||
|
||||
// This package isn't real ESM, so have to coerce it
|
||||
const matchSrcset: typeof srcsetParse = (srcsetParse as any).default;
|
||||
|
|
|
@ -1752,7 +1752,7 @@
|
|||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/debug@^4.0.0":
|
||||
"@types/debug@^4.0.0", "@types/debug@^4.1.7":
|
||||
version "4.1.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82"
|
||||
integrity sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==
|
||||
|
|
Loading…
Reference in a new issue