Add a new error overlay (#5495)
* Add new overlay * Fix CSS errors missing the proper stacktrace * Fix names not working in some cases * Add changeset * Fix Playwright not detecting the overlay * Update E2E test * Fix tests * Small refactor, fix syntax highlight on light mode, fix code element showing even with no code * Simplier injection * Add Markdown support to CLI reporting * Fix not being able to navigate with the keyboard to the open in editor link * aria-hide some svgs (#5508) we should also make the "open in editor" button a button, not a link, if we are using JS for interactions * Implement close method so Vite can close the overlay when needed * Fix filepaths not being absolute when coming from node_modules for errors * Fix multi line errors with indentation not showing correctly * Fix entire page being scrolled to the error line in certain cases * Update docs links * Put the new error overlay behind a flag * add flag for e2e tests Co-authored-by: Caleb Jasik <calebjasik@jasik.xyz> Co-authored-by: Matthew Phillips <matthew@skypack.dev>
This commit is contained in:
parent
6b156dd3b4
commit
31ec847972
19 changed files with 800 additions and 73 deletions
5
.changeset/happy-chefs-ring.md
Normal file
5
.changeset/happy-chefs-ring.md
Normal file
|
@ -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.
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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([
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 };
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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. */
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)}`);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.',
|
||||
},
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
|
|
585
packages/astro/src/core/errors/overlay.ts
Normal file
585
packages/astro/src/core/errors/overlay.ts
Normal file
|
@ -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, "<").replace(/>/g, ">");}
|
||||
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]')
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
|
|
|
@ -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, "<").replace(/>/g, ">");}
|
||||
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);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Reference in a new issue