diff --git a/packages/astro/package.json b/packages/astro/package.json index bd21e98d183d..494ca3f49672 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -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", diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index abe62b13a89e..dd43cba5cda9 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -341,8 +341,6 @@ export interface RouteData { type: 'page'; } -export type RouteCache = Record; - 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 }; diff --git a/packages/astro/src/cli/index.ts b/packages/astro/src/cli/index.ts index ae8b0ac560de..3860d9bbab08 100644 --- a/packages/astro/src/cli/index.ts +++ b/packages/astro/src/cli/index.ts @@ -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); } diff --git a/packages/astro/src/core/build/index.ts b/packages/astro/src/core/build/index.ts index a93b1f81b072..024dc4d05354 100644 --- a/packages/astro/src/core/build/index.ts +++ b/packages/astro/src/core/build/index.ts @@ -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`; diff --git a/packages/astro/src/core/build/page-data.ts b/packages/astro/src/core/build/page-data.ts index 43620ac4ef38..106e09a057e0 100644 --- a/packages/astro/src/core/build/page-data.ts +++ b/packages/astro/src/core/build/page-data.ts @@ -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 { 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 { - 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})`); } + assets[fileURLToPath(rssFile)] = content; } - 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; + } + 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 { +async function getStaticPathsForRoute(opts: CollectPagesDataOptions, route: RouteData): Promise { 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; } diff --git a/packages/astro/src/core/build/scan-based-build.ts b/packages/astro/src/core/build/scan-based-build.ts index 3a295c0dbd4e..c11795fd81ba 100644 --- a/packages/astro/src/core/build/scan-based-build.ts +++ b/packages/astro/src/core/build/scan-based-build.ts @@ -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; diff --git a/packages/astro/src/core/build/static-build.ts b/packages/astro/src/core/build/static-build.ts index 0df6194dd332..ad2b51203988 100644 --- a/packages/astro/src/core/build/static-build.ts +++ b/packages/astro/src/core/build/static-build.ts @@ -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 { } async function generatePages(result: RollupOutput, opts: StaticBuildOptions, internals: BuildInternals, facadeIdToPageDataMap: Map) { - 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( diff --git a/packages/astro/src/core/logger.ts b/packages/astro/src/core/logger.ts index ed6240bcc616..c00e71347abb 100644 --- a/packages/astro/src/core/logger.ts +++ b/packages/astro/src/core/logger.ts @@ -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 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; @@ -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) { - return log(opts, 'debug', type, ...messages); +const debuggers: Record = {}; +/** + * 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) { + 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) { 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) { 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) { 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) { @@ -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), diff --git a/packages/astro/src/core/ssr/index.ts b/packages/astro/src/core/ssr/index.ts index e7299d20a011..c4d214a72589 100644 --- a/packages/astro/src/core/ssr/index.ts +++ b/packages/astro/src/core/ssr/index.ts @@ -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})`); } diff --git a/packages/astro/src/core/ssr/route-cache.ts b/packages/astro/src/core/ssr/route-cache.ts index 3ebfc4d7ef9a..11988d36b89f 100644 --- a/packages/astro/src/core/ssr/route-cache.ts +++ b/packages/astro/src/core/ssr/route-cache.ts @@ -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 { +export async function callGetStaticPaths(mod: ComponentInstance, route: RouteData, isValidate: boolean, logging: LogOptions): Promise { + 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 { - 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 { - 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 = {}; + + 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); } diff --git a/packages/astro/src/core/ssr/rss.ts b/packages/astro/src/core/ssr/rss.ts index cba4b4e5dec7..18cce36a1be8 100644 --- a/packages/astro/src/core/ssr/rss.ts +++ b/packages/astro/src/core/ssr/rss.ts @@ -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; }; } diff --git a/packages/astro/src/vite-plugin-astro-server/index.ts b/packages/astro/src/vite-plugin-astro-server/index.ts index 701457f9376d..de57e15930ff 100644 --- a/packages/astro/src/vite-plugin-astro-server/index.ts +++ b/packages/astro/src/vite-plugin-astro-server/index.ts @@ -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) => { diff --git a/packages/astro/src/vite-plugin-build-html/index.ts b/packages/astro/src/vite-plugin-build-html/index.ts index 9a0bc6957b08..542340581073 100644 --- a/packages/astro/src/vite-plugin-build-html/index.ts +++ b/packages/astro/src/vite-plugin-build-html/index.ts @@ -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; diff --git a/yarn.lock b/yarn.lock index f46009e71fca..e08bce4e4d09 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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==