summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.changeset/ten-cheetahs-perform.md5
-rw-r--r--packages/astro/e2e/errors.test.js2
-rw-r--r--packages/astro/src/cli/index.ts2
-rw-r--r--packages/astro/src/core/build/index.ts4
-rw-r--r--packages/astro/src/core/compile/compile.ts50
-rw-r--r--packages/astro/src/core/create-vite.ts2
-rw-r--r--packages/astro/src/core/errors.ts209
-rw-r--r--packages/astro/src/core/errors/codes.ts24
-rw-r--r--packages/astro/src/core/errors/dev/index.ts2
-rw-r--r--packages/astro/src/core/errors/dev/utils.ts88
-rw-r--r--packages/astro/src/core/errors/dev/vite.ts108
-rw-r--r--packages/astro/src/core/errors/errors.ts160
-rw-r--r--packages/astro/src/core/errors/index.ts12
-rw-r--r--packages/astro/src/core/errors/printer.ts36
-rw-r--r--packages/astro/src/core/errors/utils.ts120
-rw-r--r--packages/astro/src/core/messages.ts12
-rw-r--r--packages/astro/src/core/render/dev/index.ts18
-rw-r--r--packages/astro/src/core/util.ts44
-rw-r--r--packages/astro/src/events/error.ts2
-rw-r--r--packages/astro/src/vite-plugin-astro-server/index.ts19
-rw-r--r--packages/astro/src/vite-plugin-astro/index.ts2
-rw-r--r--packages/astro/src/vite-plugin-markdown-legacy/index.ts27
-rw-r--r--packages/astro/src/vite-plugin-markdown/index.ts27
-rw-r--r--packages/astro/src/vite-style-transform/style-transform.ts72
-rw-r--r--packages/astro/test/events.test.js2
-rw-r--r--packages/astro/test/units/compile/invalid-css.test.js2
26 files changed, 744 insertions, 307 deletions
diff --git a/.changeset/ten-cheetahs-perform.md b/.changeset/ten-cheetahs-perform.md
new file mode 100644
index 000000000..3c7fac6e7
--- /dev/null
+++ b/.changeset/ten-cheetahs-perform.md
@@ -0,0 +1,5 @@
+---
+'astro': patch
+---
+
+Improve error messages related to CSS and compiler errors
diff --git a/packages/astro/e2e/errors.test.js b/packages/astro/e2e/errors.test.js
index c29ec0d4f..9742df49c 100644
--- a/packages/astro/e2e/errors.test.js
+++ b/packages/astro/e2e/errors.test.js
@@ -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
diff --git a/packages/astro/src/cli/index.ts b/packages/astro/src/cli/index.ts
index da05d989e..a8ead6707 100644
--- a/packages/astro/src/cli/index.ts
+++ b/packages/astro/src/cli/index.ts
@@ -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';
diff --git a/packages/astro/src/core/build/index.ts b/packages/astro/src/core/build/index.ts
index 25e6717d7..e4b93d9c1 100644
--- a/packages/astro/src/core/build/index.ts
+++ b/packages/astro/src/core/build/index.ts
@@ -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);
}
}
diff --git a/packages/astro/src/core/compile/compile.ts b/packages/astro/src/core/compile/compile.ts
index 776e7088f..f3fe01f0c 100644
--- a/packages/astro/src/core/compile/compile.ts
+++ b/packages/astro/src/core/compile/compile.ts
@@ -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 });
}
}
});
diff --git a/packages/astro/src/core/create-vite.ts b/packages/astro/src/core/create-vite.ts
index 63e780961..62dc46eb3 100644
--- a/packages/astro/src/core/create-vite.ts
+++ b/packages/astro/src/core/create-vite.ts
@@ -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 {
diff --git a/packages/astro/src/core/errors.ts b/packages/astro/src/core/errors.ts
deleted file mode 100644
index 06940276b..000000000
--- a/packages/astro/src/core/errors.ts
+++ /dev/null
@@ -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,
- },
- };
-}
diff --git a/packages/astro/src/core/errors/codes.ts b/packages/astro/src/core/errors/codes.ts
new file mode 100644
index 000000000..24270bed2
--- /dev/null
+++ b/packages/astro/src/core/errors/codes.ts
@@ -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,
+}
diff --git a/packages/astro/src/core/errors/dev/index.ts b/packages/astro/src/core/errors/dev/index.ts
new file mode 100644
index 000000000..93cd41385
--- /dev/null
+++ b/packages/astro/src/core/errors/dev/index.ts
@@ -0,0 +1,2 @@
+export { collectErrorMetadata } from './utils.js';
+export { createCustomViteLogger, enhanceViteSSRError, getViteErrorPayload } from './vite.js';
diff --git a/packages/astro/src/core/errors/dev/utils.ts b/packages/astro/src/core/errors/dev/utils.ts
new file mode 100644
index 000000000..44aed1889
--- /dev/null
+++ b/packages/astro/src/core/errors/dev/utils.ts
@@ -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;
+}
diff --git a/packages/astro/src/core/errors/dev/vite.ts b/packages/astro/src/core/errors/dev/vite.ts
new file mode 100644
index 000000000..4a187c4f8
--- /dev/null
+++ b/packages/astro/src/core/errors/dev/vite.ts
@@ -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,
+ },
+ };
+}
diff --git a/packages/astro/src/core/errors/errors.ts b/packages/astro/src/core/errors/errors.ts
new file mode 100644
index 000000000..ff4552ffd
--- /dev/null
+++ b/packages/astro/src/core/errors/errors.ts
@@ -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;
+ };
+}
diff --git a/packages/astro/src/core/errors/index.ts b/packages/astro/src/core/errors/index.ts
new file mode 100644
index 000000000..c41c43933
--- /dev/null
+++ b/packages/astro/src/core/errors/index.ts
@@ -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';
diff --git a/packages/astro/src/core/errors/printer.ts b/packages/astro/src/core/errors/printer.ts
new file mode 100644
index 000000000..65db14940
--- /dev/null
+++ b/packages/astro/src/core/errors/printer.ts
@@ -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;
+}
diff --git a/packages/astro/src/core/errors/utils.ts b/packages/astro/src/core/errors/utils.ts
new file mode 100644
index 000000000..471283f11
--- /dev/null
+++ b/packages/astro/src/core/errors/utils.ts
@@ -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;
+}
diff --git a/packages/astro/src/core/messages.ts b/packages/astro/src/core/messages.ts
index 278f8ac9f..adb62fc39 100644
--- a/packages/astro/src/core/messages.ts
+++ b/packages/astro/src/core/messages.ts
@@ -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:')}`);
diff --git a/packages/astro/src/core/render/dev/index.ts b/packages/astro/src/core/render/dev/index.ts
index 72c55766d..727007c35 100644
--- a/packages/astro/src/core/render/dev/index.ts
+++ b/packages/astro/src/core/render/dev/index.ts
@@ -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 {
diff --git a/packages/astro/src/core/util.ts b/packages/astro/src/core/util.ts
index 17adce83f..482b7735f 100644
--- a/packages/astro/src/core/util.ts
+++ b/packages/astro/src/core/util.ts
@@ -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);
- }
- };
diff --git a/packages/astro/src/events/error.ts b/packages/astro/src/events/error.ts
index fa9778b44..f0425098c 100644
--- a/packages/astro/src/events/error.ts
+++ b/packages/astro/src/events/error.ts
@@ -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';
diff --git a/packages/astro/src/vite-plugin-astro-server/index.ts b/packages/astro/src/vite-plugin-astro-server/index.ts
index 5ab23e809..98afcef3f 100644
--- a/packages/astro/src/vite-plugin-astro-server/index.ts
+++ b/packages/astro/src/vite-plugin-astro-server/index.ts
@@ -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('/');
diff --git a/packages/astro/src/vite-plugin-astro/index.ts b/packages/astro/src/vite-plugin-astro/index.ts
index 7d4c5d762..5c300b9e3 100644
--- a/packages/astro/src/vite-plugin-astro/index.ts
+++ b/packages/astro/src/vite-plugin-astro/index.ts
@@ -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',
diff --git a/packages/astro/src/vite-plugin-markdown-legacy/index.ts b/packages/astro/src/vite-plugin-markdown-legacy/index.ts
index 0feb3a517..e016d46a0 100644
--- a/packages/astro/src/vite-plugin-markdown-legacy/index.ts
+++ b/packages/astro/src/vite-plugin-markdown-legacy/index.ts
@@ -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;
}
}
diff --git a/packages/astro/src/vite-plugin-markdown/index.ts b/packages/astro/src/vite-plugin-markdown/index.ts
index 4c9055e6b..6a4e62b9f 100644
--- a/packages/astro/src/vite-plugin-markdown/index.ts
+++ b/packages/astro/src/vite-plugin-markdown/index.ts
@@ -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;
}
}
diff --git a/packages/astro/src/vite-style-transform/style-transform.ts b/packages/astro/src/vite-style-transform/style-transform.ts
index 6c63158af..ffa3ea57d 100644
--- a/packages/astro/src/vite-style-transform/style-transform.ts
+++ b/packages/astro/src/vite-style-transform/style-transform.ts
@@ -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;
};
diff --git a/packages/astro/test/events.test.js b/packages/astro/test/events.test.js
index f447d5d80..7e355d6aa 100644
--- a/packages/astro/test/events.test.js
+++ b/packages/astro/test/events.test.js
@@ -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', () => {
diff --git a/packages/astro/test/units/compile/invalid-css.test.js b/packages/astro/test/units/compile/invalid-css.test.js
index 00d4fb7f6..0ea6f77a1 100644
--- a/packages/astro/test/units/compile/invalid-css.test.js
+++ b/packages/astro/test/units/compile/invalid-css.test.js
@@ -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', () => {