diff --git a/.changeset/small-radios-remain.md b/.changeset/small-radios-remain.md new file mode 100644 index 0000000000..d0747a700e --- /dev/null +++ b/.changeset/small-radios-remain.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Update CLI error format and style diff --git a/packages/astro/src/cli/check.ts b/packages/astro/src/cli/check.ts index 499ac9afdd..ffdc246df4 100644 --- a/packages/astro/src/cli/check.ts +++ b/packages/astro/src/cli/check.ts @@ -57,7 +57,7 @@ function offsetAt({ line, character }: { line: number; character: number }, text return i; } -function pad(str: string, len: number) { +function generateString(str: string, len: number) { return Array.from({ length: len }, () => str).join(''); } @@ -86,8 +86,8 @@ export async function check(astroConfig: AstroConfig) { const lineNumStr = d.range.start.line.toString(); const lineNumLen = lineNumStr.length; console.error(`${bgWhite(black(lineNumStr))} ${str}`); - let tildes = pad('~', d.range.end.character - d.range.start.character); - let spaces = pad(' ', d.range.start.character + lineNumLen - 1); + let tildes = generateString('~', d.range.end.character - d.range.start.character); + let spaces = generateString(' ', d.range.start.character + lineNumLen - 1); console.error(` ${spaces}${bold(red(tildes))}\n`); result.errors++; break; diff --git a/packages/astro/src/cli/index.ts b/packages/astro/src/cli/index.ts index 4108d4984f..5b4f2abbc4 100644 --- a/packages/astro/src/cli/index.ts +++ b/packages/astro/src/cli/index.ts @@ -12,8 +12,9 @@ import add from '../core/add/index.js'; import devServer from '../core/dev/index.js'; import preview from '../core/preview/index.js'; import { check } from './check.js'; -import { formatConfigError, loadConfig } from '../core/config.js'; -import { printHelp } from '../core/messages.js'; +import { loadConfig } from '../core/config.js'; +import { printHelp, formatErrorMessage, formatConfigErrorMessage } from '../core/messages.js'; +import { createSafeError } from '../core/util.js'; type Arguments = yargs.Arguments; type CLICommand = 'help' | 'version' | 'add' | 'dev' | 'build' | 'preview' | 'reload' | 'check'; @@ -102,40 +103,33 @@ export async function cli(args: string[]) { // For now, `add` has to resolve the config again internally config = await loadConfig({ cwd: projectRoot, flags }); } catch (err) { - throwAndExit(err); - return; + return throwAndExit(err); } switch (cmd) { case 'add': { try { const packages = flags._.slice(3) as string[]; - await add(packages, { cwd: projectRoot, flags, logging }); - process.exit(0); + return await add(packages, { cwd: projectRoot, flags, logging }); } catch (err) { - throwAndExit(err); + return throwAndExit(err); } - return; } case 'dev': { try { await devServer(config, { logging }); - - await new Promise(() => {}); // don’t close dev server + return await new Promise(() => {}); // lives forever } catch (err) { - throwAndExit(err); + return throwAndExit(err); } - return; } case 'build': { try { - await build(config, { logging }); - process.exit(0); + return await build(config, { logging }); } catch (err) { - throwAndExit(err); + return throwAndExit(err); } - return; } case 'check': { @@ -145,11 +139,10 @@ export async function cli(args: string[]) { case 'preview': { try { - await preview(config, { logging }); // this will keep running + return await preview(config, { logging }); // this will keep running } catch (err) { - throwAndExit(err); + return throwAndExit(err); } - return; } default: { @@ -159,14 +152,11 @@ export async function cli(args: string[]) { } /** Display error and exit */ -function throwAndExit(err: any) { +function throwAndExit(err: unknown) { 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'))); + console.error(formatConfigErrorMessage(err)); } else { - console.error(colors.red(err.toString() || err)); + console.error(formatErrorMessage(createSafeError(err))); } process.exit(1); } diff --git a/packages/astro/src/core/add/index.ts b/packages/astro/src/core/add/index.ts index 74045cc6e9..d1f63a9cf4 100644 --- a/packages/astro/src/core/add/index.ts +++ b/packages/astro/src/core/add/index.ts @@ -54,6 +54,7 @@ export default async function add(names: string[], { cwd, flags, logging }: AddO } applyPolyfill(); + // If no integrations were given, prompt the user for some popular ones. if (names.length === 0) { const response = await prompts([ { @@ -72,16 +73,13 @@ export default async function add(names: string[], { cwd, flags, logging }: AddO }, ]); - if (!response.frameworks && !response.addons) { - info(logging, null, msg.cancelled(`Integrations skipped.`, `You can always run ${cyan('astro add')} later!`)); - return; - } - const selected = [response.frameworks ?? [], response.addons ?? []].flat(1); - if (selected.length === 0) { - error(logging, null, `\n${red('No integrations specified!')}\n${dim('Try running')} astro add again.`); - return; - } - names = selected; + names = [...(response.frameworks ?? []), ...(response.addons ?? [])]; + } + + // If still empty after prompting, exit gracefully. + if (names.length === 0) { + error(logging, null, `No integrations specified.`); + return; } // Some packages might have a common alias! We normalize those here. diff --git a/packages/astro/src/core/build/common.ts b/packages/astro/src/core/build/common.ts index 5407f66fdd..a29aa356c2 100644 --- a/packages/astro/src/core/build/common.ts +++ b/packages/astro/src/core/build/common.ts @@ -4,7 +4,7 @@ import { appendForwardSlash } from '../../core/path.js'; const STATUS_CODE_PAGES = new Set(['/404', '/500']); -export function getOutRoot(astroConfig: AstroConfig): URL { +function getOutRoot(astroConfig: AstroConfig): URL { return new URL('./', astroConfig.dist); } diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index 119274e76d..84d9dd7e2c 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -1,21 +1,21 @@ -import type { OutputAsset, OutputChunk, RollupOutput } from 'rollup'; -import type { AstroConfig, ComponentInstance, EndpointHandler, SSRLoadedRenderer } from '../../@types/astro'; -import type { PageBuildData, StaticBuildOptions, SingleFileBuiltModule } from './types'; -import type { BuildInternals } from '../../core/build/internal.js'; -import type { RenderOptions } from '../../core/render/core'; - import fs from 'fs'; +import { bgGreen, bgMagenta, black, cyan, dim, green, magenta } from 'kleur/colors'; import npath from 'path'; +import type { OutputAsset, OutputChunk, RollupOutput } from 'rollup'; import { fileURLToPath } from 'url'; -import { debug, error, info } from '../../core/logger.js'; -import { prependForwardSlash } from '../../core/path.js'; +import type { AstroConfig, ComponentInstance, EndpointHandler, SSRLoadedRenderer } from '../../@types/astro'; +import type { BuildInternals } from '../../core/build/internal.js'; +import { debug, info } from '../../core/logger.js'; +import { appendForwardSlash, prependForwardSlash } from '../../core/path.js'; +import type { RenderOptions } from '../../core/render/core'; import { BEFORE_HYDRATION_SCRIPT_ID } from '../../vite-plugin-scripts/index.js'; import { call as callEndpoint } from '../endpoint/index.js'; import { render } from '../render/core.js'; import { createLinkStylesheetElementSet, createModuleScriptElementWithSrcSet } from '../render/ssr-element.js'; -import { getOutFile, getOutRoot, getOutFolder } from './common.js'; -import { getPageDataByComponent, eachPageData } from './internal.js'; -import { bgMagenta, black, cyan, dim, magenta } from 'kleur/colors'; +import { getOutputFilename } from '../util.js'; +import { getOutFile, getOutFolder } from './common.js'; +import { eachPageData, getPageDataByComponent } from './internal.js'; +import type { PageBuildData, SingleFileBuiltModule, StaticBuildOptions } from './types'; import { getTimeStat } from './util.js'; // Render is usually compute, which Node.js can't parallelize well. @@ -67,7 +67,8 @@ export function chunkIsPage(astroConfig: AstroConfig, output: OutputAsset | Outp } export async function generatePages(result: RollupOutput, opts: StaticBuildOptions, internals: BuildInternals, facadeIdToPageDataMap: Map) { - info(opts.logging, null, `\n${bgMagenta(black(' generating static routes '))}\n`); + const timer = performance.now(); + info(opts.logging, null, `\n${bgGreen(black(' generating static routes '))}`); const ssr = !!opts.astroConfig._ctx.adapter?.serverEntrypoint; const serverEntry = opts.buildConfig.serverEntry; @@ -78,6 +79,7 @@ export async function generatePages(result: RollupOutput, opts: StaticBuildOptio for (const pageData of eachPageData(internals)) { await generatePage(opts, internals, pageData, ssrEntry); } + info(opts.logging, null, dim(`Completed in ${getTimeStat(timer, performance.now())}.\n`)); } async function generatePage( @@ -109,31 +111,18 @@ async function generatePage( renderers, }; - const icon = pageData.route.type === 'page' ? cyan('') : magenta('{-}'); + const icon = pageData.route.type === 'page' ? green('▶') : magenta('λ'); info(opts.logging, null, `${icon} ${pageData.route.component}`); - // Throttle the paths to avoid overloading the CPU with too many tasks. - const renderPromises = []; - for (const paths of throttle(MAX_CONCURRENT_RENDERS, pageData.paths)) { - for (const path of paths) { - renderPromises.push(generatePath(path, opts, generationOptions)); - } - // This blocks generating more paths until these 10 complete. - await Promise.all(renderPromises); + for (let i = 0; i < pageData.paths.length; i++) { + const path = pageData.paths[i]; + await generatePath(path, opts, generationOptions); const timeEnd = performance.now(); const timeChange = getTimeStat(timeStart, timeEnd); - let shouldLogTimeChange = !getTimeStat(timeStart, timeEnd).startsWith('0'); - for (const path of paths) { - const timeIncrease = shouldLogTimeChange ? ` ${dim(`+${timeChange}`)}` : ''; - info(opts.logging, null, ` ${dim('┃')} ${path}${timeIncrease}`); - // Should only log build time on the first generated path - // Logging for all generated paths adds extra noise - shouldLogTimeChange = false; - } - // Reset timeStart for the next batch of rendered paths - timeStart = performance.now(); - // This empties the array without allocating a new one. - renderPromises.length = 0; + const timeIncrease = `(+${timeChange})`; + const filePath = getOutputFilename(opts.astroConfig, path); + const lineIcon = i === pageData.paths.length - 1 ? '└─' : '├─'; + info(opts.logging, null, ` ${cyan(lineIcon)} ${dim(filePath)} ${dim(timeIncrease)}`); } } @@ -175,65 +164,61 @@ async function generatePath(pathname: string, opts: StaticBuildOptions, gopts: G } } - try { - const options: RenderOptions = { - legacyBuild: false, - links, - logging, - markdownRender: astroConfig.markdownOptions.render, - mod, - origin, - pathname, - scripts, - renderers, - async resolve(specifier: string) { - const hashedFilePath = internals.entrySpecifierToBundleMap.get(specifier); - if (typeof hashedFilePath !== 'string') { - // If no "astro:scripts/before-hydration.js" script exists in the build, - // then we can assume that no before-hydration scripts are needed. - // Return this as placeholder, which will be ignored by the browser. - // TODO: In the future, we hope to run this entire script through Vite, - // removing the need to maintain our own custom Vite-mimic resolve logic. - if (specifier === BEFORE_HYDRATION_SCRIPT_ID) { - return 'data:text/javascript;charset=utf-8,//[no before-hydration script]'; - } - throw new Error(`Cannot find the built path for ${specifier}`); + const options: RenderOptions = { + legacyBuild: false, + links, + logging, + markdownRender: astroConfig.markdownOptions.render, + mod, + origin, + pathname, + scripts, + renderers, + async resolve(specifier: string) { + const hashedFilePath = internals.entrySpecifierToBundleMap.get(specifier); + if (typeof hashedFilePath !== 'string') { + // If no "astro:scripts/before-hydration.js" script exists in the build, + // then we can assume that no before-hydration scripts are needed. + // Return this as placeholder, which will be ignored by the browser. + // TODO: In the future, we hope to run this entire script through Vite, + // removing the need to maintain our own custom Vite-mimic resolve logic. + if (specifier === BEFORE_HYDRATION_SCRIPT_ID) { + return 'data:text/javascript;charset=utf-8,//[no before-hydration script]'; } - const relPath = npath.posix.relative(pathname, '/' + hashedFilePath); - const fullyRelativePath = relPath[0] === '.' ? relPath : './' + relPath; - return fullyRelativePath; - }, - method: 'GET', - headers: new Headers(), - route: pageData.route, - routeCache, - site: astroConfig.buildOptions.site, - ssr: opts.astroConfig.buildOptions.experimentalSsr, - }; - - let body: string; - if (pageData.route.type === 'endpoint') { - const result = await callEndpoint(mod as unknown as EndpointHandler, options); - - if (result.type === 'response') { - throw new Error(`Returning a Response from an endpoint is not supported in SSG mode.`); + throw new Error(`Cannot find the built path for ${specifier}`); } - body = result.body; - } else { - const result = await render(options); + const relPath = npath.posix.relative(pathname, '/' + hashedFilePath); + const fullyRelativePath = relPath[0] === '.' ? relPath : './' + relPath; + return fullyRelativePath; + }, + method: 'GET', + headers: new Headers(), + route: pageData.route, + routeCache, + site: astroConfig.buildOptions.site, + ssr: opts.astroConfig.buildOptions.experimentalSsr, + }; - // If there's a redirect or something, just do nothing. - if (result.type !== 'html') { - return; - } - body = result.html; + let body: string; + if (pageData.route.type === 'endpoint') { + const result = await callEndpoint(mod as unknown as EndpointHandler, options); + + if (result.type === 'response') { + throw new Error(`Returning a Response from an endpoint is not supported in SSG mode.`); } + body = result.body; + } else { + const result = await render(options); - const outFolder = getOutFolder(astroConfig, pathname, pageData.route.type); - const outFile = getOutFile(astroConfig, outFolder, pathname, pageData.route.type); - await fs.promises.mkdir(outFolder, { recursive: true }); - await fs.promises.writeFile(outFile, body, 'utf-8'); - } catch (err) { - error(opts.logging, 'build', `Error rendering:`, err); + // If there's a redirect or something, just do nothing. + if (result.type !== 'html') { + return; + } + body = result.html; } + + const outFolder = getOutFolder(astroConfig, pathname, pageData.route.type); + const outFile = getOutFile(astroConfig, outFolder, pathname, pageData.route.type); + await fs.promises.mkdir(outFolder, { recursive: true }); + await fs.promises.writeFile(outFile, body, 'utf-8'); } diff --git a/packages/astro/src/core/build/index.ts b/packages/astro/src/core/build/index.ts index 7a360b01e3..4962ce1c7f 100644 --- a/packages/astro/src/core/build/index.ts +++ b/packages/astro/src/core/build/index.ts @@ -16,6 +16,8 @@ import { staticBuild } from './static-build.js'; import { RouteCache } from '../render/route-cache.js'; import { runHookBuildDone, runHookBuildStart, runHookConfigDone, runHookConfigSetup } from '../../integrations/index.js'; import { getTimeStat } from './util.js'; +import { createSafeError } from '../util.js'; +import { fixViteErrorMessage } from '../errors.js'; export interface BuildOptions { mode?: string; @@ -24,8 +26,9 @@ export interface BuildOptions { /** `astro build` */ export default async function build(config: AstroConfig, options: BuildOptions = { logging: defaultLogOptions }): Promise { + applyPolyfill(); const builder = new AstroBuilder(config, options); - await builder.build(); + await builder.run(); } class AstroBuilder { @@ -35,32 +38,30 @@ class AstroBuilder { private origin: string; private routeCache: RouteCache; private manifest: ManifestData; - private viteServer?: vite.ViteDevServer; - private viteConfig?: ViteConfigWithSSR; + private timer: Record; constructor(config: AstroConfig, options: BuildOptions) { - applyPolyfill(); - if (!config.buildOptions.site && config.buildOptions.sitemap !== false) { warn(options.logging, 'config', `Set "buildOptions.site" to generate correct canonical URLs and sitemap`); } - - if (options.mode) this.mode = options.mode; + if (options.mode) { + this.mode = options.mode; + } 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); + this.timer = {}; } - async build() { - info(this.logging, 'build', 'Initial setup...'); - - const { logging, origin } = this; - const timer: Record = {}; - timer.init = performance.now(); - timer.viteStart = performance.now(); + /** Setup Vite and run any async setup logic that couldn't run inside of the constructor. */ + private async setup() { + debug('build', 'Initial setup...'); + const { logging } = this; + this.timer.init = performance.now(); + this.timer.viteStart = performance.now(); this.config = await runHookConfigSetup({ config: this.config, command: 'build' }); const viteConfig = await createVite( { @@ -74,10 +75,14 @@ class AstroBuilder { ); await runHookConfigDone({ config: this.config }); warnIfUsingExperimentalSSR(logging, this.config); - this.viteConfig = viteConfig; const viteServer = await vite.createServer(viteConfig); - this.viteServer = viteServer; - debug('build', timerMessage('Vite started', timer.viteStart)); + debug('build', timerMessage('Vite started', this.timer.viteStart)); + return { viteConfig, viteServer }; + } + + /** Run the build logic. build() is marked private because usage should go through ".run()" */ + private async build({ viteConfig, viteServer }: { viteConfig: ViteConfigWithSSR; viteServer: vite.ViteDevServer }) { + const { origin } = this; const buildConfig: BuildConfig = { client: new URL('./client/', this.config.dist), server: new URL('./server/', this.config.dist), @@ -86,15 +91,15 @@ class AstroBuilder { }; await runHookBuildStart({ config: this.config, buildConfig }); - info(this.logging, 'build', 'Collecting page data...'); - timer.loadStart = performance.now(); + info(this.logging, 'build', 'Collecting build information...'); + this.timer.loadStart = performance.now(); const { assets, allPages } = await collectPagesData({ astroConfig: this.config, logging: this.logging, manifest: this.manifest, origin, routeCache: this.routeCache, - viteServer: this.viteServer, + viteServer, ssr: this.config.buildOptions.experimentalSsr, }); @@ -104,21 +109,21 @@ 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('build', timerMessage(`Skipping draft page ${page}`, timer.loadStart)); + debug('build', timerMessage(`Skipping draft page ${page}`, this.timer.loadStart)); delete allPages[page]; } } }); - debug('build', timerMessage('All pages loaded', timer.loadStart)); + debug('build', timerMessage('All pages loaded', this.timer.loadStart)); // The names of each pages const pageNames: string[] = []; // Bundle the assets in your final build: This currently takes the HTML output // of every page (stored in memory) and bundles the assets pointed to on those pages. - timer.buildStart = performance.now(); - info(this.logging, 'build', colors.dim(`Completed in ${getTimeStat(timer.init, performance.now())}`)); + this.timer.buildStart = performance.now(); + info(this.logging, 'build', colors.dim(`Completed in ${getTimeStat(this.timer.init, performance.now())}.`)); // Use the new faster static based build. if (!this.config.buildOptions.legacyBuild) { @@ -130,7 +135,7 @@ class AstroBuilder { origin: this.origin, pageNames, routeCache: this.routeCache, - viteConfig: this.viteConfig, + viteConfig, buildConfig, }); } else { @@ -141,13 +146,13 @@ class AstroBuilder { origin: this.origin, pageNames, routeCache: this.routeCache, - viteConfig: this.viteConfig, - viteServer: this.viteServer, + viteConfig, + viteServer, }); } // Write any additionally generated assets to disk. - timer.assetsStart = performance.now(); + this.timer.assetsStart = performance.now(); Object.keys(assets).map((k) => { if (!assets[k]) return; const filePath = new URL(`file://${k}`); @@ -155,11 +160,11 @@ class AstroBuilder { fs.writeFileSync(filePath, assets[k], 'utf8'); delete assets[k]; // free up memory }); - debug('build', timerMessage('Additional assets copied', timer.assetsStart)); + debug('build', timerMessage('Additional assets copied', this.timer.assetsStart)); // Build your final sitemap. if (this.config.buildOptions.sitemap && this.config.buildOptions.site) { - timer.sitemapStart = performance.now(); + this.timer.sitemapStart = performance.now(); const sitemapFilter = this.config.buildOptions.sitemapFilter ? (this.config.buildOptions.sitemapFilter as (page: string) => boolean) : undefined; const sitemap = generateSitemap( pageNames.map((pageName) => new URL(pageName, this.config.buildOptions.site).href), @@ -168,16 +173,27 @@ class AstroBuilder { const sitemapPath = new URL('./sitemap.xml', this.config.dist); await fs.promises.mkdir(new URL('./', sitemapPath), { recursive: true }); await fs.promises.writeFile(sitemapPath, sitemap, 'utf8'); - debug('build', timerMessage('Sitemap built', timer.sitemapStart)); + debug('build', timerMessage('Sitemap built', this.timer.sitemapStart)); } // You're done! Time to clean up. await viteServer.close(); await runHookBuildDone({ config: this.config, pages: pageNames, routes: Object.values(allPages).map((pd) => pd.route) }); - if (logging.level && levels[logging.level] <= levels['info']) { + if (this.logging.level && levels[this.logging.level] <= levels['info']) { const buildMode = this.config.buildOptions.experimentalSsr ? 'ssr' : 'static'; - await this.printStats({ logging, timeStart: timer.init, pageCount: pageNames.length, buildMode }); + await this.printStats({ logging: this.logging, timeStart: this.timer.init, pageCount: pageNames.length, buildMode }); + } + } + + /** Build the given Astro project. */ + async run() { + const setupData = await this.setup(); + try { + await this.build(setupData); + } catch (_err) { + debugger; + throw fixViteErrorMessage(createSafeError(_err), setupData.viteServer); } } @@ -188,14 +204,12 @@ class AstroBuilder { let messages: string[] = []; if (buildMode === 'static') { - const timePerPage = Math.round(buildTime / pageCount); - const perPageMsg = colors.dim(`(${colors.bold(`${timePerPage}ms`)} avg per page + resources)`); - messages = [`${pageCount} pages built in`, colors.bold(total), perPageMsg]; + messages = [`${pageCount} page(s) built in`, colors.bold(total)]; } else { messages = ['Server built in', colors.bold(total)]; } info(logging, 'build', messages.join(' ')); - info(logging, 'build', `🚀 ${colors.cyan(colors.bold('Done'))}`); + info(logging, 'build', `${colors.bold('Complete!')}`); } } diff --git a/packages/astro/src/core/build/static-build.ts b/packages/astro/src/core/build/static-build.ts index f5e54b7bf9..8e1559e976 100644 --- a/packages/astro/src/core/build/static-build.ts +++ b/packages/astro/src/core/build/static-build.ts @@ -41,7 +41,6 @@ export async function staticBuild(opts: StaticBuildOptions) { const timer: Record = {}; timer.buildStart = performance.now(); - info(opts.logging, 'build', 'Discovering entrypoints...'); for (const [component, pageData] of Object.entries(allPages)) { const astroModuleURL = new URL('./' + component, astroConfig.projectRoot); @@ -97,7 +96,7 @@ export async function staticBuild(opts: StaticBuildOptions) { timer.ssr = performance.now(); info(opts.logging, 'build', 'Building for SSR...'); const ssrResult = (await ssrBuild(opts, internals, pageInput)) as RollupOutput; - info(opts.logging, 'build', dim(`Completed in ${getTimeStat(timer.ssr, performance.now())}`)); + info(opts.logging, 'build', dim(`Completed in ${getTimeStat(timer.ssr, performance.now())}.`)); timer.generate = performance.now(); if (opts.buildConfig.staticMode) { @@ -107,7 +106,6 @@ export async function staticBuild(opts: StaticBuildOptions) { info(opts.logging, null, `\n${bgMagenta(black(' finalizing server assets '))}\n`); await ssrMoveAssets(opts); } - info(opts.logging, null, dim(`Completed in ${getTimeStat(timer.generate, performance.now())}\n`)); } async function ssrBuild(opts: StaticBuildOptions, internals: BuildInternals, input: Set) { @@ -171,7 +169,7 @@ async function clientBuild(opts: StaticBuildOptions, internals: BuildInternals, } // TODO: use vite.mergeConfig() here? - info(opts.logging, null, `\n${bgGreen(black(' building resources '))}\n`); + info(opts.logging, null, `\n${bgGreen(black(' building client '))}`); const out = isBuildingToSSR(astroConfig) ? opts.buildConfig.client : astroConfig.dist; @@ -210,7 +208,7 @@ async function clientBuild(opts: StaticBuildOptions, internals: BuildInternals, server: viteConfig.server, base: appendForwardSlash(astroConfig.buildOptions.site ? new URL(astroConfig.buildOptions.site).pathname : '/'), }); - info(opts.logging, null, dim(`Completed in ${getTimeStat(timer, performance.now())}\n`)); + info(opts.logging, null, dim(`Completed in ${getTimeStat(timer, performance.now())}.\n`)); return buildResult; } diff --git a/packages/astro/src/core/config.ts b/packages/astro/src/core/config.ts index 39fa44174b..394c1c42db 100644 --- a/packages/astro/src/core/config.ts +++ b/packages/astro/src/core/config.ts @@ -327,11 +327,6 @@ export async function resolveConfig(userConfig: AstroUserConfig, root: string, f return validatedConfig; } -export function formatConfigError(err: z.ZodError) { - const errorList = err.issues.map((issue) => ` ! ${colors.bold(issue.path.join('.'))} ${colors.red(issue.message + '.')}`); - return `${colors.red('[config]')} Astro found issue(s) with your configuration:\n${errorList.join('\n')}`; -} - function mergeConfigRecursively(defaults: Record, overrides: Record, rootPath: string) { const merged: Record = { ...defaults }; for (const key in overrides) { diff --git a/packages/astro/src/core/dev/index.ts b/packages/astro/src/core/dev/index.ts index 166009cda2..cc1e7bde2f 100644 --- a/packages/astro/src/core/dev/index.ts +++ b/packages/astro/src/core/dev/index.ts @@ -7,7 +7,7 @@ import { createVite } from '../create-vite.js'; import { defaultLogOptions, info, LogOptions, warn, warnIfUsingExperimentalSSR } from '../logger.js'; import * as msg from '../messages.js'; import { apply as applyPolyfill } from '../polyfill.js'; -import { getResolvedHostForVite } from './util.js'; +import { getResolvedHostForVite } from '../util.js'; export interface DevOptions { logging: LogOptions; diff --git a/packages/astro/src/core/dev/util.ts b/packages/astro/src/core/dev/util.ts deleted file mode 100644 index 9b0c974fde..0000000000 --- a/packages/astro/src/core/dev/util.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { AstroConfig } from '../../@types/astro'; - -export const localIps = new Set(['localhost', '127.0.0.1']); - -/** Pad string () */ -export function pad(input: string, minLength: number, dir?: 'left' | 'right'): string { - let output = input; - while (output.length < minLength) { - output = dir === 'left' ? ' ' + output : output + ' '; - } - return output; -} - -export function emoji(char: string, fallback: string) { - return process.platform !== 'win32' ? char : fallback; -} - -// TODO: remove once --hostname is baselined -export function getResolvedHostForVite(config: AstroConfig) { - if (config.devOptions.host === false && config.devOptions.hostname !== 'localhost') { - return config.devOptions.hostname; - } else { - return config.devOptions.host; - } -} - -export function getLocalAddress(serverAddress: string, config: AstroConfig): string { - // TODO: remove once --hostname is baselined - const host = getResolvedHostForVite(config); - if (typeof host === 'boolean' || host === 'localhost') { - return 'localhost'; - } else { - return serverAddress; - } -} - -export type NetworkLogging = 'none' | 'host-to-expose' | 'visible'; - -export function getNetworkLogging(config: AstroConfig): NetworkLogging { - // TODO: remove once --hostname is baselined - const host = getResolvedHostForVite(config); - - if (host === false) { - return 'host-to-expose'; - } else if (typeof host === 'string' && localIps.has(host)) { - return 'none'; - } else { - return 'visible'; - } -} diff --git a/packages/astro/src/core/endpoint/dev/index.ts b/packages/astro/src/core/endpoint/dev/index.ts index 3190a500f4..da3671bc04 100644 --- a/packages/astro/src/core/endpoint/dev/index.ts +++ b/packages/astro/src/core/endpoint/dev/index.ts @@ -1,21 +1,12 @@ import type { EndpointHandler } from '../../../@types/astro'; import type { SSROptions } from '../../render/dev'; - import { preload } from '../../render/dev/index.js'; -import { errorHandler } from '../../render/dev/error.js'; import { call as callEndpoint } from '../index.js'; -import { getParamsAndProps, GetParamsAndPropsError } from '../../render/core.js'; -import { createRequest } from '../../render/request.js'; export async function call(ssrOpts: SSROptions) { - try { - const [, mod] = await preload(ssrOpts); - return await callEndpoint(mod as unknown as EndpointHandler, { - ...ssrOpts, - ssr: ssrOpts.astroConfig.buildOptions.experimentalSsr, - }); - } catch (e: unknown) { - await errorHandler(e, { viteServer: ssrOpts.viteServer, filePath: ssrOpts.filePath }); - throw e; - } + const [, mod] = await preload(ssrOpts); + return await callEndpoint(mod as unknown as EndpointHandler, { + ...ssrOpts, + ssr: ssrOpts.astroConfig.buildOptions.experimentalSsr, + }); } diff --git a/packages/astro/src/core/errors.ts b/packages/astro/src/core/errors.ts new file mode 100644 index 0000000000..0150978dcb --- /dev/null +++ b/packages/astro/src/core/errors.ts @@ -0,0 +1,78 @@ +import type { BuildResult } from 'esbuild'; +import type { ViteDevServer } from 'vite'; +import type { SSRError } from '../@types/astro'; +import eol from 'eol'; +import fs from 'fs'; +import { codeFrame, createSafeError } from './util.js'; + +export interface ErrorWithMetadata { + [name: string]: any; + message: string; + stack: string; + id?: string; + frame?: string; + plugin?: string; + pluginCode?: string; + loc?: { + file?: string; + line: number; + column: number; + }; +} + +export function cleanErrorStack(stack: string) { + return stack + .split(/\n/g) + .filter((l) => /^\s*at/.test(l)) + .join('\n'); +} + +/** Update the error message to correct any vite-isms that we don't want to expose to the user. */ +export function fixViteErrorMessage(_err: unknown, server: ViteDevServer) { + const err = createSafeError(_err); + // Vite will give you better stacktraces, using sourcemaps. + server.ssrFixStacktrace(err); + // Fix: Astro.glob() compiles to import.meta.glob() by the time Vite sees it, + // so we need to update this error message in case it originally came from Astro.glob(). + if (err.message === 'import.meta.glob() can only accept string literals.') { + err.message = 'Astro.glob() and import.meta.glob() can only accept string literals.'; + } + return err; +} + +/** + * Takes any error-like object and returns a standardized Error + metadata object. + * Useful for consistent reporting regardless of where the error surfaced from. + */ +export function collectErrorMetadata(e: any): ErrorWithMetadata { + // normalize error stack line-endings to \n + if ((e as any).stack) { + (e as any).stack = eol.lf((e as any).stack); + } + + // Astro error (thrown by esbuild so it needs to be formatted for Vite) + if (Array.isArray((e as any).errors)) { + const { location, pluginName, text } = (e as BuildResult).errors[0]; + const err = e as SSRError; + if (location) { + err.loc = { file: location.file, line: location.line, column: location.column }; + err.id = err.id || location?.file; + } + const possibleFilePath = err.pluginCode || err.id || location?.file; + if (possibleFilePath && !err.frame) { + try { + const fileContents = fs.readFileSync(possibleFilePath, 'utf8'); + err.frame = codeFrame(fileContents, err.loc); + } catch { + // do nothing, code frame isn't that big a deal + } + } + if (pluginName) { + err.plugin = pluginName; + } + return err; + } + + // Generic error (probably from Vite, and already formatted) + return e; +} diff --git a/packages/astro/src/core/messages.ts b/packages/astro/src/core/messages.ts index e3a7741c44..888e6fcf4c 100644 --- a/packages/astro/src/core/messages.ts +++ b/packages/astro/src/core/messages.ts @@ -2,12 +2,13 @@ * Dev server messages (organized here to prevent clutter) */ -import stripAnsi from 'strip-ansi'; import { bold, dim, red, green, underline, yellow, bgYellow, cyan, bgGreen, black, bgRed, bgWhite } from 'kleur/colors'; -import { pad, emoji, getLocalAddress, getNetworkLogging } from './dev/util.js'; import os from 'os'; import type { AddressInfo } from 'net'; import type { AstroConfig } from '../@types/astro'; +import { collectErrorMetadata, cleanErrorStack } from './errors.js'; +import { ZodError } from 'zod'; +import { emoji, getLocalAddress, getResolvedHostForVite, padMultilineString } from './util.js'; const PREFIX_PADDING = 6; @@ -18,15 +19,15 @@ export function req({ url, statusCode, reqTime }: { url: string; statusCode: num else if (statusCode >= 400) color = yellow; else if (statusCode >= 300) color = dim; else if (statusCode >= 200) color = green; - return `${bold(color(pad(`${statusCode}`, PREFIX_PADDING)))} ${pad(url, 40)} ${reqTime ? dim(Math.round(reqTime) + 'ms') : ''}`.trim(); + return `${bold(color(`${statusCode}`.padStart(PREFIX_PADDING)))} ${url.padStart(40)} ${reqTime ? dim(Math.round(reqTime) + 'ms') : ''}`.trim(); } export function reload({ file }: { file: string }): string { - return `${green(pad('reload', PREFIX_PADDING))} ${file}`; + return `${green('reload'.padStart(PREFIX_PADDING))} ${file}`; } export function hmr({ file }: { file: string }): string { - return `${green(pad('update', PREFIX_PADDING))} ${file}`; + return `${green('update'.padStart(PREFIX_PADDING))} ${file}`; } /** Display dev server host and startup time */ @@ -91,7 +92,7 @@ export function success(message: string, tip?: string) { const badge = bgGreen(black(` success `)); const headline = green(message); const footer = tip ? `\n ▶ ${tip}` : undefined; - return ['', badge, headline, footer] + return ['', `${badge} ${headline}`, footer] .filter((v) => v !== undefined) .map((msg) => ` ${msg}`) .join('\n'); @@ -101,7 +102,7 @@ export function failure(message: string, tip?: string) { const badge = bgRed(black(` error `)); const headline = red(message); const footer = tip ? `\n ▶ ${tip}` : undefined; - return ['', badge, headline, footer] + return ['', `${badge} ${headline}`, footer] .filter((v) => v !== undefined) .map((msg) => ` ${msg}`) .join('\n'); @@ -111,7 +112,7 @@ export function cancelled(message: string, tip?: string) { const badge = bgYellow(black(` cancelled `)); const headline = yellow(message); const footer = tip ? `\n ▶ ${tip}` : undefined; - return ['', badge, headline, footer] + return ['', `${badge} ${headline}`, footer] .filter((v) => v !== undefined) .map((msg) => ` ${msg}`) .join('\n'); @@ -122,15 +123,45 @@ export function portInUse({ port }: { port: number }): string { return `Port ${port} in use. Trying a new one…`; } -/** Pretty-print errors */ -export function err(error: Error): string { - if (!error.stack) return stripAnsi(error.message); - let message = stripAnsi(error.message); - let stack = stripAnsi(error.stack); - const split = stack.indexOf(message) + message.length; - message = stack.slice(0, split); - stack = stack.slice(split).replace(/^\n+/, ''); - return `${message}\n${dim(stack)}`; +const LOCAL_IP_HOSTS = new Set(['localhost', '127.0.0.1']); + +export function getNetworkLogging(config: AstroConfig): 'none' | 'host-to-expose' | 'visible' { + // TODO: remove once --hostname is baselined + const host = getResolvedHostForVite(config); + + if (host === false) { + return 'host-to-expose'; + } else if (typeof host === 'string' && LOCAL_IP_HOSTS.has(host)) { + return 'none'; + } else { + return 'visible'; + } +} + +export function formatConfigErrorMessage(err: ZodError) { + const errorList = err.issues.map((issue) => ` ! ${bold(issue.path.join('.'))} ${red(issue.message + '.')}`); + return `${red('[config]')} Astro found issue(s) with your configuration:\n${errorList.join('\n')}`; +} + +export function formatErrorMessage(_err: Error, args: string[] = []): string { + const err = collectErrorMetadata(_err); + args.push(`${bgRed(black(` error `))}${red(bold(padMultilineString(err.message)))}`); + if (err.id) { + args.push(` ${bold('File:')}`); + args.push(red(` ${err.id}`)); + } + if (err.frame) { + args.push(` ${bold('Code:')}`); + args.push(red(padMultilineString(err.frame, 4))); + } + if (args.length === 1 && err.stack) { + args.push(dim(cleanErrorStack(err.stack))); + } else if (err.stack) { + args.push(` ${bold('Stacktrace:')}`); + args.push(dim(cleanErrorStack(err.stack))); + args.push(``); + } + return args.join('\n'); } export function printHelp({ @@ -157,7 +188,7 @@ export function printHelp({ let raw = ''; for (const row of rows) { - raw += `${opts.prefix}${bold(pad(`${row[0]}`, opts.padding - opts.prefix.length))}`; + raw += `${opts.prefix}${bold(`${row[0]}`.padStart(opts.padding - opts.prefix.length))}`; if (split) raw += '\n '; raw += dim(row[1]) + '\n'; } diff --git a/packages/astro/src/core/render/dev/error.ts b/packages/astro/src/core/render/dev/error.ts deleted file mode 100644 index 352211b1f7..0000000000 --- a/packages/astro/src/core/render/dev/error.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { BuildResult } from 'esbuild'; -import type * as vite from 'vite'; -import type { SSRError } from '../../../@types/astro'; - -import eol from 'eol'; -import fs from 'fs'; -import { codeFrame } from '../../util.js'; - -interface ErrorHandlerOptions { - filePath: URL; - viteServer: vite.ViteDevServer; -} - -export async function errorHandler(e: unknown, { viteServer, filePath }: ErrorHandlerOptions) { - // normalize error stack line-endings to \n - if ((e as any).stack) { - (e as any).stack = eol.lf((e as any).stack); - } - - // fix stack trace with Vite (this searches its module graph for matches) - if (e instanceof Error) { - viteServer.ssrFixStacktrace(e); - } - - // Astro error (thrown by esbuild so it needs to be formatted for Vite) - if (Array.isArray((e as any).errors)) { - const { location, pluginName, text } = (e as BuildResult).errors[0]; - const err = e as SSRError; - if (location) err.loc = { file: location.file, line: location.line, column: location.column }; - let src = err.pluginCode; - if (!src && err.id && fs.existsSync(err.id)) src = await fs.promises.readFile(err.id, 'utf8'); - if (!src) src = await fs.promises.readFile(filePath, 'utf8'); - err.frame = codeFrame(src, err.loc); - err.id = location?.file; - err.message = `${location?.file}: ${text} -${err.frame} -`; - if (pluginName) err.plugin = pluginName; - throw err; - } - - // Generic error (probably from Vite, and already formatted) - throw e; -} diff --git a/packages/astro/src/core/render/dev/index.ts b/packages/astro/src/core/render/dev/index.ts index d16cb8923a..af92aca857 100644 --- a/packages/astro/src/core/render/dev/index.ts +++ b/packages/astro/src/core/render/dev/index.ts @@ -7,7 +7,6 @@ import { prependForwardSlash } from '../../../core/path.js'; import { RouteCache } from '../route-cache.js'; import { createModuleScriptElementWithSrcSet } from '../ssr-element.js'; import { getStylesForURL } from './css.js'; -import { errorHandler } from './error.js'; import { getHmrScript } from './hmr.js'; import { injectTags } from './html.js'; export interface SSROptions { @@ -216,11 +215,6 @@ export async function render(renderers: SSRLoadedRenderer[], mod: ComponentInsta } export async function ssr(preloadedComponent: ComponentPreload, ssrOpts: SSROptions): Promise { - try { - const [renderers, mod] = preloadedComponent; - return await render(renderers, mod, ssrOpts); // note(drew): without "await", errors won’t get caught by errorHandler() - } catch (e: unknown) { - await errorHandler(e, { viteServer: ssrOpts.viteServer, filePath: ssrOpts.filePath }); - throw e; - } + const [renderers, mod] = preloadedComponent; + return await render(renderers, mod, ssrOpts); // NOTE: without "await", errors won’t get caught below } diff --git a/packages/astro/src/core/util.ts b/packages/astro/src/core/util.ts index 6787f177bc..373d6cff11 100644 --- a/packages/astro/src/core/util.ts +++ b/packages/astro/src/core/util.ts @@ -1,11 +1,12 @@ -import type { AstroConfig } from '../@types/astro'; -import type { ErrorPayload } from 'vite'; import eol from 'eol'; -import path from 'path'; -import slash from 'slash'; import fs from 'fs'; -import { fileURLToPath, pathToFileURL } from 'url'; +import path from 'path'; import resolve from 'resolve'; +import slash from 'slash'; +import { fileURLToPath, pathToFileURL } from 'url'; +import type { ErrorPayload } from 'vite'; +import type { AstroConfig } from '../@types/astro'; +import { removeEndingForwardSlash } from './path.js'; /** Normalize URL to its canonical form */ export function canonicalURL(url: string, base?: string): URL { @@ -35,6 +36,28 @@ export function arraify(target: T | T[]): T[] { return Array.isArray(target) ? target : [target]; } +export function padMultilineString(source: string, n = 2) { + const lines = source.split(/\r?\n/); + return lines.map((l) => ` `.repeat(n) + l).join(`\n`); +} + +const STATUS_CODE_REGEXP = /^\/?[0-9]{3}$/; + +/** + * Get the correct output filename for a route, based on your config. + * Handles both "/foo" and "foo" `name` formats. + * Handles `/404` and `/` correctly. + */ +export function getOutputFilename(astroConfig: AstroConfig, name: string) { + if (name === '/' || name === '') { + return path.posix.join(name, 'index.html'); + } + if (astroConfig.buildOptions.pageUrlFormat === 'directory' && !STATUS_CODE_REGEXP.test(name)) { + return path.posix.join(name, 'index.html'); + } + return `${removeEndingForwardSlash(name || 'index')}.html`; +} + /** is a specifier an npm package? */ export function parseNpmName(spec: string): { scope?: string; name: string; subpath?: string } | undefined { // not an npm package @@ -137,6 +160,28 @@ export function isBuildingToSSR(config: AstroConfig): boolean { return !!config._ctx.adapter?.serverEntrypoint; } +export function emoji(char: string, fallback: string) { + return process.platform !== 'win32' ? char : fallback; +} + +// TODO: remove once --hostname is baselined +export function getResolvedHostForVite(config: AstroConfig) { + if (config.devOptions.host === false && config.devOptions.hostname !== 'localhost') { + return config.devOptions.hostname; + } else { + return config.devOptions.host; + } +} + +export function getLocalAddress(serverAddress: string, config: AstroConfig): string { + const host = getResolvedHostForVite(config); + if (typeof host === 'boolean' || host === 'localhost') { + return 'localhost'; + } else { + return serverAddress; + } +} + // Vendored from https://github.com/genmon/aboutfeeds/blob/main/tools/pretty-feed-v3.xsl /** Basic stylesheet for RSS feeds */ export const PRETTY_FEED_V3 = ` diff --git a/packages/astro/src/vite-plugin-astro-server/index.ts b/packages/astro/src/vite-plugin-astro-server/index.ts index bc45596dae..5b4ae41b86 100644 --- a/packages/astro/src/vite-plugin-astro-server/index.ts +++ b/packages/astro/src/vite-plugin-astro-server/index.ts @@ -2,7 +2,7 @@ import type * as vite from 'vite'; import type http from 'http'; import type { AstroConfig, ManifestData } from '../@types/astro'; import type { RenderResponse, SSROptions } from '../core/render/dev/index'; -import { info, warn, error, LogOptions } from '../core/logger.js'; +import { debug, info, warn, error, LogOptions } from '../core/logger.js'; import { getParamsAndProps, GetParamsAndPropsError } from '../core/render/core.js'; import { createRouteManifest, matchRoute } from '../core/routing/index.js'; import stripAnsi from 'strip-ansi'; @@ -10,11 +10,10 @@ import { createSafeError } from '../core/util.js'; import { ssr, preload } from '../core/render/dev/index.js'; import { call as callEndpoint } from '../core/endpoint/dev/index.js'; import * as msg from '../core/messages.js'; - import notFoundTemplate, { subpathNotUsedTemplate } from '../template/4xx.js'; import serverErrorTemplate from '../template/5xx.js'; import { RouteCache } from '../core/render/route-cache.js'; -import { AstroRequest } from '../core/render/request.js'; +import { fixViteErrorMessage } from '../core/errors.js'; interface AstroPluginOptions { config: AstroConfig; @@ -207,11 +206,10 @@ async function handleRequest( const result = await ssr(preloadedComponent, options); return await writeSSRResult(result, res, statusCode); } - } catch (_err: any) { + } catch (_err) { debugger; - info(logging, 'serve', msg.req({ url: pathname, statusCode: 500 })); - const err = createSafeError(_err); - error(logging, 'error', msg.err(err)); + const err = fixViteErrorMessage(createSafeError(_err), viteServer); + error(logging, null, msg.formatErrorMessage(err)); handle500Response(viteServer, origin, req, res, err); } } diff --git a/packages/astro/src/vite-plugin-build-html/index.ts b/packages/astro/src/vite-plugin-build-html/index.ts index 1dc6e3392f..80dd480a1d 100644 --- a/packages/astro/src/vite-plugin-build-html/index.ts +++ b/packages/astro/src/vite-plugin-build-html/index.ts @@ -1,21 +1,22 @@ -import type { AstroConfig } from '../@types/astro'; -import type { LogOptions } from '../core/logger.js'; -import type { ViteDevServer, Plugin as VitePlugin } from 'vite'; -import type { OutputChunk, PreRenderedChunk, PluginContext } from 'rollup'; -import type { AllPagesData } from '../core/build/types'; -import type { BuildInternals } from '../core/build/internal'; -import parse5 from 'parse5'; -import srcsetParse from 'srcset-parse'; -import * as npath from 'path'; +import { createElement, createScript, getAttribute, hasAttribute, insertBefore, remove, setAttribute } from '@web/parse5-utils'; import { promises as fs } from 'fs'; -import { getAttribute, hasAttribute, insertBefore, remove, createScript, createElement, setAttribute } from '@web/parse5-utils'; -import { addRollupInput } from './add-rollup-input.js'; -import { findAssets, findExternalScripts, findInlineScripts, findInlineStyles, getTextContent, getAttributes } from './extract-assets.js'; -import { isBuildableImage, isBuildableLink, isHoistedScript, isInSrcDirectory, hasSrcSet } from './util.js'; +import parse5 from 'parse5'; +import * as npath from 'path'; +import type { OutputChunk, PluginContext, PreRenderedChunk } from 'rollup'; +import srcsetParse from 'srcset-parse'; +import type { Plugin as VitePlugin, ViteDevServer } from 'vite'; +import type { AstroConfig } from '../@types/astro'; +import type { BuildInternals } from '../core/build/internal'; +import type { AllPagesData } from '../core/build/types'; +import type { LogOptions } from '../core/logger.js'; +import { prependDotSlash } from '../core/path.js'; import { render as ssrRender } from '../core/render/dev/index.js'; -import { getAstroStyleId, getAstroPageStyleId } from '../vite-plugin-build-css/index.js'; -import { prependDotSlash, removeEndingForwardSlash } from '../core/path.js'; import { RouteCache } from '../core/render/route-cache.js'; +import { getOutputFilename } from '../core/util.js'; +import { getAstroPageStyleId, getAstroStyleId } from '../vite-plugin-build-css/index.js'; +import { addRollupInput } from './add-rollup-input.js'; +import { findAssets, findExternalScripts, findInlineScripts, findInlineStyles, getAttributes, getTextContent } from './extract-assets.js'; +import { hasSrcSet, isBuildableImage, isBuildableLink, isHoistedScript, isInSrcDirectory } from './util.js'; // This package isn't real ESM, so have to coerce it const matchSrcset: typeof srcsetParse = (srcsetParse as any).default; @@ -25,7 +26,6 @@ const ASTRO_PAGE_PREFIX = '@astro-page'; const ASTRO_SCRIPT_PREFIX = '@astro-script'; const ASTRO_EMPTY = '@astro-empty'; -const STATUS_CODE_REGEXP = /^[0-9]{3}$/; interface PluginOptions { astroConfig: AstroConfig; @@ -487,14 +487,7 @@ export function rollupPluginAstroScanHTML(options: PluginOptions): VitePlugin { const outHTML = parse5.serialize(document); const name = pathname.substr(1); - let outPath: string; - - // Output directly to 404.html rather than 404/index.html - if (astroConfig.buildOptions.pageUrlFormat === 'file' || STATUS_CODE_REGEXP.test(name)) { - outPath = `${removeEndingForwardSlash(name || 'index')}.html`; - } else { - outPath = npath.posix.join(name, 'index.html'); - } + const outPath = getOutputFilename(astroConfig, name); this.emitFile({ fileName: outPath, diff --git a/packages/astro/test/cli.test.js b/packages/astro/test/cli.test.js index d84ca56e4f..c23fca34f1 100644 --- a/packages/astro/test/cli.test.js +++ b/packages/astro/test/cli.test.js @@ -28,7 +28,7 @@ describe('astro cli', () => { it('astro build', async () => { const projectRootURL = new URL('./fixtures/astro-basic/', import.meta.url); const proc = await cli('build', '--project-root', fileURLToPath(projectRootURL)); - expect(proc.stdout).to.include('Done'); + expect(proc.stdout).to.include('Complete'); }); it('astro dev welcome', async () => { diff --git a/packages/astro/test/config-validate.test.js b/packages/astro/test/config-validate.test.js index b1079b6f49..c543a013cf 100644 --- a/packages/astro/test/config-validate.test.js +++ b/packages/astro/test/config-validate.test.js @@ -1,7 +1,8 @@ import { expect } from 'chai'; import { z } from 'zod'; import stripAnsi from 'strip-ansi'; -import { formatConfigError, validateConfig } from '../dist/core/config.js'; +import { formatConfigErrorMessage } from '../dist/core/messages.js'; +import { validateConfig } from '../dist/core/config.js'; describe('Config Validation', () => { it('empty user config is valid', async () => { @@ -22,7 +23,7 @@ describe('Config Validation', () => { it('A validation error can be formatted correctly', async () => { const configError = await validateConfig({ buildOptions: { sitemap: 42 } }, process.cwd()).catch((err) => err); expect(configError instanceof z.ZodError).to.equal(true); - const formattedError = stripAnsi(formatConfigError(configError)); + const formattedError = stripAnsi(formatConfigErrorMessage(configError)); expect(formattedError).to.equal( `[config] Astro found issue(s) with your configuration: ! buildOptions.sitemap Expected boolean, received number.` @@ -37,7 +38,7 @@ describe('Config Validation', () => { }; const configError = await validateConfig(veryBadConfig, process.cwd()).catch((err) => err); expect(configError instanceof z.ZodError).to.equal(true); - const formattedError = stripAnsi(formatConfigError(configError)); + const formattedError = stripAnsi(formatConfigErrorMessage(configError)); expect(formattedError).to.equal( `[config] Astro found issue(s) with your configuration: ! pages Expected string, received object.