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'));
|
||||
|
||||
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([
|
||||
// Wait for page reload
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
} from '../core/config/index.js';
|
||||
import { ASTRO_VERSION } from '../core/constants.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 { enableVerboseLogging, nodeLogDestination } from '../core/logger/node.js';
|
||||
import { formatConfigErrorMessage, formatErrorMessage, printHelp } from '../core/messages.js';
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
runHookConfigSetup,
|
||||
} from '../../integrations/index.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 { apply as applyPolyfill } from '../polyfill.js';
|
||||
import { RouteCache } from '../render/route-cache.js';
|
||||
|
@ -169,7 +169,7 @@ class AstroBuilder {
|
|||
try {
|
||||
await this.build(setupData);
|
||||
} 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 { 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 { AggregateError, resolvePath, viteID } from '../util.js';
|
||||
import { resolvePath, viteID } from '../util.js';
|
||||
import { createStylePreprocessor } from './style.js';
|
||||
|
||||
type CompilationCache = Map<string, CompileResult>;
|
||||
|
@ -30,7 +31,7 @@ async function compile({
|
|||
transformStyle,
|
||||
}: CompileProps): Promise<CompileResult> {
|
||||
let cssDeps = new Set<string>();
|
||||
let cssTransformErrors: Error[] = [];
|
||||
let cssTransformErrors: AstroError[] = [];
|
||||
|
||||
// Transform from `.astro` to valid `.ts`
|
||||
// use `sourcemap: "both"` so that sourcemap is included in the code
|
||||
|
@ -51,26 +52,51 @@ async function compile({
|
|||
return resolvePath(specifier, filename);
|
||||
},
|
||||
})
|
||||
.catch((err) => {
|
||||
// throw compiler errors here if encountered
|
||||
err.code = err.code || AstroErrorCodes.UnknownCompilerError;
|
||||
throw err;
|
||||
.catch((err: Error) => {
|
||||
// The compiler should be able to handle errors by itself, however
|
||||
// for the rare cases where it can't let's directly throw here with as much info as possible
|
||||
throw new CompilerError({
|
||||
errorCode: AstroErrorCodes.UnknownCompilerError,
|
||||
message: err.message ?? 'Unknown compiler error',
|
||||
stack: err.stack,
|
||||
location: {
|
||||
file: filename,
|
||||
},
|
||||
});
|
||||
})
|
||||
.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) {
|
||||
case 0:
|
||||
return result;
|
||||
case 1: {
|
||||
let error = cssTransformErrors[0];
|
||||
if (!(error as any).code) {
|
||||
(error as any).code = AstroErrorCodes.UnknownCompilerCSSError;
|
||||
if (!error.errorCode) {
|
||||
error.errorCode = AstroErrorCodes.UnknownCompilerCSSError;
|
||||
}
|
||||
|
||||
throw cssTransformErrors[0];
|
||||
}
|
||||
default: {
|
||||
const aggregateError = new AggregateError(cssTransformErrors);
|
||||
(aggregateError as any).code = AstroErrorCodes.UnknownCompilerCSSError;
|
||||
throw aggregateError;
|
||||
throw new AggregateError({ ...cssTransformErrors[0], errors: cssTransformErrors });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -16,7 +16,7 @@ import legacyMarkdownVitePlugin from '../vite-plugin-markdown-legacy/index.js';
|
|||
import markdownVitePlugin from '../vite-plugin-markdown/index.js';
|
||||
import astroScriptsPlugin from '../vite-plugin-scripts/index.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';
|
||||
|
||||
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 { ResolvedServerUrls } from 'vite';
|
||||
import { ZodError } from 'zod';
|
||||
import { ErrorWithMetadata } from './errors.js';
|
||||
import { ErrorWithMetadata } from './errors/index.js';
|
||||
import { removeTrailingForwardSlash } from './path.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(yellow(padMultilineString(err.hint, 4)));
|
||||
}
|
||||
if (err.id) {
|
||||
if (err.id || err.loc?.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) {
|
||||
args.push(` ${bold('Code:')}`);
|
||||
|
|
|
@ -9,6 +9,8 @@ import type {
|
|||
SSRLoadedRenderer,
|
||||
} from '../../../@types/astro';
|
||||
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 { isPage, resolveIdToUrl } from '../../util.js';
|
||||
import { createRenderContext, renderPage as coreRenderPage } from '../index.js';
|
||||
|
@ -91,9 +93,19 @@ export async function preload({
|
|||
}: Pick<SSROptions, 'env' | 'filePath'>): Promise<ComponentPreload> {
|
||||
// Important: This needs to happen first, in case a renderer provides polyfills.
|
||||
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;
|
||||
return [renderers, mod];
|
||||
|
||||
try {
|
||||
// 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 {
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import eol from 'eol';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import resolve from 'resolve';
|
||||
|
@ -99,38 +98,6 @@ export function createSafeError(err: any): Error {
|
|||
: 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) {
|
||||
const resolved = resolve.sync(dep, {
|
||||
basedir: fileURLToPath(projectRoot),
|
||||
|
@ -256,14 +223,3 @@ export function resolvePath(specifier: string, importer: string) {
|
|||
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 { AstroErrorCodes, ErrorWithMetadata } from '../core/errors.js';
|
||||
import { AstroErrorCodes, ErrorWithMetadata } from '../core/errors/index.js';
|
||||
|
||||
const EVENT_ERROR = 'ASTRO_CLI_ERROR';
|
||||
|
||||
|
|
|
@ -7,12 +7,6 @@ import { DevelopmentEnvironment, SSROptions } from '../core/render/dev/index';
|
|||
import { Readable } from 'stream';
|
||||
import { attachToResponse, getSetCookiesFromResponse } from '../core/cookies/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 * as msg from '../core/messages.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 { resolvePages } from '../core/util.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 {
|
||||
settings: AstroSettings;
|
||||
|
@ -282,15 +278,14 @@ async function handleRequest(
|
|||
body = Buffer.concat(bytes);
|
||||
}
|
||||
|
||||
let filePath: URL | undefined;
|
||||
try {
|
||||
const matchedRoute = await matchRoute(pathname, env, manifest);
|
||||
filePath = matchedRoute?.filePath;
|
||||
|
||||
return await handleRoute(matchedRoute, url, pathname, body, origin, env, manifest, req, res);
|
||||
} catch (_err) {
|
||||
const err = fixViteErrorMessage(_err, viteServer, filePath);
|
||||
const errorWithMetadata = collectErrorMetadata(err);
|
||||
// This is our last line of defense regarding errors where we still might have some information about the request
|
||||
// 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));
|
||||
handle500Response(viteServer, origin, req, res, errorWithMetadata);
|
||||
}
|
||||
|
@ -383,7 +378,7 @@ async function handleRoute(
|
|||
await writeWebResponse(res, result.response);
|
||||
} else {
|
||||
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 =
|
||||
route.pathname ||
|
||||
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
|
||||
if (err.stack.includes('wasm-function')) {
|
||||
if (err.stack && err.stack.includes('wasm-function')) {
|
||||
const search = new URLSearchParams({
|
||||
labels: 'compiler',
|
||||
title: '🐛 BUG: `@astrojs/compiler` panic',
|
||||
|
|
|
@ -8,7 +8,7 @@ import type { Plugin, ViteDevServer } from 'vite';
|
|||
import type { AstroSettings } from '../@types/astro';
|
||||
import { pagesVirtualModuleId } from '../core/app/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 { isMarkdownFile } from '../core/util.js';
|
||||
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) {
|
||||
try {
|
||||
return matter(source);
|
||||
} catch (e) {
|
||||
(e as any).id = id;
|
||||
throw collectErrorMetadata(e);
|
||||
} catch (err: any) {
|
||||
const markdownError = new MarkdownError({
|
||||
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 { normalizePath } from 'vite';
|
||||
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 { warn } from '../core/logger/core.js';
|
||||
import { isMarkdownFile } from '../core/util.js';
|
||||
|
@ -20,9 +20,28 @@ interface AstroPluginOptions {
|
|||
function safeMatter(source: string, id: string) {
|
||||
try {
|
||||
return matter(source);
|
||||
} catch (e) {
|
||||
(e as any).id = id;
|
||||
throw collectErrorMetadata(e);
|
||||
} catch (err: any) {
|
||||
const markdownError = new MarkdownError({
|
||||
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 { createTransformStyleWithViteFn, TransformStyleWithVite } from './transform-with-vite.js';
|
||||
|
||||
import { readFileSync } from 'fs';
|
||||
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 = {
|
||||
viteDevServer?: vite.ViteDevServer;
|
||||
|
@ -35,13 +39,67 @@ export function createTransformStyles(
|
|||
const normalizedID = getNormalizedIDForPostCSS(filename);
|
||||
|
||||
return async function (styleSource, lang) {
|
||||
const result = await viteStyleTransformer.transformStyleWithVite.call(pluginContext, {
|
||||
id: normalizedID,
|
||||
source: styleSource,
|
||||
lang,
|
||||
ssr,
|
||||
viteDevServer: viteStyleTransformer.viteDevServer,
|
||||
});
|
||||
let result: any;
|
||||
try {
|
||||
result = await viteStyleTransformer.transformStyleWithVite.call(pluginContext, {
|
||||
id: normalizedID,
|
||||
source: styleSource,
|
||||
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;
|
||||
};
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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';
|
||||
|
||||
describe('Events', () => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { expect } from 'chai';
|
||||
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('Invalid CSS', () => {
|
||||
|
|
Loading…
Reference in a new issue