From 540eacab1ecd7340a09ce99a5b9409fc4403798a Mon Sep 17 00:00:00 2001 From: neverland Date: Fri, 27 Dec 2024 11:27:59 +0800 Subject: [PATCH] fix: overlay renders invalid file path (#4280) --- packages/core/src/client/hmr.ts | 16 +- packages/core/src/client/overlay.ts | 45 ++--- packages/core/src/server/overlay.ts | 144 +++++++++++++++ packages/core/src/server/socketServer.ts | 165 ++---------------- .../tests/{ansi.test.ts => overlay.test.ts} | 15 ++ 5 files changed, 186 insertions(+), 199 deletions(-) create mode 100644 packages/core/src/server/overlay.ts rename packages/core/tests/{ansi.test.ts => overlay.test.ts} (83%) diff --git a/packages/core/src/client/hmr.ts b/packages/core/src/client/hmr.ts index fd96c62ca3..21655ef122 100644 --- a/packages/core/src/client/hmr.ts +++ b/packages/core/src/client/hmr.ts @@ -41,11 +41,11 @@ function clearOutdatedErrors() { } } -let createOverlay: undefined | ((overlay: string, errors: string[]) => void); +let createOverlay: undefined | ((html: string) => void); let clearOverlay: undefined | (() => void); export const registerOverlay = ( - createFn: (overlay: string, errors: string[]) => void, + createFn: (html: string) => void, clearFn: () => void, ): void => { createOverlay = createFn; @@ -91,15 +91,7 @@ function handleWarnings({ text }: { text: string[] }) { } // Compilation with errors (e.g. syntax error or missing modules). -function handleErrors({ - text, - html, - overlay, -}: { - text: string[]; - html: string[]; - overlay: string; -}) { +function handleErrors({ text, html }: { text: string[]; html: string }) { clearOutdatedErrors(); isFirstCompilation = false; @@ -111,7 +103,7 @@ function handleErrors({ } if (createOverlay) { - createOverlay(overlay, html); + createOverlay(html); } } diff --git a/packages/core/src/client/overlay.ts b/packages/core/src/client/overlay.ts index a0205cf51a..b6ad835a9c 100644 --- a/packages/core/src/client/overlay.ts +++ b/packages/core/src/client/overlay.ts @@ -1,40 +1,12 @@ import { registerOverlay } from './hmr'; -function linkedText(root: ShadowRoot, selector: string, text: string): void { - const el = root.querySelector(selector)!; - const fileRegex = /(?:[a-zA-Z]:\\|\/).*?:\d+:\d+/g; - - let curIndex = 0; - let match = fileRegex.exec(text); - while (match !== null) { - const { 0: file, index } = match; - if (index != null) { - const frag = text.slice(curIndex, index); - el.insertAdjacentHTML('beforeend', frag); - const link = document.createElement('a'); - link.textContent = file; - link.className = 'file-link'; - - link.onclick = () => { - fetch(`/__open-in-editor?file=${encodeURIComponent(file)}`); - }; - el.appendChild(link); - curIndex += frag.length + file.length; - } - match = fileRegex.exec(text); - } - - const frag = text.slice(curIndex); - el.insertAdjacentHTML('beforeend', frag); -} - const { HTMLElement = class {} as typeof globalThis.HTMLElement, customElements, } = typeof window !== 'undefined' ? window : globalThis; class ErrorOverlay extends HTMLElement { - constructor(overlay: string, errors: string[]) { + constructor(html: string) { super(); if (!this.attachShadow) { @@ -45,13 +17,18 @@ class ErrorOverlay extends HTMLElement { } const root = this.attachShadow({ mode: 'open' }); - root.innerHTML = overlay; + root.innerHTML = html; - linkedText(root, '.content', errors.join('\n\n').trim()); - root.querySelector('.close')?.addEventListener('click', this.close); + root.querySelector('.close')!.addEventListener('click', this.close); // close overlay when click outside this.addEventListener('click', this.close); root.querySelector('.container')!.addEventListener('click', (e) => { + if (e.target) { + const { file } = (e.target as HTMLLinkElement).dataset; + if (file) { + fetch(`/__open-in-editor?file=${encodeURIComponent(file)}`); + } + } e.stopPropagation(); }); @@ -84,9 +61,9 @@ if (customElements && !customElements.get(overlayId)) { customElements.define(overlayId, ErrorOverlay); } -function createOverlay(overlay: string, errors: string[]) { +function createOverlay(html: string) { clearOverlay(); - document.body.appendChild(new ErrorOverlay(overlay, errors)); + document.body.appendChild(new ErrorOverlay(html)); } function clearOverlay() { diff --git a/packages/core/src/server/overlay.ts b/packages/core/src/server/overlay.ts new file mode 100644 index 0000000000..fcce8e87a7 --- /dev/null +++ b/packages/core/src/server/overlay.ts @@ -0,0 +1,144 @@ +import ansiHTML from './ansiHTML'; +import { escapeHtml } from './helper'; + +export function convertLinksInHtml(text: string): string { + const fileRegex = /(?:[a-zA-Z]:\\|\/).*?:\d+:\d+/g; + + return text.replace(fileRegex, (file) => { + // If the file contains ``, it means the file path contains ANSI codes. + // We need to move the `` to the end of the file path. + const hasClosingSpan = file.includes('') && !file.includes('', '') : file; + + return `${filePath}${ + hasClosingSpan ? '' : '' + }`; + }); +} + +// HTML template for error overlay +export function genOverlayHTML(errors: string[]) { + const htmlItems = errors.map((item) => + convertLinksInHtml(ansiHTML(escapeHtml(item))), + ); + return ` + + +
+
+
+

Build failed

+
${htmlItems.join('\n\n').trim()}
+
+

Fix error, click outside, or press Esc to close the overlay.

+

Disable overlay by setting Rsbuild's dev.client.overlay config to false.

+

+
+
+`; +} diff --git a/packages/core/src/server/socketServer.ts b/packages/core/src/server/socketServer.ts index 88212b030b..8bb67fa520 100644 --- a/packages/core/src/server/socketServer.ts +++ b/packages/core/src/server/socketServer.ts @@ -10,146 +10,21 @@ import { import { formatStatsMessages } from '../helpers/format'; import { logger } from '../logger'; import type { DevConfig, Rspack } from '../types'; -import ansiHTML from './ansiHTML'; -import { escapeHtml, getCompilationId } from './helper'; +import { getCompilationId } from './helper'; +import { genOverlayHTML } from './overlay'; interface ExtWebSocket extends Ws { isAlive: boolean; } -// HTML template for error overlay -const overlayTemplate = ` - - -
-
-
-

Build failed

-

-    
-

Fix error, click outside, or press Esc to close the overlay.

-

Disable overlay by setting Rsbuild's dev.client.overlay config to false.

-

-
-
-`; - function isEqualSet(a: Set, b: Set): boolean { - if (a.size !== b.size) { - return false; - } + return a.size === b.size && [...a].every((value) => b.has(value)); +} - for (const v of a.values()) { - if (!b.has(v)) { - return false; - } - } - return true; +interface SocketMessage { + type: string; + compilationId?: string; + data?: Record | string | boolean; } export class SocketServer { @@ -232,15 +107,7 @@ export class SocketServer { } // write message to each socket - public sockWrite({ - type, - compilationId, - data, - }: { - type: string; - compilationId?: string; - data?: Record | string | boolean; - }): void { + public sockWrite({ type, compilationId, data }: SocketMessage): void { for (const socket of this.sockets) { this.send(socket, JSON.stringify({ type, data, compilationId })); } @@ -248,15 +115,7 @@ export class SocketServer { private singleWrite( socket: Ws, - { - type, - data, - compilationId, - }: { - type: string; - compilationId?: string; - data?: Record | string | boolean; - }, + { type, data, compilationId }: SocketMessage, ) { this.send(socket, JSON.stringify({ type, data, compilationId })); } @@ -413,13 +272,13 @@ export class SocketServer { errors, warnings: [], }); + return this.sockWrite({ type: 'errors', compilationId, data: { text: formattedErrors, - html: formattedErrors.map((item) => ansiHTML(escapeHtml(item))), - overlay: overlayTemplate, + html: genOverlayHTML(formattedErrors), }, }); } diff --git a/packages/core/tests/ansi.test.ts b/packages/core/tests/overlay.test.ts similarity index 83% rename from packages/core/tests/ansi.test.ts rename to packages/core/tests/overlay.test.ts index 1498fa6e81..dc9efbe253 100644 --- a/packages/core/tests/ansi.test.ts +++ b/packages/core/tests/overlay.test.ts @@ -1,4 +1,5 @@ import { ansiHTML } from '../src/server/ansiHTML'; +import { convertLinksInHtml } from '../src/server/overlay'; describe('ansiHTML', () => { it('should convert ANSI color codes to HTML', () => { @@ -113,3 +114,17 @@ describe('ansiHTML', () => { expect(ansiHTML(input)).toEqual(expected); }); }); + +describe('convertLinksInHtml', () => { + it('should convert file path with ANSI color codes to HTML', () => { + const input1 = '[\u001b[36;1;4m/path/to/src/index.js\u001b[0m:4:1]\n'; + const expected1 = + '[/path/to/src/index.js:4:1]\n'; + expect(convertLinksInHtml(ansiHTML(input1))).toEqual(expected1); + + const input2 = '[\u001b[36;1;4m/path/to/src/index.js:4:1\u001b[0m]\n'; + const expected2 = + '[/path/to/src/index.js:4:1]\n'; + expect(convertLinksInHtml(ansiHTML(input2))).toEqual(expected2); + }); +});