summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.changeset/happy-chefs-ring.md5
-rw-r--r--packages/astro/e2e/error-cyclic.test.js9
-rw-r--r--packages/astro/e2e/error-react-spectrum.test.js9
-rw-r--r--packages/astro/e2e/error-sass.test.js9
-rw-r--r--packages/astro/e2e/errors.test.js17
-rw-r--r--packages/astro/e2e/shared-component-tests.js2
-rw-r--r--packages/astro/e2e/test-utils.js12
-rw-r--r--packages/astro/src/@types/astro.ts7
-rw-r--r--packages/astro/src/core/compile/style.ts3
-rw-r--r--packages/astro/src/core/config/config.ts2
-rw-r--r--packages/astro/src/core/config/schema.ts2
-rw-r--r--packages/astro/src/core/errors/dev/utils.ts79
-rw-r--r--packages/astro/src/core/errors/dev/vite.ts77
-rw-r--r--packages/astro/src/core/errors/errors-data.ts4
-rw-r--r--packages/astro/src/core/errors/errors.ts8
-rw-r--r--packages/astro/src/core/errors/overlay.ts585
-rw-r--r--packages/astro/src/core/messages.ts17
-rw-r--r--packages/astro/src/vite-plugin-astro-server/plugin.ts22
-rw-r--r--packages/astro/src/vite-plugin-astro-server/response.ts4
19 files changed, 800 insertions, 73 deletions
diff --git a/.changeset/happy-chefs-ring.md b/.changeset/happy-chefs-ring.md
new file mode 100644
index 000000000..c8ac61ddb
--- /dev/null
+++ b/.changeset/happy-chefs-ring.md
@@ -0,0 +1,5 @@
+---
+'astro': minor
+---
+
+Add a new error overlay designed by @doodlemarks! This new overlay should be much more informative, clearer, astro-y, and prettier than the previous one.
diff --git a/packages/astro/e2e/error-cyclic.test.js b/packages/astro/e2e/error-cyclic.test.js
index 78c3bd1ea..ef17a32d3 100644
--- a/packages/astro/e2e/error-cyclic.test.js
+++ b/packages/astro/e2e/error-cyclic.test.js
@@ -1,7 +1,10 @@
import { expect } from '@playwright/test';
-import { testFactory, getErrorOverlayMessage } from './test-utils.js';
+import { testFactory, getErrorOverlayContent } from './test-utils.js';
-const test = testFactory({ root: './fixtures/error-cyclic/' });
+const test = testFactory({
+ experimentalErrorOverlay: true,
+ root: './fixtures/error-cyclic/'
+});
let devServer;
@@ -18,7 +21,7 @@ test.describe('Error: Cyclic Reference', () => {
test('overlay', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/'));
- const message = await getErrorOverlayMessage(page);
+ const message = (await getErrorOverlayContent(page)).message;
expect(message).toMatch('Cyclic reference');
});
});
diff --git a/packages/astro/e2e/error-react-spectrum.test.js b/packages/astro/e2e/error-react-spectrum.test.js
index 63934c9ca..618859ac1 100644
--- a/packages/astro/e2e/error-react-spectrum.test.js
+++ b/packages/astro/e2e/error-react-spectrum.test.js
@@ -1,7 +1,10 @@
import { expect } from '@playwright/test';
-import { testFactory, getErrorOverlayMessage } from './test-utils.js';
+import { testFactory, getErrorOverlayContent } from './test-utils.js';
-const test = testFactory({ root: './fixtures/error-react-spectrum/' });
+const test = testFactory({
+ experimentalErrorOverlay: true,
+ root: './fixtures/error-react-spectrum/'
+});
let devServer;
@@ -17,7 +20,7 @@ test.describe('Error: React Spectrum', () => {
test('overlay', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/'));
- const message = await getErrorOverlayMessage(page);
+ const message = (await getErrorOverlayContent(page)).hint;
expect(message).toMatch('@adobe/react-spectrum is not compatible');
});
});
diff --git a/packages/astro/e2e/error-sass.test.js b/packages/astro/e2e/error-sass.test.js
index e7b87e105..2eeab13df 100644
--- a/packages/astro/e2e/error-sass.test.js
+++ b/packages/astro/e2e/error-sass.test.js
@@ -1,7 +1,10 @@
import { expect } from '@playwright/test';
-import { testFactory, getErrorOverlayMessage } from './test-utils.js';
+import { testFactory, getErrorOverlayContent } from './test-utils.js';
-const test = testFactory({ root: './fixtures/error-sass/' });
+const test = testFactory({
+ experimentalErrorOverlay: true,
+ root: './fixtures/error-sass/'
+});
let devServer;
@@ -18,7 +21,7 @@ test.describe('Error: Sass', () => {
test('overlay', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/'));
- const message = await getErrorOverlayMessage(page);
+ const message = (await getErrorOverlayContent(page)).message;
expect(message).toMatch('Undefined variable');
});
});
diff --git a/packages/astro/e2e/errors.test.js b/packages/astro/e2e/errors.test.js
index c9aff5269..34f3c59ad 100644
--- a/packages/astro/e2e/errors.test.js
+++ b/packages/astro/e2e/errors.test.js
@@ -1,7 +1,10 @@
import { expect } from '@playwright/test';
-import { getErrorOverlayMessage, testFactory } from './test-utils.js';
+import { getErrorOverlayContent, testFactory } from './test-utils.js';
-const test = testFactory({ root: './fixtures/errors/' });
+const test = testFactory({
+ experimentalErrorOverlay: true,
+ root: './fixtures/errors/'
+});
let devServer;
@@ -18,7 +21,7 @@ test.describe('Error display', () => {
test('detect syntax errors in template', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/astro-syntax-error'));
- const message = await getErrorOverlayMessage(page);
+ const message = (await getErrorOverlayContent(page)).message;
expect(message).toMatch('Unexpected "}"');
await Promise.all([
@@ -37,10 +40,8 @@ test.describe('Error display', () => {
test('shows useful error when frontmatter import is not found', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/import-not-found'));
- const message = await getErrorOverlayMessage(page);
- expect(message).toMatch(
- 'Could not import `../abc.astro`.\n\nThis is often caused by a typo in the import path. Please make sure the file exists.'
- );
+ const message = (await getErrorOverlayContent(page)).message;
+ expect(message).toMatch('Could not import ../abc.astro');
await Promise.all([
// Wait for page reload
@@ -55,7 +56,7 @@ test.describe('Error display', () => {
test('framework errors recover when fixed', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/svelte-syntax-error'));
- const message = await getErrorOverlayMessage(page);
+ const message = (await getErrorOverlayContent(page)).message;
expect(message).toMatch('</div> attempted to close an element that was not open');
await Promise.all([
diff --git a/packages/astro/e2e/shared-component-tests.js b/packages/astro/e2e/shared-component-tests.js
index db8620835..92423c1c0 100644
--- a/packages/astro/e2e/shared-component-tests.js
+++ b/packages/astro/e2e/shared-component-tests.js
@@ -1,5 +1,5 @@
import { expect } from '@playwright/test';
-import { testFactory, getErrorOverlayMessage } from './test-utils.js';
+import { testFactory } from './test-utils.js';
export function prepareTestFactory(opts) {
const test = testFactory(opts);
diff --git a/packages/astro/e2e/test-utils.js b/packages/astro/e2e/test-utils.js
index 2a8651fd5..b75efeec4 100644
--- a/packages/astro/e2e/test-utils.js
+++ b/packages/astro/e2e/test-utils.js
@@ -32,7 +32,12 @@ export function testFactory(inlineConfig) {
return test;
}
-export async function getErrorOverlayMessage(page) {
+/**
+ *
+ * @param {string} page
+ * @returns {Promise<{message: string, hint: string}>}
+ */
+export async function getErrorOverlayContent(page) {
const overlay = await page.waitForSelector('vite-error-overlay', {
strict: true,
timeout: 10 * 1000,
@@ -40,7 +45,10 @@ export async function getErrorOverlayMessage(page) {
expect(overlay).toBeTruthy();
- return await overlay.$$eval('.message-body', (m) => m[0].textContent);
+ const message = await overlay.$$eval('#message-content', (m) => m[0].textContent);
+ const hint = await overlay.$$eval('#hint-content', (m) => m[0].textContent);
+
+ return { message, hint };
}
/**
diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts
index 1b6c98ea5..55620c745 100644
--- a/packages/astro/src/@types/astro.ts
+++ b/packages/astro/src/@types/astro.ts
@@ -82,6 +82,7 @@ export interface CLIFlags {
port?: number;
config?: string;
drafts?: boolean;
+ experimentalErrorOverlay?: boolean;
}
export interface BuildConfig {
@@ -894,6 +895,12 @@ export interface AstroUserConfig {
astroFlavoredMarkdown?: boolean;
};
+ /**
+ * @hidden
+ * Turn on experimental support for the new error overlay component.
+ */
+ experimentalErrorOverlay?: boolean;
+
// Legacy options to be removed
/** @deprecated - Use "integrations" instead. Run Astro to learn more about migrating. */
diff --git a/packages/astro/src/core/compile/style.ts b/packages/astro/src/core/compile/style.ts
index 9f32ad3a6..b9a5bbcfa 100644
--- a/packages/astro/src/core/compile/style.ts
+++ b/packages/astro/src/core/compile/style.ts
@@ -62,6 +62,7 @@ function enhanceCSSError(err: any, filename: string) {
line: errorLine,
column: err.column,
},
+ stack: err.stack,
});
}
@@ -78,6 +79,7 @@ function enhanceCSSError(err: any, filename: string) {
column: err.column,
},
frame: err.frame,
+ stack: err.stack,
});
}
@@ -94,5 +96,6 @@ function enhanceCSSError(err: any, filename: string) {
column: 0,
},
frame: err.frame,
+ stack: err.stack,
});
}
diff --git a/packages/astro/src/core/config/config.ts b/packages/astro/src/core/config/config.ts
index 25efc0306..864a13eaf 100644
--- a/packages/astro/src/core/config/config.ts
+++ b/packages/astro/src/core/config/config.ts
@@ -100,6 +100,7 @@ export function resolveFlags(flags: Partial<Flags>): CLIFlags {
host:
typeof flags.host === 'string' || typeof flags.host === 'boolean' ? flags.host : undefined,
drafts: typeof flags.drafts === 'boolean' ? flags.drafts : undefined,
+ experimentalErrorOverlay: typeof flags.experimentalErrorOverlay === 'boolean' ? flags.experimentalErrorOverlay : undefined,
};
}
@@ -127,6 +128,7 @@ function mergeCLIFlags(astroConfig: AstroUserConfig, flags: CLIFlags, cmd: strin
// TODO: Come back here and refactor to remove this expected error.
astroConfig.server.host = flags.host;
}
+ astroConfig.experimentalErrorOverlay = flags.experimentalErrorOverlay ?? false;
return astroConfig;
}
diff --git a/packages/astro/src/core/config/schema.ts b/packages/astro/src/core/config/schema.ts
index 9e5fc377b..78a0ed60c 100644
--- a/packages/astro/src/core/config/schema.ts
+++ b/packages/astro/src/core/config/schema.ts
@@ -47,6 +47,7 @@ const ASTRO_CONFIG_DEFAULTS: AstroUserConfig & any = {
legacy: {
astroFlavoredMarkdown: false,
},
+ experimentalErrorOverlay: false,
};
export const AstroConfigSchema = z.object({
@@ -196,6 +197,7 @@ export const AstroConfigSchema = z.object({
})
.optional()
.default({}),
+ experimentalErrorOverlay: z.boolean().optional().default(false),
});
interface PostCSSConfigResult {
diff --git a/packages/astro/src/core/errors/dev/utils.ts b/packages/astro/src/core/errors/dev/utils.ts
index 7ee90115c..9843f3b0f 100644
--- a/packages/astro/src/core/errors/dev/utils.ts
+++ b/packages/astro/src/core/errors/dev/utils.ts
@@ -1,12 +1,15 @@
import * as fs from 'node:fs';
-import { join } from 'node:path';
+import { isAbsolute, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import stripAnsi from 'strip-ansi';
+import { escape } from 'html-escaper';
+import type { BuildResult } from 'esbuild';
import type { ESBuildTransformResult } from 'vite';
import type { SSRError } from '../../../@types/astro.js';
import { AggregateError, ErrorWithMetadata } from '../errors.js';
import { codeFrame } from '../printer.js';
import { normalizeLF } from '../utils.js';
+import { bold, underline } from 'kleur/colors';
type EsbuildMessage = ESBuildTransformResult['warnings'][number];
@@ -30,16 +33,32 @@ export function collectErrorMetadata(e: any, rootFolder?: URL | undefined): Erro
error = collectInfoFromStacktrace(e);
}
- if (error.loc?.file && rootFolder && !error.loc.file.startsWith('/')) {
+ // Make sure the file location is absolute, otherwise:
+ // - It won't be clickable in the terminal
+ // - We'll fail to show the file's content in the browser
+ // - We'll fail to show the code frame in the terminal
+ // - The "Open in Editor" button won't work
+ if (
+ error.loc?.file &&
+ rootFolder &&
+ (!error.loc.file.startsWith(rootFolder.pathname) || !isAbsolute(error.loc.file))
+ ) {
error.loc.file = join(fileURLToPath(rootFolder), error.loc.file);
}
// 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) {
+ if (error.loc && (!error.frame || !error.fullCode)) {
try {
const fileContents = fs.readFileSync(error.loc.file!, 'utf8');
- const frame = codeFrame(fileContents, error.loc);
- error.frame = frame;
+
+ if (!error.frame) {
+ const frame = codeFrame(fileContents, error.loc);
+ error.frame = frame;
+ }
+
+ if (!error.fullCode) {
+ error.fullCode = fileContents;
+ }
} catch {}
}
@@ -47,13 +66,16 @@ export function collectErrorMetadata(e: any, rootFolder?: URL | undefined): Erro
error.hint = generateHint(e);
});
- // 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 we received an array of errors and it's not from us, it's most likely from ESBuild, try to extract info for Vite to display
+ // NOTE: We still need to be defensive here, because it might not necessarily be from ESBuild, it's just fairly likely.
if (!AggregateError.is(e) && Array.isArray((e as any).errors)) {
(e.errors as EsbuildMessage[]).forEach((buildError, i) => {
const { location, pluginName, text } = buildError;
// ESBuild can give us a slightly better error message than the one in the error, so let's use it
- err[i].message = text;
+ if (text) {
+ err[i].message = text;
+ }
if (location) {
err[i].loc = { file: location.file, line: location.line, column: location.column };
@@ -71,13 +93,17 @@ export function collectErrorMetadata(e: any, rootFolder?: URL | undefined): Erro
}
}
- const possibleFilePath = err[i].pluginCode || err[i].id || location?.file;
- if (possibleFilePath && !err[i].frame) {
+ const possibleFilePath = location?.file ?? err[i].id;
+ if (possibleFilePath && err[i].loc && (!err[i].frame || !err[i].fullCode)) {
try {
const fileContents = fs.readFileSync(possibleFilePath, 'utf8');
- err[i].frame = codeFrame(fileContents, { ...err[i].loc, file: possibleFilePath });
+ if (!err[i].frame) {
+ err[i].frame = codeFrame(fileContents, { ...err[i].loc, file: possibleFilePath });
+ }
+
+ err[i].fullCode = fileContents;
} catch {
- // do nothing, code frame isn't that big a deal
+ err[i].fullCode = err[i].pluginCode;
}
}
@@ -94,15 +120,17 @@ export function collectErrorMetadata(e: any, rootFolder?: URL | undefined): Erro
}
function generateHint(err: ErrorWithMetadata): string | undefined {
+ const commonBrowserAPIs = ['document', 'window'];
+
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().includes('document')) {
+ } else if (commonBrowserAPIs.some((api) => err.toString().includes(api))) {
const hint = `Browser APIs are not available on the server.
${
err.loc?.file?.endsWith('.astro')
- ? 'Move your code to a <script> tag outside of the frontmatter, so the code runs on the client'
- : 'If the code is in a framework component, try to access these objects after rendering using lifecycle methods or use a `client:only` directive to make the component exclusively run on the client'
+ ? 'Move your code to a <script> tag outside of the frontmatter, so the code runs on the client.'
+ : 'If the code is in a framework component, try to access these objects after rendering using lifecycle methods or use a `client:only` directive to make the component exclusively run on the client.'
}
See https://docs.astro.build/en/guides/troubleshooting/#document-or-window-is-not-defined for more information.
@@ -173,3 +201,26 @@ function cleanErrorStack(stack: string) {
.map((l) => l.replace(/\/@fs\//g, '/'))
.join('\n');
}
+
+/**
+ * Render a subset of Markdown to HTML or a CLI output
+ */
+export function renderErrorMarkdown(markdown: string, target: 'html' | 'cli') {
+ const linkRegex = /\[(.+)\]\((.+)\)/gm;
+ const boldRegex = /\*\*(.+)\*\*/gm;
+ const urlRegex = / (\b(https?|ftp):\/\/[-A-Z0-9+&@#\\/%?=~_|!:,.;]*[-A-Z0-9+&@#\\/%=~_|]) /gim;
+ const codeRegex = /`([^`]+)`/gim;
+
+ if (target === 'html') {
+ return escape(markdown)
+ .replace(linkRegex, `<a href="$2" target="_blank">$1</a>`)
+ .replace(boldRegex, '<b>$1</b>')
+ .replace(urlRegex, ' <a href="$1" target="_blank">$1</a> ')
+ .replace(codeRegex, '<code>$1</code>');
+ } else {
+ return markdown
+ .replace(linkRegex, (fullMatch, m1, m2) => `${bold(m1)} ${underline(m2)}`)
+ .replace(urlRegex, (fullMatch, m1) => ` ${underline(fullMatch.trim())} `)
+ .replace(boldRegex, (fullMatch, m1) => `${bold(m1)}`);
+ }
+}
diff --git a/packages/astro/src/core/errors/dev/vite.ts b/packages/astro/src/core/errors/dev/vite.ts
index 88c947e5f..d0dcb1ea7 100644
--- a/packages/astro/src/core/errors/dev/vite.ts
+++ b/packages/astro/src/core/errors/dev/vite.ts
@@ -1,11 +1,12 @@
import * as fs from 'fs';
+import { getHighlighter } from 'shiki';
import { fileURLToPath } from 'url';
import { createLogger, type ErrorPayload, type Logger, type LogLevel } from 'vite';
import type { ModuleLoader } from '../../module-loader/index.js';
import { AstroErrorData } from '../errors-data.js';
import { type ErrorWithMetadata } from '../errors.js';
import { createSafeError } from '../utils.js';
-import { incompatPackageExp } from './utils.js';
+import { incompatPackageExp, renderErrorMarkdown } from './utils.js';
/**
* Custom logger with better error reporting for incompatible packages
@@ -46,6 +47,8 @@ export function enhanceViteSSRError(error: unknown, filePath?: URL, loader?: Mod
if (/failed to load module for ssr:/.test(safeError.message)) {
const importName = safeError.message.split('for ssr:').at(1)?.trim();
if (importName) {
+ safeError.title = AstroErrorData.FailedToLoadModuleSSR.title;
+ safeError.name = 'FailedToLoadModuleSSR';
safeError.message = AstroErrorData.FailedToLoadModuleSSR.message(importName);
safeError.hint = AstroErrorData.FailedToLoadModuleSSR.hint;
safeError.code = AstroErrorData.FailedToLoadModuleSSR.code;
@@ -69,8 +72,10 @@ export function enhanceViteSSRError(error: unknown, filePath?: URL, loader?: Mod
if (globPattern) {
safeError.message = AstroErrorData.InvalidGlob.message(globPattern);
+ safeError.name = 'InvalidGlob';
safeError.hint = AstroErrorData.InvalidGlob.hint;
safeError.code = AstroErrorData.InvalidGlob.code;
+ safeError.title = AstroErrorData.InvalidGlob.title;
const line = lns.findIndex((ln) => ln.includes(globPattern));
@@ -90,31 +95,83 @@ export function enhanceViteSSRError(error: unknown, filePath?: URL, loader?: Mod
return safeError;
}
+export interface AstroErrorPayload {
+ type: ErrorPayload['type'];
+ err: Omit<ErrorPayload['err'], 'loc'> & {
+ name?: string;
+ title?: string;
+ hint?: string;
+ docslink?: string;
+ highlightedCode?: string;
+ loc: {
+ file?: string;
+ line?: number;
+ column?: number;
+ };
+ };
+}
+
/**
* Generate a payload for Vite's error overlay
*/
-export function getViteErrorPayload(err: ErrorWithMetadata): ErrorPayload {
+export async function getViteErrorPayload(err: ErrorWithMetadata): Promise<AstroErrorPayload> {
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, ' ');
+
+ const message = renderErrorMarkdown(err.message.trim(), 'html');
+ const hint = err.hint ? renderErrorMarkdown(err.hint.trim(), 'html') : undefined;
+
+ const hasDocs =
+ (err.type &&
+ err.name && [
+ 'AstroError',
+ 'AggregateError',
+ /* 'CompilerError' ,*/
+ 'CSSError',
+ 'MarkdownError',
+ ]) ||
+ ['FailedToLoadModuleSSR', 'InvalidGlob'].includes(err.name);
+
+ const docslink = hasDocs
+ ? `https://docs.astro.build/en/reference/errors/${getKebabErrorName(err.name)}/`
+ : undefined;
+
+ const highlighter = await getHighlighter({ theme: 'css-variables' });
+ const highlightedCode = err.fullCode
+ ? highlighter.codeToHtml(err.fullCode, {
+ lang: err.loc?.file?.split('.').pop(),
+ lineOptions: err.loc?.line ? [{ line: err.loc.line, classes: ['error-line'] }] : undefined,
+ })
+ : undefined;
+
return {
type: 'error',
err: {
...err,
- frame: frame,
+ name: err.name,
+ type: err.type,
+ message,
+ hint,
+ frame: err.frame,
+ highlightedCode,
+ docslink,
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,
+ line: err.loc?.line,
+ column: err.loc?.column,
},
plugin,
- message: message.trim(),
stack: err.stack,
},
};
+
+ /**
+ * The docs has kebab-case urls for errors, so we need to convert the error name
+ * @param errorName
+ */
+ function getKebabErrorName(errorName: string): string {
+ return errorName.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
+ }
}
diff --git a/packages/astro/src/core/errors/errors-data.ts b/packages/astro/src/core/errors/errors-data.ts
index 8e8f41c43..62ccbfd68 100644
--- a/packages/astro/src/core/errors/errors-data.ts
+++ b/packages/astro/src/core/errors/errors-data.ts
@@ -109,9 +109,9 @@ export const AstroErrorData = defineErrors({
title: 'Invalid type returned by Astro page.',
code: 3005,
message: (route: string | undefined, returnedValue: string) =>
- `Route ${
+ `Route \`${
route ? route : ''
- } returned a \`${returnedValue}\`. Only a Response can be returned from Astro files.`,
+ }\` returned a \`${returnedValue}\`. Only a [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) can be returned from Astro files.`,
hint: 'See https://docs.astro.build/en/guides/server-side-rendering/#response for more information.',
},
/**
diff --git a/packages/astro/src/core/errors/errors.ts b/packages/astro/src/core/errors/errors.ts
index 89bf3be6b..712528329 100644
--- a/packages/astro/src/core/errors/errors.ts
+++ b/packages/astro/src/core/errors/errors.ts
@@ -46,7 +46,7 @@ export class AstroError extends Error {
const { code, name, title, message, stack, location, hint, frame } = props;
this.errorCode = code;
- if (name) {
+ if (name && name !== 'Error') {
this.name = name;
} else {
// If we don't have a name, let's generate one from the code
@@ -63,8 +63,6 @@ export class AstroError extends Error {
public setErrorCode(errorCode: AstroErrorCodes) {
this.errorCode = errorCode;
-
- this.name = getErrorDataByCode(this.errorCode)?.name ?? 'UnknownError';
}
public setLocation(location: ErrorLocation): void {
@@ -154,15 +152,17 @@ export class AggregateError extends AstroError {
export interface ErrorWithMetadata {
[name: string]: any;
name: string;
+ title?: string;
type?: ErrorTypes;
message: string;
stack: string;
- code?: number;
+ errorCode?: number;
hint?: string;
id?: string;
frame?: string;
plugin?: string;
pluginCode?: string;
+ fullCode?: string;
loc?: {
file?: string;
line?: number;
diff --git a/packages/astro/src/core/errors/overlay.ts b/packages/astro/src/core/errors/overlay.ts
new file mode 100644
index 000000000..159a095f4
--- /dev/null
+++ b/packages/astro/src/core/errors/overlay.ts
@@ -0,0 +1,585 @@
+import type { AstroConfig } from '../../@types/astro';
+import type { AstroErrorPayload } from './dev/vite';
+
+const style = /* css */ `
+* {
+ box-sizing: border-box;
+}
+
+:host {
+ /** Needed so Playwright can find the element */
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 99999;
+
+ /* Fonts */
+ --font-normal: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
+ "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans",
+ "Helvetica Neue", Arial, sans-serif;
+ --font-monospace: ui-monospace, Menlo, Monaco, "Cascadia Mono",
+ "Segoe UI Mono", "Roboto Mono", "Oxygen Mono", "Ubuntu Monospace",
+ "Source Code Pro", "Fira Mono", "Droid Sans Mono", "Courier New", monospace;
+
+ /* Borders */
+ --roundiness: 4px;
+
+ /* Colors */
+ --background: #ffffff;
+ --error-text: #ba1212;
+ --error-text-hover: #a10000;
+ --title-text: #090b11;
+ --box-background: #f3f4f7;
+ --box-background-hover: #dadbde;
+ --hint-text: #505d84;
+ --hint-text-hover: #37446b;
+ --border: #c3cadb;
+ --accent: #5f11a6;
+ --accent-hover: #792bc0;
+ --stack-text: #3d4663;
+ --misc-text: #6474a2;
+
+ --houston-overlay: linear-gradient(
+ 180deg,
+ rgba(255, 255, 255, 0) 3.95%,
+ rgba(255, 255, 255, 0.0086472) 9.68%,
+ rgba(255, 255, 255, 0.03551) 15.4%,
+ rgba(255, 255, 255, 0.0816599) 21.13%,
+ rgba(255, 255, 255, 0.147411) 26.86%,
+ rgba(255, 255, 255, 0.231775) 32.58%,
+ rgba(255, 255, 255, 0.331884) 38.31%,
+ rgba(255, 255, 255, 0.442691) 44.03%,
+ rgba(255, 255, 255, 0.557309) 49.76%,
+ rgba(255, 255, 255, 0.668116) 55.48%,
+ rgba(255, 255, 255, 0.768225) 61.21%,
+ rgba(255, 255, 255, 0.852589) 66.93%,
+ rgba(255, 255, 255, 0.91834) 72.66%,
+ rgba(255, 255, 255, 0.96449) 78.38%,
+ rgba(255, 255, 255, 0.991353) 84.11%,
+ #ffffff 89.84%
+ );
+
+ /* Syntax Highlighting */
+ --shiki-color-text: #000000;
+ --shiki-token-constant: #4ca48f;
+ --shiki-token-string: #9f722a;
+ --shiki-token-comment: #8490b5;
+ --shiki-token-keyword: var(--accent);
+ --shiki-token-parameter: #aa0000;
+ --shiki-token-function: #4ca48f;
+ --shiki-token-string-expression: #9f722a;
+ --shiki-token-punctuation: #ffffff;
+ --shiki-token-link: #ee0000;
+}
+
+@media (prefers-color-scheme: dark) {
+ :host {
+ --background: #090b11;
+ --error-text: #f49090;
+ --error-text-hover: #ffaaaa;
+ --title-text: #ffffff;
+ --box-background: #141925;
+ --box-background-hover: #2e333f;
+ --hint-text: #a3acc8;
+ --hint-text-hover: #bdc6e2;
+ --border: #283044;
+ --accent: #c490f4;
+ --accent-hover: #deaaff;
+ --stack-text: #c3cadb;
+ --misc-text: #8490b5;
+
+ --houston-overlay: linear-gradient(
+ 180deg,
+ rgba(9, 11, 17, 0) 3.95%,
+ rgba(9, 11, 17, 0.0086472) 9.68%,
+ rgba(9, 11, 17, 0.03551) 15.4%,
+ rgba(9, 11, 17, 0.0816599) 21.13%,
+ rgba(9, 11, 17, 0.147411) 26.86%,
+ rgba(9, 11, 17, 0.231775) 32.58%,
+ rgba(9, 11, 17, 0.331884) 38.31%,
+ rgba(9, 11, 17, 0.442691) 44.03%,
+ rgba(9, 11, 17, 0.557309) 49.76%,
+ rgba(9, 11, 17, 0.668116) 55.48%,
+ rgba(9, 11, 17, 0.768225) 61.21%,
+ rgba(9, 11, 17, 0.852589) 66.93%,
+ rgba(9, 11, 17, 0.91834) 72.66%,
+ rgba(9, 11, 17, 0.96449) 78.38%,
+ rgba(9, 11, 17, 0.991353) 84.11%,
+ #090b11 89.84%
+ );
+
+ /* Syntax Highlighting */
+ --shiki-color-text: #ffffff;
+ --shiki-token-constant: #90f4e3;
+ --shiki-token-string: #f4cf90;
+ --shiki-token-comment: #8490b5;
+ --shiki-token-keyword: var(--accent);
+ --shiki-token-parameter: #aa0000;
+ --shiki-token-function: #90f4e3;
+ --shiki-token-string-expression: #f4cf90;
+ --shiki-token-punctuation: #ffffff;
+ --shiki-token-link: #ee0000;
+ }
+}
+
+#backdrop {
+ font-family: var(--font-monospace);
+ position: fixed;
+ z-index: 99999;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: var(--background);
+ overflow-y: auto;
+}
+
+#layout {
+ max-width: min(100%, 1280px);
+ width: 1280px;
+ margin: 0 auto;
+ padding: 40px;
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+}
+
+@media (max-width: 768px) {
+ #header {
+ padding: 12px;
+ margin-top: 12px;
+ }
+
+ #layout {
+ padding: 0;
+ }
+}
+
+@media (max-width: 1024px) {
+ #houston,
+ #houston-overlay {
+ display: none;
+ }
+}
+
+#header {
+ position: relative;
+ margin-top: 48px;
+}
+
+#header-left {
+ min-height: 63px;
+ display: flex;
+ flex-direction: column;
+ justify-content: end;
+}
+
+#name {
+ font-size: 18px;
+ font-weight: normal;
+ line-height: 22px;
+ color: var(--error-text);
+ margin: 0;
+ padding: 0;
+}
+
+#title {
+ font-size: 34px;
+ line-height: 41px;
+ font-weight: 600;
+ margin: 0;
+ padding: 0;
+ color: var(--title-text);
+ font-family: var(--font-normal);
+}
+
+#houston {
+ position: absolute;
+ bottom: -50px;
+ right: 32px;
+ z-index: -50;
+ color: var(--error-text);
+}
+
+#houston-overlay {
+ width: 175px;
+ height: 250px;
+ position: absolute;
+ bottom: -100px;
+ right: 32px;
+ z-index: -25;
+ background: var(--houston-overlay);
+}
+
+#message-hints,
+#stack,
+#code {
+ border-radius: var(--roundiness);
+ background-color: var(--box-background);
+}
+
+#message,
+#hint {
+ display: flex;
+ padding: 16px;
+ gap: 16px;
+}
+
+#message-content,
+#hint-content {
+ white-space: pre-wrap;
+ line-height: 24px;
+ flex-grow: 1;
+}
+
+#message {
+ color: var(--error-text);
+}
+
+#message-content a {
+ color: var(--error-text);
+}
+
+#message-content a:hover {
+ color: var(--error-text-hover);
+}
+
+#hint {
+ color: var(--hint-text);
+ border-top: 1px solid var(--border);
+ display: none;
+}
+
+#hint a {
+ color: var(--hint-text);
+}
+
+#hint a:hover {
+ color: var(--hint-text-hover);
+}
+
+#message-hints code {
+ font-family: var(--font-monospace);
+ background-color: var(--border);
+ padding: 4px;
+ border-radius: var(--roundiness);
+}
+
+.link {
+ min-width: fit-content;
+ padding-right: 8px;
+ padding-top: 8px;
+}
+
+.link button {
+ background: none;
+ border: none;
+ font-size: inherit;
+ font-family: inherit;
+}
+
+.link a, .link button {
+ color: var(--accent);
+ text-decoration: none;
+ display: flex;
+ gap: 8px;
+}
+
+.link a:hover, .link button:hover {
+ color: var(--accent-hover);
+ text-decoration: underline;
+ cursor: pointer;
+}
+
+.link svg {
+ vertical-align: text-top;
+}
+
+#code {
+ display: none;
+}
+
+#code header {
+ padding: 24px;
+ display: flex;
+ justify-content: space-between;
+}
+
+#code h2 {
+ font-family: var(--font-monospace);
+ color: var(--title-text);
+ font-size: 18px;
+ margin: 0;
+}
+
+#code .link {
+ padding: 0;
+}
+
+.shiki {
+ margin: 0;
+ border-top: 1px solid var(--border);
+ max-height: 17rem;
+ overflow: auto;
+}
+
+.shiki code {
+ font-family: var(--font-monospace);
+ counter-reset: step;
+ counter-increment: step 0;
+ font-size: 14px;
+ line-height: 21px;
+ tab-size: 1;
+}
+
+.shiki code .line:not(.error-caret)::before {
+ content: counter(step);
+ counter-increment: step;
+ width: 1rem;
+ margin-right: 16px;
+ display: inline-block;
+ text-align: right;
+ padding: 0 8px;
+ color: var(--misc-text);
+ border-right: solid 1px var(--border);
+}
+
+.shiki code .line:first-child::before {
+ padding-top: 8px;
+}
+
+.shiki code .line:last-child::before {
+ padding-bottom: 8px;
+}
+
+.error-line {
+ background-color: #f4909026;
+ display: inline-block;
+ width: 100%;
+}
+
+.error-caret {
+ margin-left: calc(33px + 1rem);
+ color: var(--error-text);
+}
+
+#stack h2 {
+ color: var(--title-text);
+ font-family: var(--font-normal);
+ font-size: 22px;
+ margin: 0;
+ padding: 24px;
+ border-bottom: 1px solid var(--border);
+}
+
+#stack-content {
+ font-size: 14px;
+ white-space: pre;
+ line-height: 21px;
+ overflow: auto;
+ padding: 24px;
+ color: var(--stack-text);
+}
+`;
+
+const overlayTemplate = /* html */ `
+<style>
+${style.trim()}
+</style>
+<div id="backdrop">
+ <div id="layout">
+ <header id="header">
+ <section id="header-left">
+ <h2 id="name"></h2>
+ <h1 id="title">An error occurred.</h1>
+ </section>
+ <div id="houston-overlay"></div>
+ <div id="houston">
+<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="175" height="131" fill="none"><path fill="currentColor" d="M55.977 81.512c0 8.038-6.516 14.555-14.555 14.555S26.866 89.55 26.866 81.512c0-8.04 6.517-14.556 14.556-14.556 8.039 0 14.555 6.517 14.555 14.556Zm24.745-5.822c0-.804.651-1.456 1.455-1.456h11.645c.804 0 1.455.652 1.455 1.455v11.645c0 .804-.651 1.455-1.455 1.455H82.177a1.456 1.456 0 0 1-1.455-1.455V75.689Zm68.411 5.822c0 8.038-6.517 14.555-14.556 14.555-8.039 0-14.556-6.517-14.556-14.555 0-8.04 6.517-14.556 14.556-14.556 8.039 0 14.556 6.517 14.556 14.556Z"/><rect width="168.667" height="125" x="3.667" y="3" stroke="currentColor" stroke-width="4" rx="20.289"/></svg>
+ </div>
+ </header>
+
+ <section id="message-hints">
+ <section id="message">
+ <span id="message-icon">
+ <svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="24" height="24" fill="none"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 7v6m0 4.01.01-.011M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10Z"/></svg>
+ </span>
+ <div id="message-content"></div>
+ </section>
+ <section id="hint">
+ <span id="hint-icon">
+ <svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="24" height="24" fill="none"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m21 2-1 1M3 2l1 1m17 13-1-1M3 16l1-1m5 3h6m-5 3h4M12 3C8 3 5.952 4.95 6 8c.023 1.487.5 2.5 1.5 3.5S9 13 9 15h6c0-2 .5-2.5 1.5-3.5h0c1-1 1.477-2.013 1.5-3.5.048-3.05-2-5-6-5Z"/></svg>
+ </span>
+ <div id="hint-content"></div>
+ </section>
+ </section>
+
+ <section id="code">
+ <header>
+ <h2></h2>
+ </header>
+ <div id="code-content"></div>
+ </section>
+
+ <section id="stack">
+ <h2>Stack Trace</h2>
+ <div id="stack-content"></div>
+ </section>
+ </div>
+</div>
+`;
+
+const openNewWindowIcon =
+ /* html */
+ '<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="16" height="16" fill="none"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M14 2h-4m4 0L8 8m6-6v4"/><path stroke="currentColor" stroke-linecap="round" stroke-width="1.5" d="M14 8.667V12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h3.333"/></svg>';
+
+// Make HTMLElement available in non-browser environments
+const { HTMLElement = class {} as typeof globalThis.HTMLElement } = globalThis;
+class ErrorOverlay extends HTMLElement {
+ root: ShadowRoot;
+
+ constructor(err: AstroErrorPayload['err']) {
+ super();
+ this.root = this.attachShadow({ mode: 'open' });
+ this.root.innerHTML = overlayTemplate;
+
+ this.text('#name', err.name);
+ this.text('#title', err.title);
+ this.text('#message-content', err.message, true);
+
+ const hint = this.root.querySelector<HTMLElement>('#hint');
+ if (hint && err.hint) {
+ this.text('#hint-content', err.hint, true);
+ hint.style.display = 'flex';
+ }
+
+ const docslink = this.root.querySelector<HTMLElement>('#message');
+ if (docslink && err.docslink) {
+ docslink.appendChild(this.createLink(`See Docs Reference${openNewWindowIcon}`, err.docslink));
+ }
+
+ const code = this.root.querySelector<HTMLElement>('#code');
+ if (code && err.loc.file) {
+ code.style.display = 'block';
+ const codeHeader = code.querySelector<HTMLHeadingElement>('#code header');
+ const codeContent = code.querySelector<HTMLDivElement>('#code-content');
+
+ if (codeHeader) {
+ const cleanFile = err.loc.file.split('/').slice(-2).join('/');
+ const fileLocation = [cleanFile, err.loc.line, err.loc.column].filter(Boolean).join(':');
+ const absoluteFileLocation = [err.loc.file, err.loc.line, err.loc.column]
+ .filter(Boolean)
+ .join(':');
+
+ const codeFile = codeHeader.getElementsByTagName('h2')[0];
+ codeFile.textContent = fileLocation;
+ codeFile.title = absoluteFileLocation;
+
+ const editorLink = this.createLink(`Open in editor${openNewWindowIcon}`, undefined);
+ editorLink.onclick = () => {
+ fetch('/__open-in-editor?file=' + encodeURIComponent(absoluteFileLocation));
+ };
+
+ codeHeader.appendChild(editorLink);
+ }
+
+ if (codeContent && err.highlightedCode) {
+ codeContent.innerHTML = err.highlightedCode;
+
+ window.requestAnimationFrame(() => {
+ // NOTE: This cannot be `codeContent.querySelector` because `codeContent` still contain the old HTML
+ const errorLine = this.root.querySelector<HTMLSpanElement>('.error-line');
+
+ if (errorLine) {
+ if (errorLine.parentElement && errorLine.parentElement.parentElement) {
+ errorLine.parentElement.parentElement.scrollTop =
+ errorLine.offsetTop - errorLine.parentElement.parentElement.offsetTop - 8;
+ }
+
+ // Add an empty line below the error line so we can show a caret under the error
+ if (err.loc.column) {
+ errorLine.insertAdjacentHTML(
+ 'afterend',
+ `\n<span class="line error-caret"><span style="padding-left:${
+ err.loc.column - 1
+ }ch;">^</span></span>`
+ );
+ }
+ }
+ });
+ }
+ }
+
+ this.text('#stack-content', err.stack);
+ }
+
+ text(selector: string, text: string | undefined, html = false): void {
+ if (!text) {
+ return;
+ }
+
+ const el = this.root.querySelector(selector);
+
+ if (el) {
+ if (!html) {
+ el.textContent = text.trim();
+ } else {
+ el.innerHTML = text.trim();
+ }
+ }
+ }
+
+ createLink(text: string, href: string | undefined): HTMLDivElement {
+ const linkContainer = document.createElement('div');
+ const linkElement = href ? document.createElement('a') : document.createElement('button');
+ linkElement.innerHTML = text;
+
+ if (href && linkElement instanceof HTMLAnchorElement) {
+ linkElement.href = href;
+ linkElement.target = '_blank';
+ }
+
+ linkContainer.appendChild(linkElement);
+ linkContainer.className = 'link';
+
+ return linkContainer;
+ }
+
+ close(): void {
+ this.parentNode?.removeChild(this);
+ }
+}
+
+function getOverlayCode() {
+ return `
+ const overlayTemplate = \`${overlayTemplate}\`;
+ const openNewWindowIcon = \`${openNewWindowIcon}\`;
+ ${ErrorOverlay.toString()}
+ `;
+}
+
+export function patchOverlay(code: string, config: AstroConfig) {
+ if(config.experimentalErrorOverlay) {
+ return code.replace('class ErrorOverlay', getOverlayCode() + '\nclass ViteErrorOverlay');
+ } else {
+ // Legacy overlay
+ return (
+ code
+ // Transform links in the message to clickable links
+ .replace(
+ "this.text('.message-body', message.trim());",
+ `const urlPattern = /(\\b(https?|ftp):\\/\\/[-A-Z0-9+&@#\\/%?=~_|!:,.;]*[-A-Z0-9+&@#\\/%=~_|])/gim;
+ function escapeHtml(unsafe){return unsafe.replace(/</g, "&lt;").replace(/>/g, "&gt;");}
+ const escapedMessage = escapeHtml(message);
+ this.root.querySelector(".message-body").innerHTML = escapedMessage.trim().replace(urlPattern, '<a href="$1" target="_blank">$1</a>');`
+ )
+ .replace('</style>', '.message-body a {\n color: #ededed;\n}\n</style>')
+ // Hide `.tip` in Vite's ErrorOverlay
+ .replace(/\.tip \{[^}]*\}/gm, '.tip {\n display: none;\n}')
+ // Replace [vite] messages with [astro]
+ .replace(/\[vite\]/g, '[astro]')
+ )
+ }
+}
diff --git a/packages/astro/src/core/messages.ts b/packages/astro/src/core/messages.ts
index 3bfcbb61a..efac24f5e 100644
--- a/packages/astro/src/core/messages.ts
+++ b/packages/astro/src/core/messages.ts
@@ -18,7 +18,8 @@ import type { AddressInfo } from 'net';
import os from 'os';
import { ResolvedServerUrls } from 'vite';
import { ZodError } from 'zod';
-import { ErrorWithMetadata } from './errors/index.js';
+import { renderErrorMarkdown } from './errors/dev/utils.js';
+import { AstroError, CompilerError, ErrorWithMetadata } from './errors/index.js';
import { removeTrailingForwardSlash } from './path.js';
import { emoji, getLocalAddress, padMultilineString } from './util.js';
@@ -254,10 +255,18 @@ export function formatConfigErrorMessage(err: ZodError) {
}
export function formatErrorMessage(err: ErrorWithMetadata, args: string[] = []): string {
- args.push(`${bgRed(black(` error `))}${red(bold(padMultilineString(err.message)))}`);
+ const isOurError = AstroError.is(err) || CompilerError.is(err);
+
+ args.push(
+ `${bgRed(black(` error `))}${red(
+ padMultilineString(isOurError ? renderErrorMarkdown(err.message, 'cli') : err.message)
+ )}`
+ );
if (err.hint) {
args.push(` ${bold('Hint:')}`);
- args.push(yellow(padMultilineString(err.hint, 4)));
+ args.push(
+ yellow(padMultilineString(isOurError ? renderErrorMarkdown(err.hint, 'cli') : err.hint, 4))
+ );
}
if (err.id || err.loc?.file) {
args.push(` ${bold('File:')}`);
@@ -271,7 +280,7 @@ export function formatErrorMessage(err: ErrorWithMetadata, args: string[] = []):
}
if (err.frame) {
args.push(` ${bold('Code:')}`);
- args.push(red(padMultilineString(err.frame, 4)));
+ args.push(red(padMultilineString(err.frame.trim(), 4)));
}
if (args.length === 1 && err.stack) {
args.push(dim(err.stack));
diff --git a/packages/astro/src/vite-plugin-astro-server/plugin.ts b/packages/astro/src/vite-plugin-astro-server/plugin.ts
index 589a74e74..a880bf36f 100644
--- a/packages/astro/src/vite-plugin-astro-server/plugin.ts
+++ b/packages/astro/src/vite-plugin-astro-server/plugin.ts
@@ -9,6 +9,7 @@ import { createRouteManifest } from '../core/routing/index.js';
import { baseMiddleware } from './base.js';
import { createController } from './controller.js';
import { handleRequest } from './request.js';
+import { patchOverlay } from '../core/errors/overlay.js';
export interface AstroPluginOptions {
settings: AstroSettings;
@@ -59,27 +60,12 @@ export default function createVitePluginAstroServer({
});
};
},
- // HACK: Manually replace code in Vite's overlay to fit it to our needs
- // In the future, we'll instead take over the overlay entirely, which should be safer and cleaner
transform(code, id, opts = {}) {
if (opts.ssr) return;
if (!id.includes('vite/dist/client/client.mjs')) return;
- return (
- code
- // Transform links in the message to clickable links
- .replace(
- "this.text('.message-body', message.trim());",
- `const urlPattern = /(\\b(https?|ftp):\\/\\/[-A-Z0-9+&@#\\/%?=~_|!:,.;]*[-A-Z0-9+&@#\\/%=~_|])/gim;
- function escapeHtml(unsafe){return unsafe.replace(/</g, "&lt;").replace(/>/g, "&gt;");}
- const escapedMessage = escapeHtml(message);
- this.root.querySelector(".message-body").innerHTML = escapedMessage.trim().replace(urlPattern, '<a href="$1" target="_blank">$1</a>');`
- )
- .replace('</style>', '.message-body a {\n color: #ededed;\n}\n</style>')
- // Hide `.tip` in Vite's ErrorOverlay
- .replace(/\.tip \{[^}]*\}/gm, '.tip {\n display: none;\n}')
- // Replace [vite] messages with [astro]
- .replace(/\[vite\]/g, '[astro]')
- );
+
+ // Replace the Vite overlay with ours
+ return patchOverlay(code, settings.config);
},
};
}
diff --git a/packages/astro/src/vite-plugin-astro-server/response.ts b/packages/astro/src/vite-plugin-astro-server/response.ts
index 6a8d3a608..911ccf413 100644
--- a/packages/astro/src/vite-plugin-astro-server/response.ts
+++ b/packages/astro/src/vite-plugin-astro-server/response.ts
@@ -28,7 +28,9 @@ export async function handle500Response(
res: http.ServerResponse,
err: ErrorWithMetadata
) {
- res.on('close', () => setTimeout(() => loader.webSocketSend(getViteErrorPayload(err)), 200));
+ res.on('close', async () =>
+ setTimeout(async () => loader.webSocketSend(await getViteErrorPayload(err)), 200)
+ );
if (res.headersSent) {
res.write(`<script type="module" src="/@vite/client"></script>`);
res.end();