Refactor error handling (#5206)
* Refactor error handling
* Fix import path in test
* Revert "Fix import path in test"
This reverts commit 5ca34f3c09
.
* Fix import path in test
* Fix AggregateErrors actually not being.. an aggregration of errors
* Fix missing info in Vite enhanced error
* Conserve original error name if we have one
* Workaround compiler issue
* GitHub action please
* Update E2E test
* Wrap ssrFixStacktrace in try/catch
* Refactor Vite/Node methods out of the general index.ts
* Fix missing import
* Add changeset
This commit is contained in:
parent
3c1af36f5e
commit
d64d5b9b52
26 changed files with 744 additions and 307 deletions
5
.changeset/ten-cheetahs-perform.md
Normal file
5
.changeset/ten-cheetahs-perform.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'astro': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Improve error messages related to CSS and compiler errors
|
|
@ -38,7 +38,7 @@ test.describe('Error display', () => {
|
||||||
await page.goto(astro.resolveUrl('/import-not-found'));
|
await page.goto(astro.resolveUrl('/import-not-found'));
|
||||||
|
|
||||||
const message = await getErrorOverlayMessage(page);
|
const message = await getErrorOverlayMessage(page);
|
||||||
expect(message).toMatch('failed to load module for ssr: ../abc.astro');
|
expect(message).toMatch('Could not import "../abc.astro"');
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
// Wait for page reload
|
// Wait for page reload
|
||||||
|
|
|
@ -16,7 +16,7 @@ import {
|
||||||
} from '../core/config/index.js';
|
} from '../core/config/index.js';
|
||||||
import { ASTRO_VERSION } from '../core/constants.js';
|
import { ASTRO_VERSION } from '../core/constants.js';
|
||||||
import devServer from '../core/dev/index.js';
|
import devServer from '../core/dev/index.js';
|
||||||
import { collectErrorMetadata } from '../core/errors.js';
|
import { collectErrorMetadata } from '../core/errors/dev/index.js';
|
||||||
import { debug, error, info, LogOptions } from '../core/logger/core.js';
|
import { debug, error, info, LogOptions } from '../core/logger/core.js';
|
||||||
import { enableVerboseLogging, nodeLogDestination } from '../core/logger/node.js';
|
import { enableVerboseLogging, nodeLogDestination } from '../core/logger/node.js';
|
||||||
import { formatConfigErrorMessage, formatErrorMessage, printHelp } from '../core/messages.js';
|
import { formatConfigErrorMessage, formatErrorMessage, printHelp } from '../core/messages.js';
|
||||||
|
|
|
@ -13,7 +13,7 @@ import {
|
||||||
runHookConfigSetup,
|
runHookConfigSetup,
|
||||||
} from '../../integrations/index.js';
|
} from '../../integrations/index.js';
|
||||||
import { createVite } from '../create-vite.js';
|
import { createVite } from '../create-vite.js';
|
||||||
import { fixViteErrorMessage } from '../errors.js';
|
import { enhanceViteSSRError } from '../errors/dev/index.js';
|
||||||
import { debug, info, levels, timerMessage } from '../logger/core.js';
|
import { debug, info, levels, timerMessage } from '../logger/core.js';
|
||||||
import { apply as applyPolyfill } from '../polyfill.js';
|
import { apply as applyPolyfill } from '../polyfill.js';
|
||||||
import { RouteCache } from '../render/route-cache.js';
|
import { RouteCache } from '../render/route-cache.js';
|
||||||
|
@ -169,7 +169,7 @@ class AstroBuilder {
|
||||||
try {
|
try {
|
||||||
await this.build(setupData);
|
await this.build(setupData);
|
||||||
} catch (_err) {
|
} catch (_err) {
|
||||||
throw fixViteErrorMessage(_err);
|
throw enhanceViteSSRError(_err as Error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,9 +3,10 @@ import type { AstroConfig } from '../../@types/astro';
|
||||||
import type { TransformStyle } from './types';
|
import type { TransformStyle } from './types';
|
||||||
|
|
||||||
import { transform } from '@astrojs/compiler';
|
import { transform } from '@astrojs/compiler';
|
||||||
import { AstroErrorCodes } from '../errors.js';
|
import { AstroErrorCodes } from '../errors/codes.js';
|
||||||
|
import { AggregateError, AstroError, CompilerError } from '../errors/errors.js';
|
||||||
import { prependForwardSlash } from '../path.js';
|
import { prependForwardSlash } from '../path.js';
|
||||||
import { AggregateError, resolvePath, viteID } from '../util.js';
|
import { resolvePath, viteID } from '../util.js';
|
||||||
import { createStylePreprocessor } from './style.js';
|
import { createStylePreprocessor } from './style.js';
|
||||||
|
|
||||||
type CompilationCache = Map<string, CompileResult>;
|
type CompilationCache = Map<string, CompileResult>;
|
||||||
|
@ -30,7 +31,7 @@ async function compile({
|
||||||
transformStyle,
|
transformStyle,
|
||||||
}: CompileProps): Promise<CompileResult> {
|
}: CompileProps): Promise<CompileResult> {
|
||||||
let cssDeps = new Set<string>();
|
let cssDeps = new Set<string>();
|
||||||
let cssTransformErrors: Error[] = [];
|
let cssTransformErrors: AstroError[] = [];
|
||||||
|
|
||||||
// Transform from `.astro` to valid `.ts`
|
// Transform from `.astro` to valid `.ts`
|
||||||
// use `sourcemap: "both"` so that sourcemap is included in the code
|
// use `sourcemap: "both"` so that sourcemap is included in the code
|
||||||
|
@ -51,26 +52,51 @@ async function compile({
|
||||||
return resolvePath(specifier, filename);
|
return resolvePath(specifier, filename);
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err: Error) => {
|
||||||
// throw compiler errors here if encountered
|
// The compiler should be able to handle errors by itself, however
|
||||||
err.code = err.code || AstroErrorCodes.UnknownCompilerError;
|
// for the rare cases where it can't let's directly throw here with as much info as possible
|
||||||
throw err;
|
throw new CompilerError({
|
||||||
|
errorCode: AstroErrorCodes.UnknownCompilerError,
|
||||||
|
message: err.message ?? 'Unknown compiler error',
|
||||||
|
stack: err.stack,
|
||||||
|
location: {
|
||||||
|
file: filename,
|
||||||
|
},
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
|
const compilerError = result.diagnostics.find(
|
||||||
|
// HACK: The compiler currently mistakenly returns the wrong severity for warnings, so we'll also filter by code
|
||||||
|
// https://github.com/withastro/compiler/issues/595
|
||||||
|
(diag) => diag.severity === 1 && diag.code < 2000
|
||||||
|
);
|
||||||
|
|
||||||
|
if (compilerError) {
|
||||||
|
throw new CompilerError({
|
||||||
|
errorCode: compilerError.code,
|
||||||
|
message: compilerError.text,
|
||||||
|
location: {
|
||||||
|
line: compilerError.location.line,
|
||||||
|
column: compilerError.location.column,
|
||||||
|
file: compilerError.location.file,
|
||||||
|
},
|
||||||
|
hint: compilerError.hint ? compilerError.hint : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
switch (cssTransformErrors.length) {
|
switch (cssTransformErrors.length) {
|
||||||
case 0:
|
case 0:
|
||||||
return result;
|
return result;
|
||||||
case 1: {
|
case 1: {
|
||||||
let error = cssTransformErrors[0];
|
let error = cssTransformErrors[0];
|
||||||
if (!(error as any).code) {
|
if (!error.errorCode) {
|
||||||
(error as any).code = AstroErrorCodes.UnknownCompilerCSSError;
|
error.errorCode = AstroErrorCodes.UnknownCompilerCSSError;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw cssTransformErrors[0];
|
throw cssTransformErrors[0];
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
const aggregateError = new AggregateError(cssTransformErrors);
|
throw new AggregateError({ ...cssTransformErrors[0], errors: cssTransformErrors });
|
||||||
(aggregateError as any).code = AstroErrorCodes.UnknownCompilerCSSError;
|
|
||||||
throw aggregateError;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -16,7 +16,7 @@ import legacyMarkdownVitePlugin from '../vite-plugin-markdown-legacy/index.js';
|
||||||
import markdownVitePlugin from '../vite-plugin-markdown/index.js';
|
import markdownVitePlugin from '../vite-plugin-markdown/index.js';
|
||||||
import astroScriptsPlugin from '../vite-plugin-scripts/index.js';
|
import astroScriptsPlugin from '../vite-plugin-scripts/index.js';
|
||||||
import astroScriptsPageSSRPlugin from '../vite-plugin-scripts/page-ssr.js';
|
import astroScriptsPageSSRPlugin from '../vite-plugin-scripts/page-ssr.js';
|
||||||
import { createCustomViteLogger } from './errors.js';
|
import { createCustomViteLogger } from './errors/dev/index.js';
|
||||||
import { resolveDependency } from './util.js';
|
import { resolveDependency } from './util.js';
|
||||||
|
|
||||||
interface CreateViteOptions {
|
interface CreateViteOptions {
|
||||||
|
|
|
@ -1,209 +0,0 @@
|
||||||
import type { BuildResult } from 'esbuild';
|
|
||||||
import type { ErrorPayload, Logger, LogLevel, ViteDevServer } from 'vite';
|
|
||||||
import type { SSRError } from '../@types/astro';
|
|
||||||
|
|
||||||
import eol from 'eol';
|
|
||||||
import fs from 'fs';
|
|
||||||
import { fileURLToPath } from 'node:url';
|
|
||||||
import stripAnsi from 'strip-ansi';
|
|
||||||
import { createLogger } from 'vite';
|
|
||||||
import { codeFrame, createSafeError } from './util.js';
|
|
||||||
|
|
||||||
export enum AstroErrorCodes {
|
|
||||||
// 1xxx: Astro Runtime Errors
|
|
||||||
UnknownError = 1000,
|
|
||||||
ConfigError = 1001,
|
|
||||||
// 2xxx: Astro Compiler Errors
|
|
||||||
UnknownCompilerError = 2000,
|
|
||||||
UnknownCompilerCSSError = 2001,
|
|
||||||
}
|
|
||||||
export interface ErrorWithMetadata {
|
|
||||||
[name: string]: any;
|
|
||||||
message: string;
|
|
||||||
stack: string;
|
|
||||||
code?: number;
|
|
||||||
hint?: 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)
|
|
||||||
.map((l) => l.replace(/\/@fs\//g, '/'))
|
|
||||||
.join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the error message to correct any vite-isms that we don't want to expose to the user.
|
|
||||||
* The `server` is required if the error may come from `server.ssrLoadModule()`.
|
|
||||||
*/
|
|
||||||
export function fixViteErrorMessage(_err: unknown, server?: ViteDevServer, filePath?: URL) {
|
|
||||||
const err = createSafeError(_err);
|
|
||||||
// Vite will give you better stacktraces, using sourcemaps.
|
|
||||||
try {
|
|
||||||
server?.ssrFixStacktrace(err);
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
// 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.';
|
|
||||||
}
|
|
||||||
if (filePath && /failed to load module for ssr:/.test(err.message)) {
|
|
||||||
const importName = err.message.split('for ssr:').at(1)?.trim();
|
|
||||||
if (importName) {
|
|
||||||
try {
|
|
||||||
const content = fs.readFileSync(fileURLToPath(filePath)).toString();
|
|
||||||
const lns = content.split('\n');
|
|
||||||
const line = lns.findIndex((ln) => ln.includes(importName));
|
|
||||||
if (line == -1) return err;
|
|
||||||
const column = lns[line]?.indexOf(importName);
|
|
||||||
if (!(err as any).id) {
|
|
||||||
(err as any).id = `${fileURLToPath(filePath)}:${line + 1}:${column + 1}`;
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return err;
|
|
||||||
}
|
|
||||||
|
|
||||||
const incompatiblePackages = {
|
|
||||||
'react-spectrum': `@adobe/react-spectrum is not compatible with Vite's server-side rendering mode at the moment. You can still use React Spectrum from the client. Create an island React component and use the client:only directive. From there you can use React Spectrum.`,
|
|
||||||
};
|
|
||||||
const incompatPackageExp = new RegExp(`(${Object.keys(incompatiblePackages).join('|')})`);
|
|
||||||
|
|
||||||
export function createCustomViteLogger(logLevel: LogLevel): Logger {
|
|
||||||
const viteLogger = createLogger(logLevel);
|
|
||||||
const logger: Logger = {
|
|
||||||
...viteLogger,
|
|
||||||
error(msg, options?) {
|
|
||||||
// Silence warnings from incompatible packages (we log better errors for these)
|
|
||||||
if (incompatPackageExp.test(msg)) return;
|
|
||||||
return viteLogger.error(msg, options);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
return logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateHint(err: ErrorWithMetadata, filePath?: URL): string | undefined {
|
|
||||||
if (/Unknown file extension \"\.(jsx|vue|svelte|astro|css)\" for /.test(err.message)) {
|
|
||||||
return 'You likely need to add this package to `vite.ssr.noExternal` in your astro config file.';
|
|
||||||
} else if (
|
|
||||||
err.toString().startsWith('ReferenceError') &&
|
|
||||||
(err.loc?.file ?? filePath?.pathname)?.endsWith('.astro')
|
|
||||||
) {
|
|
||||||
return 'export statements in `.astro` files do not have access to local variable declarations, only imported values.';
|
|
||||||
} else {
|
|
||||||
const res = incompatPackageExp.exec(err.stack);
|
|
||||||
if (res) {
|
|
||||||
const key = res[0] as keyof typeof incompatiblePackages;
|
|
||||||
return incompatiblePackages[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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, filePath?: URL): ErrorWithMetadata {
|
|
||||||
const err = e as SSRError;
|
|
||||||
|
|
||||||
if ((e as any).stack) {
|
|
||||||
// normalize error stack line-endings to \n
|
|
||||||
(e as any).stack = eol.lf((e as any).stack);
|
|
||||||
// derive error location from stack (if possible)
|
|
||||||
const stackText = stripAnsi(e.stack);
|
|
||||||
// TODO: this could be better, `src` might be something else
|
|
||||||
const possibleFilePath =
|
|
||||||
err.pluginCode ||
|
|
||||||
err.id ||
|
|
||||||
stackText.split('\n').find((ln) => ln.includes('src') || ln.includes('node_modules'));
|
|
||||||
const source = possibleFilePath?.replace(/^[^(]+\(([^)]+).*$/, '$1').replace(/^\s+at\s+/, '');
|
|
||||||
const [file, line, column] = source?.split(':') ?? [];
|
|
||||||
if (!err.loc && line && column) {
|
|
||||||
err.loc = {
|
|
||||||
file,
|
|
||||||
line: Number.parseInt(line),
|
|
||||||
column: Number.parseInt(column),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Derive plugin from stack (if possible)
|
|
||||||
if (!err.plugin) {
|
|
||||||
err.plugin =
|
|
||||||
/withastro\/astro\/packages\/integrations\/([\w-]+)/gim.exec(stackText)?.at(1) ||
|
|
||||||
/(@astrojs\/[\w-]+)\/(server|client|index)/gim.exec(stackText)?.at(1) ||
|
|
||||||
undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normalize stack (remove `/@fs/` urls, etc)
|
|
||||||
err.stack = cleanErrorStack(e.stack);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.name === 'YAMLException') {
|
|
||||||
err.loc = { file: (e as any).id, line: (e as any).mark.line, column: (e as any).mark.column };
|
|
||||||
err.message = (e as any).reason;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!err.frame && err.loc) {
|
|
||||||
try {
|
|
||||||
const fileContents = fs.readFileSync(err.loc.file!, 'utf8');
|
|
||||||
const frame = codeFrame(fileContents, err.loc);
|
|
||||||
err.frame = frame;
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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];
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
err.hint = generateHint(err, filePath);
|
|
||||||
return err;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generic error (probably from Vite, and already formatted)
|
|
||||||
err.hint = generateHint(e, filePath);
|
|
||||||
return err;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getViteErrorPayload(err: ErrorWithMetadata): ErrorPayload {
|
|
||||||
let plugin = err.plugin;
|
|
||||||
if (!plugin && err.hint) {
|
|
||||||
plugin = 'astro';
|
|
||||||
}
|
|
||||||
const message = `${err.message}\n\n${err.hint ?? ''}`;
|
|
||||||
return {
|
|
||||||
type: 'error',
|
|
||||||
err: {
|
|
||||||
...err,
|
|
||||||
plugin,
|
|
||||||
message: message.trim(),
|
|
||||||
stack: err.stack,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
24
packages/astro/src/core/errors/codes.ts
Normal file
24
packages/astro/src/core/errors/codes.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
export enum AstroErrorCodes {
|
||||||
|
// 1xxx are reserved for compiler errors
|
||||||
|
StaticRedirectNotAllowed = 2005,
|
||||||
|
UnavailableInSSR = 2006,
|
||||||
|
// Runtime errors
|
||||||
|
GenericRuntimeError = 3000,
|
||||||
|
// PostCSS errors
|
||||||
|
CssSyntaxError = 4000,
|
||||||
|
CssUnknownError = 4001,
|
||||||
|
// Vite SSR errors
|
||||||
|
FailedToLoadModuleSSR = 5000,
|
||||||
|
// Config Errors
|
||||||
|
ConfigError = 6000,
|
||||||
|
|
||||||
|
// Markdown Errors
|
||||||
|
GenericMarkdownError = 7000,
|
||||||
|
MarkdownFrontmatterParseError = 7001,
|
||||||
|
|
||||||
|
// General catch-alls for cases where we have zero information
|
||||||
|
UnknownCompilerError = 9000,
|
||||||
|
UnknownCompilerCSSError = 9001,
|
||||||
|
UnknownViteSSRError = 9002,
|
||||||
|
UnknownError = 9999,
|
||||||
|
}
|
2
packages/astro/src/core/errors/dev/index.ts
Normal file
2
packages/astro/src/core/errors/dev/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export { collectErrorMetadata } from './utils.js';
|
||||||
|
export { createCustomViteLogger, enhanceViteSSRError, getViteErrorPayload } from './vite.js';
|
88
packages/astro/src/core/errors/dev/utils.ts
Normal file
88
packages/astro/src/core/errors/dev/utils.ts
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
import type { BuildResult } from 'esbuild';
|
||||||
|
import * as fs from 'node:fs';
|
||||||
|
import type { SSRError } from '../../../@types/astro.js';
|
||||||
|
import { AggregateError, ErrorWithMetadata } from '../errors.js';
|
||||||
|
import { codeFrame } from '../printer.js';
|
||||||
|
import { collectInfoFromStacktrace } from '../utils.js';
|
||||||
|
|
||||||
|
export const incompatiblePackages = {
|
||||||
|
'react-spectrum': `@adobe/react-spectrum is not compatible with Vite's server-side rendering mode at the moment. You can still use React Spectrum from the client. Create an island React component and use the client:only directive. From there you can use React Spectrum.`,
|
||||||
|
};
|
||||||
|
export const incompatPackageExp = new RegExp(`(${Object.keys(incompatiblePackages).join('|')})`);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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, filePath?: URL): ErrorWithMetadata {
|
||||||
|
const err = AggregateError.is(e) ? (e.errors as SSRError[]) : [e as SSRError];
|
||||||
|
|
||||||
|
err.forEach((error) => {
|
||||||
|
if (error.stack) {
|
||||||
|
error = collectInfoFromStacktrace(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we don't have a frame, but we have a location let's try making up a frame for it
|
||||||
|
if (!error.frame && error.loc) {
|
||||||
|
try {
|
||||||
|
const fileContents = fs.readFileSync(error.loc.file!, 'utf8');
|
||||||
|
const frame = codeFrame(fileContents, error.loc);
|
||||||
|
error.frame = frame;
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generic error (probably from Vite, and already formatted)
|
||||||
|
if (!error.hint) {
|
||||||
|
error.hint = generateHint(e, filePath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// If we received an array of errors and it's not from us, it should be from ESBuild, try to extract info for Vite to display
|
||||||
|
if (!AggregateError.is(e) && Array.isArray((e as any).errors)) {
|
||||||
|
(e as BuildResult).errors.forEach((buildError, i) => {
|
||||||
|
const { location, pluginName } = buildError;
|
||||||
|
|
||||||
|
if (location) {
|
||||||
|
err[i].loc = { file: location.file, line: location.line, column: location.column };
|
||||||
|
err[i].id = err[0].id || location?.file;
|
||||||
|
}
|
||||||
|
|
||||||
|
const possibleFilePath = err[i].pluginCode || err[i].id || location?.file;
|
||||||
|
if (possibleFilePath && !err[i].frame) {
|
||||||
|
try {
|
||||||
|
const fileContents = fs.readFileSync(possibleFilePath, 'utf8');
|
||||||
|
err[i].frame = codeFrame(fileContents, { ...err[i].loc, file: possibleFilePath });
|
||||||
|
} catch {
|
||||||
|
// do nothing, code frame isn't that big a deal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pluginName) {
|
||||||
|
err[i].plugin = pluginName;
|
||||||
|
}
|
||||||
|
|
||||||
|
err[i].hint = generateHint(err[0], filePath);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Handle returning multiple errors
|
||||||
|
return err[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateHint(err: ErrorWithMetadata, filePath?: URL): string | undefined {
|
||||||
|
if (/Unknown file extension \"\.(jsx|vue|svelte|astro|css)\" for /.test(err.message)) {
|
||||||
|
return 'You likely need to add this package to `vite.ssr.noExternal` in your astro config file.';
|
||||||
|
} else if (
|
||||||
|
err.toString().startsWith('ReferenceError') &&
|
||||||
|
(err.loc?.file ?? filePath?.pathname)?.endsWith('.astro')
|
||||||
|
) {
|
||||||
|
return 'export statements in `.astro` files do not have access to local variable declarations, only imported values.';
|
||||||
|
} else {
|
||||||
|
const res = incompatPackageExp.exec(err.stack);
|
||||||
|
if (res) {
|
||||||
|
const key = res[0] as keyof typeof incompatiblePackages;
|
||||||
|
return incompatiblePackages[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
108
packages/astro/src/core/errors/dev/vite.ts
Normal file
108
packages/astro/src/core/errors/dev/vite.ts
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import {
|
||||||
|
createLogger,
|
||||||
|
type ErrorPayload,
|
||||||
|
type Logger,
|
||||||
|
type LogLevel,
|
||||||
|
type ViteDevServer,
|
||||||
|
} from 'vite';
|
||||||
|
import { AstroErrorCodes } from '../codes.js';
|
||||||
|
import { AstroError, type ErrorWithMetadata } from '../errors.js';
|
||||||
|
import { incompatPackageExp } from './utils.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom logger with better error reporting for incompatible packages
|
||||||
|
*/
|
||||||
|
export function createCustomViteLogger(logLevel: LogLevel): Logger {
|
||||||
|
const viteLogger = createLogger(logLevel);
|
||||||
|
const logger: Logger = {
|
||||||
|
...viteLogger,
|
||||||
|
error(msg, options?) {
|
||||||
|
// Silence warnings from incompatible packages (we log better errors for these)
|
||||||
|
if (incompatPackageExp.test(msg)) return;
|
||||||
|
return viteLogger.error(msg, options);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function enhanceViteSSRError(
|
||||||
|
error: Error,
|
||||||
|
filePath?: URL,
|
||||||
|
viteServer?: ViteDevServer
|
||||||
|
): AstroError {
|
||||||
|
// Vite will give you better stacktraces, using sourcemaps.
|
||||||
|
if (viteServer) {
|
||||||
|
try {
|
||||||
|
viteServer.ssrFixStacktrace(error);
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newError = new AstroError({
|
||||||
|
name: error.name,
|
||||||
|
message: error.message,
|
||||||
|
location: (error as any).loc,
|
||||||
|
stack: error.stack,
|
||||||
|
errorCode: (error as AstroError).errorCode
|
||||||
|
? (error as AstroError).errorCode
|
||||||
|
: AstroErrorCodes.UnknownViteSSRError,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Vite has a fairly generic error message when it fails to load a module, let's try to enhance it a bit
|
||||||
|
// https://github.com/vitejs/vite/blob/ee7c28a46a6563d54b828af42570c55f16b15d2c/packages/vite/src/node/ssr/ssrModuleLoader.ts#L91
|
||||||
|
if (filePath && /failed to load module for ssr:/.test(error.message)) {
|
||||||
|
const importName = error.message.split('for ssr:').at(1)?.trim();
|
||||||
|
if (importName) {
|
||||||
|
newError.setMessage(`Could not import "${importName}"`);
|
||||||
|
newError.setHint('Make sure the file exists');
|
||||||
|
newError.setErrorCode(AstroErrorCodes.FailedToLoadModuleSSR);
|
||||||
|
|
||||||
|
const path = fileURLToPath(filePath);
|
||||||
|
const content = fs.readFileSync(path).toString();
|
||||||
|
const lns = content.split('\n');
|
||||||
|
const line = lns.findIndex((ln) => ln.includes(importName));
|
||||||
|
|
||||||
|
if (line !== -1) {
|
||||||
|
const column = lns[line]?.indexOf(importName);
|
||||||
|
|
||||||
|
newError.setLocation({
|
||||||
|
file: path,
|
||||||
|
line: line + 1,
|
||||||
|
column,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a payload for Vite's error overlay
|
||||||
|
*/
|
||||||
|
export function getViteErrorPayload(err: ErrorWithMetadata): ErrorPayload {
|
||||||
|
let plugin = err.plugin;
|
||||||
|
if (!plugin && err.hint) {
|
||||||
|
plugin = 'astro';
|
||||||
|
}
|
||||||
|
const message = `${err.message}\n\n${err.hint ?? ''}`;
|
||||||
|
// Vite doesn't handle tabs correctly in its frames, so let's replace them with spaces
|
||||||
|
const frame = err.frame?.replace(/\t/g, ' ');
|
||||||
|
return {
|
||||||
|
type: 'error',
|
||||||
|
err: {
|
||||||
|
...err,
|
||||||
|
frame: frame,
|
||||||
|
loc: {
|
||||||
|
file: err.loc?.file,
|
||||||
|
// If we don't have a line and column, Vite won't make a clickable link, so let's fake 0:0 if we don't have a location
|
||||||
|
line: err.loc?.line ?? 0,
|
||||||
|
column: err.loc?.column ?? 0,
|
||||||
|
},
|
||||||
|
plugin,
|
||||||
|
message: message.trim(),
|
||||||
|
stack: err.stack,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
160
packages/astro/src/core/errors/errors.ts
Normal file
160
packages/astro/src/core/errors/errors.ts
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
import type { DiagnosticCode } from '@astrojs/compiler/shared/diagnostics.js';
|
||||||
|
import { AstroErrorCodes } from './codes.js';
|
||||||
|
import { codeFrame } from './printer.js';
|
||||||
|
|
||||||
|
interface ErrorProperties {
|
||||||
|
errorCode: AstroErrorCodes | DiagnosticCode;
|
||||||
|
name?: string;
|
||||||
|
message?: string;
|
||||||
|
location?: ErrorLocation;
|
||||||
|
hint?: string;
|
||||||
|
stack?: string;
|
||||||
|
frame?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ErrorLocation {
|
||||||
|
file?: string;
|
||||||
|
line?: number;
|
||||||
|
column?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrorTypes =
|
||||||
|
| 'CSSError'
|
||||||
|
| 'CompilerError'
|
||||||
|
| 'RuntimeError'
|
||||||
|
| 'MarkdownError'
|
||||||
|
| 'AstroAggregateError';
|
||||||
|
|
||||||
|
export class AstroError extends Error {
|
||||||
|
public errorCode: AstroErrorCodes | DiagnosticCode;
|
||||||
|
public loc: ErrorLocation | undefined;
|
||||||
|
public hint: string | undefined;
|
||||||
|
public frame: string | undefined;
|
||||||
|
|
||||||
|
type: ErrorTypes | undefined;
|
||||||
|
|
||||||
|
constructor(props: ErrorProperties, ...params: any) {
|
||||||
|
super(...params);
|
||||||
|
|
||||||
|
const { errorCode, name, message, stack, location, hint, frame } = props;
|
||||||
|
|
||||||
|
this.errorCode = errorCode;
|
||||||
|
if (name) {
|
||||||
|
this.name = name;
|
||||||
|
} else {
|
||||||
|
// If we don't have a name, let's generate one from the code
|
||||||
|
this.name = AstroErrorCodes[errorCode];
|
||||||
|
}
|
||||||
|
if (message) this.message = message;
|
||||||
|
// Only set this if we actually have a stack passed, otherwise uses Error's
|
||||||
|
this.stack = stack ? stack : this.stack;
|
||||||
|
this.loc = location;
|
||||||
|
this.hint = hint;
|
||||||
|
this.frame = frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setErrorCode(errorCode: AstroErrorCodes | DiagnosticCode) {
|
||||||
|
this.errorCode = errorCode;
|
||||||
|
this.name = AstroErrorCodes[errorCode];
|
||||||
|
}
|
||||||
|
|
||||||
|
public setLocation(location: ErrorLocation): void {
|
||||||
|
this.loc = location;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setName(name: string): void {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setMessage(message: string): void {
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setHint(hint: string): void {
|
||||||
|
this.hint = hint;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setFrame(source: string, location: ErrorLocation): void {
|
||||||
|
this.frame = codeFrame(source, location);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CSSError extends AstroError {
|
||||||
|
type: ErrorTypes = 'CSSError';
|
||||||
|
|
||||||
|
static is(err: Error | unknown): boolean {
|
||||||
|
return (err as CSSError).type === 'CSSError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CompilerError extends AstroError {
|
||||||
|
type: ErrorTypes = 'CompilerError';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
props: ErrorProperties & { errorCode: DiagnosticCode | AstroErrorCodes.UnknownCompilerError },
|
||||||
|
...params: any
|
||||||
|
) {
|
||||||
|
super(props, ...params);
|
||||||
|
|
||||||
|
this.name = 'CompilerError';
|
||||||
|
}
|
||||||
|
|
||||||
|
static is(err: Error | unknown): boolean {
|
||||||
|
return (err as CompilerError).type === 'CompilerError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RuntimeError extends AstroError {
|
||||||
|
type: ErrorTypes = 'RuntimeError';
|
||||||
|
|
||||||
|
static is(err: Error | unknown): boolean {
|
||||||
|
return (err as RuntimeError).type === 'RuntimeError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MarkdownError extends AstroError {
|
||||||
|
type: ErrorTypes = 'MarkdownError';
|
||||||
|
|
||||||
|
static is(err: Error | unknown): boolean {
|
||||||
|
return (err as MarkdownError).type === 'MarkdownError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AggregateError extends AstroError {
|
||||||
|
type: ErrorTypes = 'AstroAggregateError';
|
||||||
|
errors: AstroError[];
|
||||||
|
|
||||||
|
// Despite being a collection of errors, AggregateError still needs to have a main error attached to it
|
||||||
|
// This is because Vite expects every thrown errors handled during HMR to be, well, Error and have a message
|
||||||
|
constructor(props: ErrorProperties & { errors: AstroError[] }, ...params: any) {
|
||||||
|
super(props, ...params);
|
||||||
|
|
||||||
|
this.errors = props.errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
static is(err: Error | unknown): boolean {
|
||||||
|
return (err as AggregateError).type === 'AstroAggregateError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic object representing an error with all possible data
|
||||||
|
* Compatible with both Astro's and Vite's errors
|
||||||
|
*/
|
||||||
|
export interface ErrorWithMetadata {
|
||||||
|
[name: string]: any;
|
||||||
|
type?: ErrorTypes;
|
||||||
|
message: string;
|
||||||
|
stack: string;
|
||||||
|
code?: number;
|
||||||
|
hint?: string;
|
||||||
|
id?: string;
|
||||||
|
frame?: string;
|
||||||
|
plugin?: string;
|
||||||
|
pluginCode?: string;
|
||||||
|
loc?: {
|
||||||
|
file?: string;
|
||||||
|
line?: number;
|
||||||
|
column?: number;
|
||||||
|
};
|
||||||
|
}
|
12
packages/astro/src/core/errors/index.ts
Normal file
12
packages/astro/src/core/errors/index.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
export { AstroErrorCodes } from './codes.js';
|
||||||
|
export {
|
||||||
|
AstroError,
|
||||||
|
CSSError,
|
||||||
|
CompilerError,
|
||||||
|
RuntimeError,
|
||||||
|
MarkdownError,
|
||||||
|
AggregateError,
|
||||||
|
} from './errors.js';
|
||||||
|
export type { ErrorLocation, ErrorWithMetadata } from './errors';
|
||||||
|
export { codeFrame } from './printer.js';
|
||||||
|
export { positionAt, collectInfoFromStacktrace } from './utils.js';
|
36
packages/astro/src/core/errors/printer.ts
Normal file
36
packages/astro/src/core/errors/printer.ts
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import eol from 'eol';
|
||||||
|
import type { ErrorLocation } from './errors.js';
|
||||||
|
|
||||||
|
/** Generate a code frame from string and an error location */
|
||||||
|
export function codeFrame(src: string, loc: ErrorLocation): string {
|
||||||
|
if (!loc || loc.line === undefined || loc.column === undefined) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const lines = eol
|
||||||
|
.lf(src)
|
||||||
|
.split('\n')
|
||||||
|
.map((ln) => ln.replace(/\t/g, ' '));
|
||||||
|
// grab 2 lines before, and 3 lines after focused line
|
||||||
|
const visibleLines = [];
|
||||||
|
for (let n = -2; n <= 2; n++) {
|
||||||
|
if (lines[loc.line + n]) visibleLines.push(loc.line + n);
|
||||||
|
}
|
||||||
|
// figure out gutter width
|
||||||
|
let gutterWidth = 0;
|
||||||
|
for (const lineNo of visibleLines) {
|
||||||
|
let w = `> ${lineNo}`;
|
||||||
|
if (w.length > gutterWidth) gutterWidth = w.length;
|
||||||
|
}
|
||||||
|
// print lines
|
||||||
|
let output = '';
|
||||||
|
for (const lineNo of visibleLines) {
|
||||||
|
const isFocusedLine = lineNo === loc.line - 1;
|
||||||
|
output += isFocusedLine ? '> ' : ' ';
|
||||||
|
output += `${lineNo + 1} | ${lines[lineNo]}\n`;
|
||||||
|
if (isFocusedLine)
|
||||||
|
output += `${Array.from({ length: gutterWidth }).join(' ')} | ${Array.from({
|
||||||
|
length: loc.column,
|
||||||
|
}).join(' ')}^\n`;
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
120
packages/astro/src/core/errors/utils.ts
Normal file
120
packages/astro/src/core/errors/utils.ts
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
import stripAnsi from 'strip-ansi';
|
||||||
|
import type { SSRError } from '../../@types/astro.js';
|
||||||
|
import eol from 'eol';
|
||||||
|
|
||||||
|
export function collectInfoFromStacktrace(error: SSRError): SSRError {
|
||||||
|
if (!error.stack) return error;
|
||||||
|
|
||||||
|
// normalize error stack line-endings to \n
|
||||||
|
error.stack = eol.lf(error.stack);
|
||||||
|
const stackText = stripAnsi(error.stack);
|
||||||
|
|
||||||
|
// Try to find possible location from stack if we don't have one
|
||||||
|
if (!error.loc || (!error.loc.column && !error.loc.line)) {
|
||||||
|
const possibleFilePath =
|
||||||
|
error.loc?.file ||
|
||||||
|
error.pluginCode ||
|
||||||
|
error.id ||
|
||||||
|
// TODO: this could be better, `src` might be something else
|
||||||
|
stackText.split('\n').find((ln) => ln.includes('src') || ln.includes('node_modules'));
|
||||||
|
const source = possibleFilePath?.replace(/^[^(]+\(([^)]+).*$/, '$1').replace(/^\s+at\s+/, '');
|
||||||
|
|
||||||
|
const [file, line, column] = source?.split(':') ?? [];
|
||||||
|
if (line && column) {
|
||||||
|
error.loc = {
|
||||||
|
file,
|
||||||
|
line: Number.parseInt(line),
|
||||||
|
column: Number.parseInt(column),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Derive plugin from stack (if possible)
|
||||||
|
if (!error.plugin) {
|
||||||
|
error.plugin =
|
||||||
|
/withastro\/astro\/packages\/integrations\/([\w-]+)/gim.exec(stackText)?.at(1) ||
|
||||||
|
/(@astrojs\/[\w-]+)\/(server|client|index)/gim.exec(stackText)?.at(1) ||
|
||||||
|
undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize stack (remove `/@fs/` urls, etc)
|
||||||
|
error.stack = cleanErrorStack(error.stack);
|
||||||
|
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanErrorStack(stack: string) {
|
||||||
|
return stack
|
||||||
|
.split(/\n/g)
|
||||||
|
.map((l) => l.replace(/\/@fs\//g, '/'))
|
||||||
|
.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the line and character based on the offset
|
||||||
|
* @param offset The index of the position
|
||||||
|
* @param text The text for which the position should be retrived
|
||||||
|
*/
|
||||||
|
export function positionAt(
|
||||||
|
offset: number,
|
||||||
|
text: string
|
||||||
|
): {
|
||||||
|
line: number;
|
||||||
|
column: number;
|
||||||
|
} {
|
||||||
|
const lineOffsets = getLineOffsets(text);
|
||||||
|
offset = Math.max(0, Math.min(text.length, offset));
|
||||||
|
|
||||||
|
let low = 0;
|
||||||
|
let high = lineOffsets.length;
|
||||||
|
if (high === 0) {
|
||||||
|
return {
|
||||||
|
line: 0,
|
||||||
|
column: offset,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
while (low <= high) {
|
||||||
|
const mid = Math.floor((low + high) / 2);
|
||||||
|
const lineOffset = lineOffsets[mid];
|
||||||
|
|
||||||
|
if (lineOffset === offset) {
|
||||||
|
return {
|
||||||
|
line: mid,
|
||||||
|
column: 0,
|
||||||
|
};
|
||||||
|
} else if (offset > lineOffset) {
|
||||||
|
low = mid + 1;
|
||||||
|
} else {
|
||||||
|
high = mid - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// low is the least x for which the line offset is larger than the current offset
|
||||||
|
// or array.length if no line offset is larger than the current offset
|
||||||
|
const line = low - 1;
|
||||||
|
return { line, column: offset - lineOffsets[line] };
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLineOffsets(text: string) {
|
||||||
|
const lineOffsets = [];
|
||||||
|
let isLineStart = true;
|
||||||
|
|
||||||
|
for (let i = 0; i < text.length; i++) {
|
||||||
|
if (isLineStart) {
|
||||||
|
lineOffsets.push(i);
|
||||||
|
isLineStart = false;
|
||||||
|
}
|
||||||
|
const ch = text.charAt(i);
|
||||||
|
isLineStart = ch === '\r' || ch === '\n';
|
||||||
|
if (ch === '\r' && i + 1 < text.length && text.charAt(i + 1) === '\n') {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLineStart && text.length > 0) {
|
||||||
|
lineOffsets.push(text.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lineOffsets;
|
||||||
|
}
|
|
@ -18,7 +18,7 @@ import type { AddressInfo } from 'net';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import { ResolvedServerUrls } from 'vite';
|
import { ResolvedServerUrls } from 'vite';
|
||||||
import { ZodError } from 'zod';
|
import { ZodError } from 'zod';
|
||||||
import { ErrorWithMetadata } from './errors.js';
|
import { ErrorWithMetadata } from './errors/index.js';
|
||||||
import { removeTrailingForwardSlash } from './path.js';
|
import { removeTrailingForwardSlash } from './path.js';
|
||||||
import { emoji, getLocalAddress, padMultilineString } from './util.js';
|
import { emoji, getLocalAddress, padMultilineString } from './util.js';
|
||||||
|
|
||||||
|
@ -257,9 +257,15 @@ export function formatErrorMessage(err: ErrorWithMetadata, args: string[] = []):
|
||||||
args.push(` ${bold('Hint:')}`);
|
args.push(` ${bold('Hint:')}`);
|
||||||
args.push(yellow(padMultilineString(err.hint, 4)));
|
args.push(yellow(padMultilineString(err.hint, 4)));
|
||||||
}
|
}
|
||||||
if (err.id) {
|
if (err.id || err.loc?.file) {
|
||||||
args.push(` ${bold('File:')}`);
|
args.push(` ${bold('File:')}`);
|
||||||
args.push(red(` ${err.id}`));
|
args.push(
|
||||||
|
red(
|
||||||
|
` ${err.id ?? err.loc?.file}${
|
||||||
|
err.loc?.line && err.loc.column ? `:${err.loc.line}:${err.loc.column}` : ''
|
||||||
|
}`
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (err.frame) {
|
if (err.frame) {
|
||||||
args.push(` ${bold('Code:')}`);
|
args.push(` ${bold('Code:')}`);
|
||||||
|
|
|
@ -9,6 +9,8 @@ import type {
|
||||||
SSRLoadedRenderer,
|
SSRLoadedRenderer,
|
||||||
} from '../../../@types/astro';
|
} from '../../../@types/astro';
|
||||||
import { PAGE_SCRIPT_ID } from '../../../vite-plugin-scripts/index.js';
|
import { PAGE_SCRIPT_ID } from '../../../vite-plugin-scripts/index.js';
|
||||||
|
import { enhanceViteSSRError } from '../../errors/dev/index.js';
|
||||||
|
import { MarkdownError, CSSError, AggregateError } from '../../errors/index.js';
|
||||||
import { LogOptions } from '../../logger/core.js';
|
import { LogOptions } from '../../logger/core.js';
|
||||||
import { isPage, resolveIdToUrl } from '../../util.js';
|
import { isPage, resolveIdToUrl } from '../../util.js';
|
||||||
import { createRenderContext, renderPage as coreRenderPage } from '../index.js';
|
import { createRenderContext, renderPage as coreRenderPage } from '../index.js';
|
||||||
|
@ -91,9 +93,19 @@ export async function preload({
|
||||||
}: Pick<SSROptions, 'env' | 'filePath'>): Promise<ComponentPreload> {
|
}: Pick<SSROptions, 'env' | 'filePath'>): Promise<ComponentPreload> {
|
||||||
// Important: This needs to happen first, in case a renderer provides polyfills.
|
// Important: This needs to happen first, in case a renderer provides polyfills.
|
||||||
const renderers = await loadRenderers(env.viteServer, env.settings);
|
const renderers = await loadRenderers(env.viteServer, env.settings);
|
||||||
// Load the module from the Vite SSR Runtime.
|
|
||||||
const mod = (await env.viteServer.ssrLoadModule(fileURLToPath(filePath))) as ComponentInstance;
|
try {
|
||||||
return [renderers, mod];
|
// Load the module from the Vite SSR Runtime.
|
||||||
|
const mod = (await env.viteServer.ssrLoadModule(fileURLToPath(filePath))) as ComponentInstance;
|
||||||
|
return [renderers, mod];
|
||||||
|
} catch (err) {
|
||||||
|
// If the error came from Markdown or CSS, we already handled it and there's no need to enhance it
|
||||||
|
if (MarkdownError.is(err) || CSSError.is(err) || AggregateError.is(err)) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw enhanceViteSSRError(err as Error, filePath, env.viteServer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GetScriptsAndStylesParams {
|
interface GetScriptsAndStylesParams {
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import eol from 'eol';
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import resolve from 'resolve';
|
import resolve from 'resolve';
|
||||||
|
@ -99,38 +98,6 @@ export function createSafeError(err: any): Error {
|
||||||
: new Error(JSON.stringify(err));
|
: new Error(JSON.stringify(err));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** generate code frame from esbuild error */
|
|
||||||
export function codeFrame(src: string, loc: ErrorPayload['err']['loc']): string {
|
|
||||||
if (!loc) return '';
|
|
||||||
const lines = eol
|
|
||||||
.lf(src)
|
|
||||||
.split('\n')
|
|
||||||
.map((ln) => ln.replace(/\t/g, ' '));
|
|
||||||
// grab 2 lines before, and 3 lines after focused line
|
|
||||||
const visibleLines = [];
|
|
||||||
for (let n = -2; n <= 2; n++) {
|
|
||||||
if (lines[loc.line + n]) visibleLines.push(loc.line + n);
|
|
||||||
}
|
|
||||||
// figure out gutter width
|
|
||||||
let gutterWidth = 0;
|
|
||||||
for (const lineNo of visibleLines) {
|
|
||||||
let w = `> ${lineNo}`;
|
|
||||||
if (w.length > gutterWidth) gutterWidth = w.length;
|
|
||||||
}
|
|
||||||
// print lines
|
|
||||||
let output = '';
|
|
||||||
for (const lineNo of visibleLines) {
|
|
||||||
const isFocusedLine = lineNo === loc.line - 1;
|
|
||||||
output += isFocusedLine ? '> ' : ' ';
|
|
||||||
output += `${lineNo + 1} | ${lines[lineNo]}\n`;
|
|
||||||
if (isFocusedLine)
|
|
||||||
output += `${Array.from({ length: gutterWidth }).join(' ')} | ${Array.from({
|
|
||||||
length: loc.column,
|
|
||||||
}).join(' ')}^\n`;
|
|
||||||
}
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveDependency(dep: string, projectRoot: URL) {
|
export function resolveDependency(dep: string, projectRoot: URL) {
|
||||||
const resolved = resolve.sync(dep, {
|
const resolved = resolve.sync(dep, {
|
||||||
basedir: fileURLToPath(projectRoot),
|
basedir: fileURLToPath(projectRoot),
|
||||||
|
@ -256,14 +223,3 @@ export function resolvePath(specifier: string, importer: string) {
|
||||||
return specifier;
|
return specifier;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AggregateError =
|
|
||||||
typeof (globalThis as any).AggregateError !== 'undefined'
|
|
||||||
? (globalThis as any).AggregateError
|
|
||||||
: class extends Error {
|
|
||||||
errors: Array<any> = [];
|
|
||||||
constructor(errors: Iterable<any>, message?: string | undefined) {
|
|
||||||
super(message);
|
|
||||||
this.errors = Array.from(errors);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { ZodError } from 'zod';
|
import { ZodError } from 'zod';
|
||||||
import { AstroErrorCodes, ErrorWithMetadata } from '../core/errors.js';
|
import { AstroErrorCodes, ErrorWithMetadata } from '../core/errors/index.js';
|
||||||
|
|
||||||
const EVENT_ERROR = 'ASTRO_CLI_ERROR';
|
const EVENT_ERROR = 'ASTRO_CLI_ERROR';
|
||||||
|
|
||||||
|
|
|
@ -7,12 +7,6 @@ import { DevelopmentEnvironment, SSROptions } from '../core/render/dev/index';
|
||||||
import { Readable } from 'stream';
|
import { Readable } from 'stream';
|
||||||
import { attachToResponse, getSetCookiesFromResponse } from '../core/cookies/index.js';
|
import { attachToResponse, getSetCookiesFromResponse } from '../core/cookies/index.js';
|
||||||
import { call as callEndpoint } from '../core/endpoint/dev/index.js';
|
import { call as callEndpoint } from '../core/endpoint/dev/index.js';
|
||||||
import {
|
|
||||||
collectErrorMetadata,
|
|
||||||
ErrorWithMetadata,
|
|
||||||
fixViteErrorMessage,
|
|
||||||
getViteErrorPayload,
|
|
||||||
} from '../core/errors.js';
|
|
||||||
import { error, info, LogOptions, warn } from '../core/logger/core.js';
|
import { error, info, LogOptions, warn } from '../core/logger/core.js';
|
||||||
import * as msg from '../core/messages.js';
|
import * as msg from '../core/messages.js';
|
||||||
import { appendForwardSlash } from '../core/path.js';
|
import { appendForwardSlash } from '../core/path.js';
|
||||||
|
@ -22,6 +16,8 @@ import { createRequest } from '../core/request.js';
|
||||||
import { createRouteManifest, matchAllRoutes } from '../core/routing/index.js';
|
import { createRouteManifest, matchAllRoutes } from '../core/routing/index.js';
|
||||||
import { resolvePages } from '../core/util.js';
|
import { resolvePages } from '../core/util.js';
|
||||||
import notFoundTemplate, { subpathNotUsedTemplate } from '../template/4xx.js';
|
import notFoundTemplate, { subpathNotUsedTemplate } from '../template/4xx.js';
|
||||||
|
import { collectErrorMetadata, getViteErrorPayload } from '../core/errors/dev/index.js';
|
||||||
|
import type { ErrorWithMetadata } from '../core/errors/index.js';
|
||||||
|
|
||||||
interface AstroPluginOptions {
|
interface AstroPluginOptions {
|
||||||
settings: AstroSettings;
|
settings: AstroSettings;
|
||||||
|
@ -282,15 +278,14 @@ async function handleRequest(
|
||||||
body = Buffer.concat(bytes);
|
body = Buffer.concat(bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
let filePath: URL | undefined;
|
|
||||||
try {
|
try {
|
||||||
const matchedRoute = await matchRoute(pathname, env, manifest);
|
const matchedRoute = await matchRoute(pathname, env, manifest);
|
||||||
filePath = matchedRoute?.filePath;
|
|
||||||
|
|
||||||
return await handleRoute(matchedRoute, url, pathname, body, origin, env, manifest, req, res);
|
return await handleRoute(matchedRoute, url, pathname, body, origin, env, manifest, req, res);
|
||||||
} catch (_err) {
|
} catch (_err) {
|
||||||
const err = fixViteErrorMessage(_err, viteServer, filePath);
|
// This is our last line of defense regarding errors where we still might have some information about the request
|
||||||
const errorWithMetadata = collectErrorMetadata(err);
|
// Our error should already be complete, but let's try to add a bit more through some guesswork
|
||||||
|
const errorWithMetadata = collectErrorMetadata(_err);
|
||||||
|
|
||||||
error(env.logging, null, msg.formatErrorMessage(errorWithMetadata));
|
error(env.logging, null, msg.formatErrorMessage(errorWithMetadata));
|
||||||
handle500Response(viteServer, origin, req, res, errorWithMetadata);
|
handle500Response(viteServer, origin, req, res, errorWithMetadata);
|
||||||
}
|
}
|
||||||
|
@ -383,7 +378,7 @@ async function handleRoute(
|
||||||
await writeWebResponse(res, result.response);
|
await writeWebResponse(res, result.response);
|
||||||
} else {
|
} else {
|
||||||
let contentType = 'text/plain';
|
let contentType = 'text/plain';
|
||||||
// Dynamic routes don’t include `route.pathname`, so synthesise a path for these (e.g. 'src/pages/[slug].svg')
|
// Dynamic routes don’t include `route.pathname`, so synthesize a path for these (e.g. 'src/pages/[slug].svg')
|
||||||
const filepath =
|
const filepath =
|
||||||
route.pathname ||
|
route.pathname ||
|
||||||
route.segments.map((segment) => segment.map((p) => p.content).join('')).join('/');
|
route.segments.map((segment) => segment.map((p) => p.content).join('')).join('/');
|
||||||
|
|
|
@ -311,7 +311,7 @@ export default function astro({ settings, logging }: AstroPluginOptions): vite.P
|
||||||
}
|
}
|
||||||
|
|
||||||
// improve compiler errors
|
// improve compiler errors
|
||||||
if (err.stack.includes('wasm-function')) {
|
if (err.stack && err.stack.includes('wasm-function')) {
|
||||||
const search = new URLSearchParams({
|
const search = new URLSearchParams({
|
||||||
labels: 'compiler',
|
labels: 'compiler',
|
||||||
title: '🐛 BUG: `@astrojs/compiler` panic',
|
title: '🐛 BUG: `@astrojs/compiler` panic',
|
||||||
|
|
|
@ -8,7 +8,7 @@ import type { Plugin, ViteDevServer } from 'vite';
|
||||||
import type { AstroSettings } from '../@types/astro';
|
import type { AstroSettings } from '../@types/astro';
|
||||||
import { pagesVirtualModuleId } from '../core/app/index.js';
|
import { pagesVirtualModuleId } from '../core/app/index.js';
|
||||||
import { cachedCompilation, CompileProps } from '../core/compile/index.js';
|
import { cachedCompilation, CompileProps } from '../core/compile/index.js';
|
||||||
import { collectErrorMetadata } from '../core/errors.js';
|
import { AstroErrorCodes, MarkdownError } from '../core/errors/index.js';
|
||||||
import type { LogOptions } from '../core/logger/core.js';
|
import type { LogOptions } from '../core/logger/core.js';
|
||||||
import { isMarkdownFile } from '../core/util.js';
|
import { isMarkdownFile } from '../core/util.js';
|
||||||
import type { PluginMetadata as AstroPluginMetadata } from '../vite-plugin-astro/types';
|
import type { PluginMetadata as AstroPluginMetadata } from '../vite-plugin-astro/types';
|
||||||
|
@ -30,9 +30,28 @@ const MARKDOWN_CONTENT_FLAG = '?content';
|
||||||
function safeMatter(source: string, id: string) {
|
function safeMatter(source: string, id: string) {
|
||||||
try {
|
try {
|
||||||
return matter(source);
|
return matter(source);
|
||||||
} catch (e) {
|
} catch (err: any) {
|
||||||
(e as any).id = id;
|
const markdownError = new MarkdownError({
|
||||||
throw collectErrorMetadata(e);
|
errorCode: AstroErrorCodes.GenericMarkdownError,
|
||||||
|
message: err.message,
|
||||||
|
stack: err.stack,
|
||||||
|
location: {
|
||||||
|
file: id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (err.name === 'YAMLException') {
|
||||||
|
markdownError.setErrorCode(AstroErrorCodes.MarkdownFrontmatterParseError);
|
||||||
|
markdownError.setLocation({
|
||||||
|
file: id,
|
||||||
|
line: err.mark.line,
|
||||||
|
column: err.mark.column,
|
||||||
|
});
|
||||||
|
|
||||||
|
markdownError.setMessage(err.reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw markdownError;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { fileURLToPath } from 'node:url';
|
||||||
import type { Plugin } from 'vite';
|
import type { Plugin } from 'vite';
|
||||||
import { normalizePath } from 'vite';
|
import { normalizePath } from 'vite';
|
||||||
import type { AstroSettings } from '../@types/astro';
|
import type { AstroSettings } from '../@types/astro';
|
||||||
import { collectErrorMetadata } from '../core/errors.js';
|
import { AstroErrorCodes, MarkdownError } from '../core/errors/index.js';
|
||||||
import type { LogOptions } from '../core/logger/core.js';
|
import type { LogOptions } from '../core/logger/core.js';
|
||||||
import { warn } from '../core/logger/core.js';
|
import { warn } from '../core/logger/core.js';
|
||||||
import { isMarkdownFile } from '../core/util.js';
|
import { isMarkdownFile } from '../core/util.js';
|
||||||
|
@ -20,9 +20,28 @@ interface AstroPluginOptions {
|
||||||
function safeMatter(source: string, id: string) {
|
function safeMatter(source: string, id: string) {
|
||||||
try {
|
try {
|
||||||
return matter(source);
|
return matter(source);
|
||||||
} catch (e) {
|
} catch (err: any) {
|
||||||
(e as any).id = id;
|
const markdownError = new MarkdownError({
|
||||||
throw collectErrorMetadata(e);
|
errorCode: AstroErrorCodes.GenericMarkdownError,
|
||||||
|
message: err.message,
|
||||||
|
stack: err.stack,
|
||||||
|
location: {
|
||||||
|
file: id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (err.name === 'YAMLException') {
|
||||||
|
markdownError.setErrorCode(AstroErrorCodes.MarkdownFrontmatterParseError);
|
||||||
|
markdownError.setLocation({
|
||||||
|
file: id,
|
||||||
|
line: err.mark.line,
|
||||||
|
column: err.mark.column,
|
||||||
|
});
|
||||||
|
|
||||||
|
markdownError.setMessage(err.reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw markdownError;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,11 @@ import { fileURLToPath } from 'url';
|
||||||
import type { TransformStyle } from '../core/compile/index';
|
import type { TransformStyle } from '../core/compile/index';
|
||||||
import { createTransformStyleWithViteFn, TransformStyleWithVite } from './transform-with-vite.js';
|
import { createTransformStyleWithViteFn, TransformStyleWithVite } from './transform-with-vite.js';
|
||||||
|
|
||||||
|
import { readFileSync } from 'fs';
|
||||||
import type * as vite from 'vite';
|
import type * as vite from 'vite';
|
||||||
|
import { AstroErrorCodes } from '../core/errors/codes.js';
|
||||||
|
import { CSSError } from '../core/errors/errors.js';
|
||||||
|
import { positionAt } from '../core/errors/utils.js';
|
||||||
|
|
||||||
export type ViteStyleTransformer = {
|
export type ViteStyleTransformer = {
|
||||||
viteDevServer?: vite.ViteDevServer;
|
viteDevServer?: vite.ViteDevServer;
|
||||||
|
@ -35,13 +39,67 @@ export function createTransformStyles(
|
||||||
const normalizedID = getNormalizedIDForPostCSS(filename);
|
const normalizedID = getNormalizedIDForPostCSS(filename);
|
||||||
|
|
||||||
return async function (styleSource, lang) {
|
return async function (styleSource, lang) {
|
||||||
const result = await viteStyleTransformer.transformStyleWithVite.call(pluginContext, {
|
let result: any;
|
||||||
id: normalizedID,
|
try {
|
||||||
source: styleSource,
|
result = await viteStyleTransformer.transformStyleWithVite.call(pluginContext, {
|
||||||
lang,
|
id: normalizedID,
|
||||||
ssr,
|
source: styleSource,
|
||||||
viteDevServer: viteStyleTransformer.viteDevServer,
|
lang,
|
||||||
});
|
ssr,
|
||||||
|
viteDevServer: viteStyleTransformer.viteDevServer,
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
const fileContent = readFileSync(filename).toString();
|
||||||
|
const styleTagBeginning = fileContent.indexOf(err.input?.source ?? err.code);
|
||||||
|
|
||||||
|
// PostCSS Syntax Error
|
||||||
|
if (err.name === 'CssSyntaxError') {
|
||||||
|
const errorLine = positionAt(styleTagBeginning, fileContent).line + (err.line ?? 0);
|
||||||
|
|
||||||
|
// Vite will handle creating the frame for us with proper line numbers, no need to create one
|
||||||
|
|
||||||
|
throw new CSSError({
|
||||||
|
errorCode: AstroErrorCodes.CssSyntaxError,
|
||||||
|
message: err.reason,
|
||||||
|
location: {
|
||||||
|
file: filename,
|
||||||
|
line: errorLine,
|
||||||
|
column: err.column,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some CSS processor will return a line and a column, so let's try to show a pretty error
|
||||||
|
if (err.line && err.column) {
|
||||||
|
const errorLine = positionAt(styleTagBeginning, fileContent).line + (err.line ?? 0);
|
||||||
|
|
||||||
|
throw new CSSError({
|
||||||
|
errorCode: AstroErrorCodes.CssUnknownError,
|
||||||
|
message: err.message,
|
||||||
|
location: {
|
||||||
|
file: filename,
|
||||||
|
line: errorLine,
|
||||||
|
column: err.column,
|
||||||
|
},
|
||||||
|
frame: err.frame,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// For other errors we'll just point to the beginning of the style tag
|
||||||
|
const errorPosition = positionAt(styleTagBeginning, fileContent);
|
||||||
|
errorPosition.line += 1;
|
||||||
|
|
||||||
|
throw new CSSError({
|
||||||
|
errorCode: AstroErrorCodes.CssUnknownError,
|
||||||
|
message: err.message,
|
||||||
|
location: {
|
||||||
|
file: filename,
|
||||||
|
line: errorPosition.line,
|
||||||
|
column: 0,
|
||||||
|
},
|
||||||
|
frame: err.frame,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import { AstroErrorCodes } from '../dist/core/errors.js';
|
import { AstroErrorCodes } from '../dist/core/errors/codes.js';
|
||||||
import * as events from '../dist/events/index.js';
|
import * as events from '../dist/events/index.js';
|
||||||
|
|
||||||
describe('Events', () => {
|
describe('Events', () => {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import { cachedCompilation } from '../../../dist/core/compile/index.js';
|
import { cachedCompilation } from '../../../dist/core/compile/index.js';
|
||||||
import { AggregateError } from '../../../dist/core/util.js';
|
import { AggregateError } from '../../../dist/core/errors/index.js';
|
||||||
|
|
||||||
describe('astro/src/core/compile', () => {
|
describe('astro/src/core/compile', () => {
|
||||||
describe('Invalid CSS', () => {
|
describe('Invalid CSS', () => {
|
||||||
|
|
Loading…
Reference in a new issue