\d+)/;
+ let sourceInfo = {} as any;
+ for (let i = 0; i < lines.length; i++) {
+ const line = lines[i];
+ const match = line.match(regex);
+ if (match) {
+ const { filename, line, column } = match.groups as any;
+ sourceInfo = { filename, line, column };
+ break;
+ }
+ }
+ if (!sourceInfo.filename) return;
+
+ try {
+ const content = await findSourceCode(sourceInfo);
+ return content;
+ } catch (e) {}
+}
diff --git a/packages/core/src/client/hmr.ts b/packages/core/src/client/hmr.ts
index 18bf0458e3..7fc5431182 100644
--- a/packages/core/src/client/hmr.ts
+++ b/packages/core/src/client/hmr.ts
@@ -6,7 +6,8 @@
*/
import type { StatsError } from '@rsbuild/shared';
import type { ClientConfig } from '@rsbuild/shared';
-import { formatStatsMessages } from './format';
+import type { OverlayError } from '../types';
+import { formatBuildErrors, formatStatsMessages } from './format';
/**
* hmr socket connect path
@@ -62,11 +63,11 @@ function clearOutdatedErrors() {
}
}
-let createOverlay: undefined | ((err: string[]) => void);
+let createOverlay: undefined | ((err: OverlayError) => void);
let clearOverlay: undefined | (() => void);
export const registerOverlay = (
- createFn: (err: string[]) => void,
+ createFn: (err: OverlayError) => void,
clearFn: () => void,
) => {
createOverlay = createFn;
@@ -124,18 +125,13 @@ function handleErrors(errors: StatsError[]) {
hasCompileErrors = true;
// "Massage" webpack messages.
- const formatted = formatStatsMessages({
- errors,
- warnings: [],
- });
+ const overlayError = formatBuildErrors(errors);
// Also log them to the console.
- for (const error of formatted.errors) {
- console.error(error);
- }
+ console.error(overlayError.content);
if (createOverlay) {
- createOverlay(formatted.errors);
+ createOverlay(overlayError);
}
// Do not attempt to reload now.
diff --git a/packages/core/src/client/overlay.ts b/packages/core/src/client/overlay.ts
index dae07a106d..b61d44596a 100644
--- a/packages/core/src/client/overlay.ts
+++ b/packages/core/src/client/overlay.ts
@@ -1,3 +1,5 @@
+import { type OverlayError } from '../types';
+import { formatRuntimeErrors } from './format';
import { registerOverlay } from './hmr';
function stripAnsi(content: string) {
@@ -11,8 +13,13 @@ function stripAnsi(content: string) {
return content.replace(regex, '');
}
+function displayElement(el: Element) {
+ (el as HTMLElement).style.display = 'block';
+}
+
function linkedText(root: ShadowRoot, selector: string, text: string): void {
const el = root.querySelector(selector)!;
+ displayElement(el);
const fileRegex = /(?:[a-zA-Z]:\\|\/).*?:\d+:\d+/g;
let curIndex = 0;
@@ -39,6 +46,24 @@ function linkedText(root: ShadowRoot, selector: string, text: string): void {
el.appendChild(document.createTextNode(frag));
}
+function updateLink(root: ShadowRoot, selector: string, file: string): void {
+ const el = root.querySelector(selector)!;
+ el.addEventListener('click', () => {
+ fetch(`/__open-in-editor?file=${encodeURIComponent(file)}`);
+ });
+ el.classList.add('cursor-pointer');
+}
+
+function updateElement(
+ root: ShadowRoot,
+ selector: string,
+ innerHTML: string,
+): void {
+ const el = root.querySelector(selector)!;
+ el.innerHTML = innerHTML;
+ displayElement(el);
+}
+
const overlayTemplate = `
-
Compilation failed
-
-
+
+
+
+
+
`;
@@ -166,7 +215,7 @@ const {
} = typeof window !== 'undefined' ? window : globalThis;
class ErrorOverlay extends HTMLElement {
- constructor(message: string[]) {
+ constructor(error: OverlayError) {
super();
if (!this.attachShadow) {
@@ -176,28 +225,55 @@ class ErrorOverlay extends HTMLElement {
return;
}
+ const { type, content, title, stack, sourceFile } = error;
const root = this.attachShadow({ mode: 'open' });
root.innerHTML = overlayTemplate;
- linkedText(root, '.content', stripAnsi(message.join('/n')).trim());
+ updateElement(root, '.title', title);
- root.querySelector('.close')?.addEventListener('click', this.close);
+ if (type === 'build') {
+ linkedText(root, '.build-content', stripAnsi(content).trim());
+ } else {
+ updateElement(root, '.runtime-content', content);
+ if (sourceFile) {
+ updateLink(root, '.runtime-content', sourceFile);
+ }
+ }
- // close overlay when click outside
- this.addEventListener('click', this.close);
+ if (stack?.length) {
+ updateElement(
+ root,
+ '.stack',
+ `
+
+ ${stack.length} stack frames
+ ${stack.join('\n')}
+
+ `,
+ );
+ }
- root.querySelector('.container')!.addEventListener('click', (e) => {
- e.stopPropagation();
- });
+ updateElement(
+ root,
+ '.footer',
+ type === 'build'
+ ? ' This error occurred during the build time and cannot be dismissed.
'
+ : "Fix error, click outside, or press Esc to close the overlay.
Disable overlay by setting Rsbuild's dev.client.runtimeErrors config to false.
",
+ );
- const onEscKeydown = (e: KeyboardEvent) => {
- if (e.key === 'Escape' || e.code === 'Escape') {
- this.close();
- }
- document.removeEventListener('keydown', onEscKeydown);
- };
+ if (type === 'runtime') {
+ displayElement(root.querySelector('.close') as Element);
+ root.querySelector('.close')?.addEventListener('click', this.close);
- document.addEventListener('keydown', onEscKeydown);
+ const onEscKeydown = (e: KeyboardEvent) => {
+ if (e.key === 'Escape' || e.code === 'Escape') {
+ this.close();
+ }
+ document.removeEventListener('keydown', onEscKeydown);
+ };
+
+ document.addEventListener('keydown', onEscKeydown);
+ }
}
close = () => {
@@ -220,17 +296,42 @@ if (customElements && !customElements.get(overlayId)) {
customElements.define(overlayId, ErrorOverlay);
}
-function createOverlay(err: string[]) {
+const documentAvailable = typeof document !== 'undefined';
+
+export function createOverlay(err: OverlayError) {
+ if (!documentAvailable) {
+ console.info(
+ '[Rsbuild] Failed to display error overlay as document is not available, you can disable the `dev.client.overlay` option.',
+ );
+ return;
+ }
+
+ if (!err) return;
+
+ if (hasOverlay()) return;
+
clearOverlay();
document.body.appendChild(new ErrorOverlay(err));
}
-function clearOverlay() {
+export function clearOverlay() {
+ if (!documentAvailable) {
+ return;
+ }
+
// use NodeList's forEach api instead of dom.iterable
// biome-ignore lint/complexity/noForEach:
document.querySelectorAll(overlayId).forEach((n) => n.close());
}
+export function hasOverlay() {
+ if (!documentAvailable) {
+ return false;
+ }
+
+ return document.querySelector(overlayId) !== null;
+}
+
if (typeof document !== 'undefined') {
registerOverlay(createOverlay, clearOverlay);
} else {
@@ -238,3 +339,19 @@ if (typeof document !== 'undefined') {
'[Rsbuild] Failed to display error overlay as document is not available, you can disable the `dev.client.overlay` option.',
);
}
+
+const config = RSBUILD_CLIENT_CONFIG;
+
+if (config.overlay.runtimeErrors) {
+ window.addEventListener('error', async (event) => {
+ const formatted = await formatRuntimeErrors(event, false);
+ createOverlay(formatted);
+ });
+
+ window.addEventListener('unhandledrejection', async (event) => {
+ if (event.reason?.stack) {
+ const formatted = await formatRuntimeErrors(event, true);
+ createOverlay(formatted);
+ }
+ });
+}
diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts
index 28dcf8c475..7f61111c01 100644
--- a/packages/core/src/types.ts
+++ b/packages/core/src/types.ts
@@ -53,3 +53,15 @@ export type {
NormalizedPerformanceConfig,
NormalizedModuleFederationConfig,
} from '@rsbuild/shared';
+
+export interface OverlayError {
+ content: string;
+
+ title: string;
+
+ type: 'build' | 'runtime';
+
+ stack?: string[];
+
+ sourceFile?: string;
+}
diff --git a/packages/shared/src/types/config/dev.ts b/packages/shared/src/types/config/dev.ts
index 48de313d9e..e92456f668 100644
--- a/packages/shared/src/types/config/dev.ts
+++ b/packages/shared/src/types/config/dev.ts
@@ -27,7 +27,12 @@ export type ClientConfig = {
host?: string;
protocol?: 'ws' | 'wss';
/** Shows an overlay in the browser when there are compiler errors. */
- overlay?: boolean;
+ overlay?:
+ | boolean
+ | {
+ // unhandled runtime errors
+ runtimeErrors?: boolean;
+ };
};
export type ChokidarWatchOptions = WatchOptions;
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ed0eace7cc..ed56e6ba5b 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -695,6 +695,9 @@ importers:
postcss:
specifier: ^8.4.38
version: 8.4.38
+ source-map:
+ specifier: 0.5.7
+ version: 0.5.7
devDependencies:
'@types/node':
specifier: 18.x
@@ -7874,6 +7877,10 @@ packages:
source-map-support@0.5.21:
resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
+ source-map@0.5.7:
+ resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==}
+ engines: {node: '>=0.10.0'}
+
source-map@0.6.1:
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
engines: {node: '>=0.10.0'}
@@ -15967,6 +15974,8 @@ snapshots:
buffer-from: 1.1.2
source-map: 0.6.1
+ source-map@0.5.7: {}
+
source-map@0.6.1: {}
source-map@0.7.4: {}
From 075c2bb1fb78cf198da91683f0f5a1e1aaaf55ce Mon Sep 17 00:00:00 2001
From: nanianlisao <597219320@qq.com>
Date: Sat, 11 May 2024 16:10:14 +0800
Subject: [PATCH 2/3] fix: fix lint error
---
packages/core/src/client/findSourceMap.ts | 15 +++++----------
packages/core/src/client/format.ts | 6 +++---
packages/core/src/client/overlay.ts | 2 +-
3 files changed, 9 insertions(+), 14 deletions(-)
diff --git a/packages/core/src/client/findSourceMap.ts b/packages/core/src/client/findSourceMap.ts
index e733de0918..6302d9f35d 100644
--- a/packages/core/src/client/findSourceMap.ts
+++ b/packages/core/src/client/findSourceMap.ts
@@ -6,7 +6,7 @@ const fetchContent = (url: string) => fetch(url).then((r) => r.text());
const findSourceMap = async (fileSource: string, filename: string) => {
try {
// Prefer to get it via filename + '.map'.
- const mapUrl = filename + '.map';
+ const mapUrl = `${filename}.map`;
return await fetchContent(mapUrl);
} catch (e) {
const mapUrl = fileSource.match(/\/\/# sourceMappingURL=(.*)$/)?.[1];
@@ -33,7 +33,7 @@ const escapeHTML = (str: string) =>
const formatSourceCode = (sourceCode: string, pos: any) => {
// Note that the line starts at 1, not 0.
const { line: crtLine, column, name } = pos;
- let lines = sourceCode.split('\n');
+ const lines = sourceCode.split('\n');
// Display up to 6 lines of source code
const lineCount = Math.min(lines.length, 6);
@@ -51,12 +51,7 @@ const formatSourceCode = (sourceCode: string, pos: any) => {
// When the sourcemap information includes specific column details, add an error hint below the error line.
if (line === crtLine && column > 0) {
- const errorLine =
- ' '.repeat(prefix.length + column) +
- '' +
- '^'.repeat(name?.length || 1) +
- '';
-
+ const errorLine = `${' '.repeat(prefix.length + column)}${'^'.repeat(name?.length || 1)}`;
result.push(errorLine);
}
}
@@ -78,8 +73,8 @@ export const findSourceCode = async (sourceInfo: any) => {
// Use sourcemap to find the source code location
const pos = consumer.originalPositionFor({
- line: parseInt(line, 10),
- column: parseInt(column, 10),
+ line: Number.parseInt(line, 10),
+ column: Number.parseInt(column, 10),
});
const url = `${pos.source}:${pos.line}:${pos.column}`;
diff --git a/packages/core/src/client/format.ts b/packages/core/src/client/format.ts
index a08ff6eca2..3443d3003d 100644
--- a/packages/core/src/client/format.ts
+++ b/packages/core/src/client/format.ts
@@ -1,5 +1,5 @@
import type { StatsCompilation, StatsError } from '@rspack/core';
-import { type OverlayError } from '../types';
+import type { OverlayError } from '../types';
import { findSourceCode } from './findSourceMap';
function resolveFileName(stats: StatsError) {
@@ -82,13 +82,13 @@ export async function formatRuntimeErrors(
event: PromiseRejectionEvent | ErrorEvent,
isRejection: boolean,
): Promise {
- let error = isRejectionEvent(isRejection, event)
+ const error = isRejectionEvent(isRejection, event)
? event.reason
: event?.error;
if (!error) return;
const errorName = isRejection
- ? 'Unhandled Rejection (' + error.name + ')'
+ ? `Unhandled Rejection (${error.name})`
: error.name;
const stack = parseRuntimeStack(error.stack);
diff --git a/packages/core/src/client/overlay.ts b/packages/core/src/client/overlay.ts
index b61d44596a..71bcb3d853 100644
--- a/packages/core/src/client/overlay.ts
+++ b/packages/core/src/client/overlay.ts
@@ -1,4 +1,4 @@
-import { type OverlayError } from '../types';
+import type { OverlayError } from '../types';
import { formatRuntimeErrors } from './format';
import { registerOverlay } from './hmr';
From 5a2f9acf3ef7da1b7966bc65e71fc430ae8a6f37 Mon Sep 17 00:00:00 2001
From: nanianlisao <597219320@qq.com>
Date: Mon, 20 May 2024 14:08:41 +0800
Subject: [PATCH 3/3] chore: use source-map-js
---
e2e/cases/server/overlay/index.test.ts | 2 +-
packages/core/modern.config.ts | 4 +++-
packages/core/package.json | 6 +++++-
packages/core/src/client/findSourceMap.ts | 7 ++++---
packages/core/src/client/overlay.ts | 17 -----------------
packages/core/src/client/runtimeErrors.ts | 14 ++++++++++++++
.../core/src/server/compilerDevMiddleware.ts | 9 +++++++++
pnpm-lock.yaml | 12 +++---------
8 files changed, 39 insertions(+), 32 deletions(-)
create mode 100644 packages/core/src/client/runtimeErrors.ts
diff --git a/e2e/cases/server/overlay/index.test.ts b/e2e/cases/server/overlay/index.test.ts
index 19dcc4895b..cb5908f940 100644
--- a/e2e/cases/server/overlay/index.test.ts
+++ b/e2e/cases/server/overlay/index.test.ts
@@ -39,7 +39,7 @@ test('should show overlay correctly', async ({ page }) => {
fse.readFileSync(appPath, 'utf-8').replace('', ''),
);
- await expect(errorOverlay.locator('.title')).toHaveText('Compilation failed');
+ await expect(errorOverlay.locator('.title')).toHaveText('Failed to compile');
await rsbuild.close();
diff --git a/packages/core/modern.config.ts b/packages/core/modern.config.ts
index be36b4bc4c..1ef88d3e24 100644
--- a/packages/core/modern.config.ts
+++ b/packages/core/modern.config.ts
@@ -18,6 +18,7 @@ const externals = [
...commonExternals,
'@rsbuild/core/client/hmr',
'@rsbuild/core/client/overlay',
+ '@rsbuild/core/client/runtimeErrors',
];
// Since the relative paths of bundle and compiled have changed,
@@ -69,10 +70,11 @@ export default defineConfig({
input: {
hmr: 'src/client/hmr.ts',
overlay: 'src/client/overlay.ts',
+ runtimeErrors: 'src/client/runtimeErrors.ts',
},
target: BUILD_TARGET.client,
dts: false,
- externals: ['./hmr'],
+ externals: ['./hmr', './overlay'],
outDir: './dist/client',
autoExtension: true,
externalHelpers: true,
diff --git a/packages/core/package.json b/packages/core/package.json
index e2888a911a..2855f6cb6d 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -27,6 +27,10 @@
"types": "./dist-types/client/overlay.d.ts",
"default": "./dist/client/overlay.js"
},
+ "./client/runtimeErrors": {
+ "types": "./dist/client/runtimeErrors.d.ts",
+ "default": "./dist/client/runtimeErrors.js"
+ },
"./types": {
"types": "./types.d.ts"
},
@@ -57,7 +61,7 @@
"core-js": "~3.36.0",
"html-webpack-plugin": "npm:html-rspack-plugin@5.7.2",
"postcss": "^8.4.38",
- "source-map": "0.5.7"
+ "source-map-js": "^1.2.0"
},
"devDependencies": {
"@types/fs-extra": "^11.0.4",
diff --git a/packages/core/src/client/findSourceMap.ts b/packages/core/src/client/findSourceMap.ts
index 6302d9f35d..d0ef5b8414 100644
--- a/packages/core/src/client/findSourceMap.ts
+++ b/packages/core/src/client/findSourceMap.ts
@@ -1,6 +1,3 @@
-// @ts-expect-error
-import { SourceMapConsumer } from 'source-map';
-
const fetchContent = (url: string) => fetch(url).then((r) => r.text());
const findSourceMap = async (fileSource: string, filename: string) => {
@@ -69,6 +66,8 @@ export const findSourceCode = async (sourceInfo: any) => {
if (!smContent) return;
const rawSourceMap = JSON.parse(smContent);
+ const { SourceMapConsumer } = await import('source-map-js');
+
const consumer = await new SourceMapConsumer(rawSourceMap);
// Use sourcemap to find the source code location
@@ -81,6 +80,8 @@ export const findSourceCode = async (sourceInfo: any) => {
const sourceCode = consumer.sourceContentFor(pos.source);
return {
sourceCode: formatSourceCode(sourceCode, pos),
+ // Please use an absolute path in order to open it in vscode.
+ // Take webpack as an example. Please configure it correctly for [output.devtoolModuleFilenameTemplate](https://www.webpackjs.com/configuration/output/#outputdevtoolmodulefilenametemplate)
sourceFile: url,
};
};
diff --git a/packages/core/src/client/overlay.ts b/packages/core/src/client/overlay.ts
index 71bcb3d853..5451768d1b 100644
--- a/packages/core/src/client/overlay.ts
+++ b/packages/core/src/client/overlay.ts
@@ -1,5 +1,4 @@
import type { OverlayError } from '../types';
-import { formatRuntimeErrors } from './format';
import { registerOverlay } from './hmr';
function stripAnsi(content: string) {
@@ -339,19 +338,3 @@ if (typeof document !== 'undefined') {
'[Rsbuild] Failed to display error overlay as document is not available, you can disable the `dev.client.overlay` option.',
);
}
-
-const config = RSBUILD_CLIENT_CONFIG;
-
-if (config.overlay.runtimeErrors) {
- window.addEventListener('error', async (event) => {
- const formatted = await formatRuntimeErrors(event, false);
- createOverlay(formatted);
- });
-
- window.addEventListener('unhandledrejection', async (event) => {
- if (event.reason?.stack) {
- const formatted = await formatRuntimeErrors(event, true);
- createOverlay(formatted);
- }
- });
-}
diff --git a/packages/core/src/client/runtimeErrors.ts b/packages/core/src/client/runtimeErrors.ts
new file mode 100644
index 0000000000..0aa6f0fdf6
--- /dev/null
+++ b/packages/core/src/client/runtimeErrors.ts
@@ -0,0 +1,14 @@
+import { formatRuntimeErrors } from './format';
+import { createOverlay } from './overlay';
+
+window.addEventListener('error', async (event) => {
+ const formatted = await formatRuntimeErrors(event, false);
+ createOverlay(formatted);
+});
+
+window.addEventListener('unhandledrejection', async (event) => {
+ if (event.reason?.stack) {
+ const formatted = await formatRuntimeErrors(event, true);
+ createOverlay(formatted);
+ }
+});
diff --git a/packages/core/src/server/compilerDevMiddleware.ts b/packages/core/src/server/compilerDevMiddleware.ts
index c1c5333113..af6b75616f 100644
--- a/packages/core/src/server/compilerDevMiddleware.ts
+++ b/packages/core/src/server/compilerDevMiddleware.ts
@@ -31,6 +31,15 @@ function getClientPaths(devConfig: DevConfig) {
clientPaths.push(`${require.resolve('@rsbuild/core/client/overlay')}`);
}
+ if (
+ typeof devConfig?.client?.overlay === 'object' &&
+ devConfig.client.overlay.runtimeErrors
+ ) {
+ clientPaths.push(
+ `${require.resolve('@rsbuild/core/client/runtimeErrors')}`,
+ );
+ }
+
return clientPaths;
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e5f8d543be..e06ec7ef79 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -704,9 +704,9 @@ importers:
postcss:
specifier: ^8.4.38
version: 8.4.38
- source-map:
- specifier: 0.5.7
- version: 0.5.7
+ source-map-js:
+ specifier: ^1.2.0
+ version: 1.2.0
devDependencies:
'@types/fs-extra':
specifier: ^11.0.4
@@ -7873,10 +7873,6 @@ packages:
source-map-support@0.5.21:
resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
- source-map@0.5.7:
- resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==}
- engines: {node: '>=0.10.0'}
-
source-map@0.6.1:
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
engines: {node: '>=0.10.0'}
@@ -15955,8 +15951,6 @@ snapshots:
buffer-from: 1.1.2
source-map: 0.6.1
- source-map@0.5.7: {}
-
source-map@0.6.1: {}
source-map@0.7.4: {}