Skip to content

Commit

Permalink
feat: display log colors on error overlay (#4266)
Browse files Browse the repository at this point in the history
  • Loading branch information
chenjiahan authored Dec 25, 2024
1 parent 59044d7 commit 91c23ca
Show file tree
Hide file tree
Showing 6 changed files with 159 additions and 22 deletions.
2 changes: 1 addition & 1 deletion e2e/cases/server/overlay/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ test('should show overlay correctly', async ({ page }) => {
fs.readFileSync(appPath, 'utf-8').replace('</div>', '</aaaaa>'),
);

await expect(errorOverlay.locator('.title')).toHaveText('Compilation failed');
await expect(errorOverlay.locator('.title')).toHaveText('Build failed');

await rsbuild.close();

Expand Down
25 changes: 7 additions & 18 deletions packages/core/src/client/overlay.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,5 @@
import { registerOverlay } from './hmr';

function stripAnsi(content: string) {
const pattern = [
'[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)',
'(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))',
].join('|');

const regex = new RegExp(pattern, 'g');

return content.replace(regex, '');
}

function linkedText(root: ShadowRoot, selector: string, text: string): void {
const el = root.querySelector(selector)!;
const fileRegex = /(?:[a-zA-Z]:\\|\/).*?:\d+:\d+/g;
Expand All @@ -21,7 +10,7 @@ function linkedText(root: ShadowRoot, selector: string, text: string): void {
const { 0: file, index } = match;
if (index != null) {
const frag = text.slice(curIndex, index);
el.appendChild(document.createTextNode(frag));
el.insertAdjacentHTML('beforeend', frag);
const link = document.createElement('a');
link.textContent = file;
link.className = 'file-link';
Expand All @@ -36,7 +25,7 @@ function linkedText(root: ShadowRoot, selector: string, text: string): void {
}

const frag = text.slice(curIndex);
el.appendChild(document.createTextNode(frag));
el.insertAdjacentHTML('beforeend', frag);
}

const overlayTemplate = `
Expand Down Expand Up @@ -76,7 +65,7 @@ const overlayTemplate = `
padding-bottom: 12px;
font-size: 17px;
font-weight: 600;
color: #fc5e5e;
color: #fb6a6a;
border-bottom: 2px solid rgba(252,94,94,.66);
}
.content {
Expand All @@ -85,15 +74,15 @@ const overlayTemplate = `
font-family: inherit;
overflow-x: scroll;
scrollbar-width: none;
color: #b8b8b8;
}
.content::-webkit-scrollbar {
display: none;
}
.file-link {
cursor: pointer;
color: #27caca;
color: #6eecf7;
text-decoration: underline;
text-underline-offset: 3px;
&:hover {
opacity: 0.8;
}
Expand Down Expand Up @@ -150,7 +139,7 @@ const overlayTemplate = `
<div class="root">
<div class="container">
<div class="close"></div>
<p class="title">Compilation failed</p>
<p class="title">Build failed</p>
<pre class="content"></pre>
<footer class="footer">
<p><span>Fix error</span>, click outside, or press Esc to close the overlay.</p>
Expand Down Expand Up @@ -179,7 +168,7 @@ class ErrorOverlay extends HTMLElement {
const root = this.attachShadow({ mode: 'open' });
root.innerHTML = overlayTemplate;

linkedText(root, '.content', stripAnsi(message.join('\n')).trim());
linkedText(root, '.content', message.join('\n\n').trim());

root.querySelector('.close')?.addEventListener('click', this.close);

Expand Down
128 changes: 128 additions & 0 deletions packages/core/src/server/ansiHTML.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/**
* This module is modified based on `ansi-html-community`
* https://github.com/mahdyar/ansi-html-community
*
* Licensed under the Apache License, Version 2.0 (the "License");
* https://github.com/mahdyar/ansi-html-community/blob/master/LICENSE
*/

// https://github.com/chalk/ansi-regex
function ansiRegex() {
// Valid string terminator sequences are BEL, ESC\, and 0x9c
const ST = '(?:\\u0007|\\u001B\\u005C|\\u009C)';
const pattern = [
`[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?${ST})`,
'(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))',
].join('|');

return new RegExp(pattern, 'g');
}

const colors: Record<string, string> = {
black: '#000',
// hsl(0deg 95% 70%)
red: '#fb6a6a',
// hsl(135deg 90% 70%)
green: '#6ef790',
// hsl(65deg 90% 75%)
yellow: '#eff986',
// hsl(185deg 90% 70%)
cyan: '#6eecf7',
// hsl(210deg 90% 70%)
blue: '#6eb2f7',
// hsl(325deg 90% 70%)
magenta: '#f76ebe',
lightgrey: '#f0f0f0',
darkgrey: '#888',
};

const styles: Record<string, string> = {
30: 'black',
31: 'red',
32: 'green',
33: 'yellow',
34: 'blue',
35: 'magenta',
36: 'cyan',
37: 'lightgrey',
};

const openTags: Record<string, string> = {
'1': 'font-weight:bold', // bold
'2': 'opacity:0.5', // dim
'3': '<i>', // italic
'4': '<u>', // underscore
'8': 'display:none', // hidden
'9': '<del>', // delete
};

const closeTags: Record<string, string> = {
'23': '</i>', // reset italic
'24': '</u>', // reset underscore
'29': '</del>', // reset delete
};

for (const n of [0, 21, 22, 27, 28, 39, 49]) {
closeTags[n.toString()] = '</span>';
}

/**
* Converts text with ANSI color codes to HTML markup.
*/
export function ansiHTML(text: string): string {
// Returns the text if the string has no ANSI escape code.
if (!ansiRegex().test(text)) {
return text;
}

// Cache opened sequence.
const ansiCodes: string[] = [];
// Replace with markup.
let ret = text.replace(
// biome-ignore lint/suspicious/noControlCharactersInRegex: allowed
/\x1B\[(\d+)m/g,
(_match: string, seq: string): string => {
const ot = openTags[seq];
if (ot) {
// If current sequence has been opened, close it.
if (ansiCodes.indexOf(seq) !== -1) {
ansiCodes.pop();
return '</span>';
}
// Open tag.
ansiCodes.push(seq);
return ot[0] === '<' ? ot : `<span style="${ot}">`;
}

const ct = closeTags[seq];
if (ct) {
// Pop sequence
ansiCodes.pop();
return ct;
}
return '';
},
);

// Make sure tags are closed.
const l = ansiCodes.length;
if (l > 0) {
ret += Array(l + 1).join('</span>');
}

return ret;
}

function setTags(): void {
openTags['90'] = `color:${colors.darkgrey}`;

for (const code in styles) {
const color = styles[code];
const oriColor = colors[color] || colors.black;
openTags[code] = `color:${oriColor}`;
}
}

setTags();

export default ansiHTML;
18 changes: 18 additions & 0 deletions packages/core/src/server/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -476,3 +476,21 @@ export function getServerTerminator(
}
});
}

/**
* Escape HTML characters
* @example
* escapeHtml('<div>Hello</div>') // '&lt;div&gt;Hello&lt;/div&gt;'
*/
export function escapeHtml(text: string | null | undefined): string {
if (!text) {
return '';
}
// `&` must be replaced first to avoid double-escaping
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
7 changes: 4 additions & 3 deletions packages/core/src/server/socketServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import {
import { formatStatsMessages } from '../helpers/format';
import { logger } from '../logger';
import type { DevConfig, Rspack } from '../types';
import { getCompilationId } from './helper';
import ansiHTML from './ansiHTML';
import { escapeHtml, getCompilationId } from './helper';

interface ExtWebSocket extends Ws {
isAlive: boolean;
Expand Down Expand Up @@ -293,7 +294,7 @@ export class SocketServer {
return this.sockWrite({
type: 'errors',
compilationId,
data: formattedErrors,
data: formattedErrors.map((item) => ansiHTML(escapeHtml(item))),
});
}

Expand All @@ -306,7 +307,7 @@ export class SocketServer {
return this.sockWrite({
type: 'warnings',
compilationId,
data: formattedWarnings,
data: formattedWarnings.map((item) => ansiHTML(escapeHtml(item))),
});
}

Expand Down
1 change: 1 addition & 0 deletions scripts/dictionary.txt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ corejs
corepack
craco
crossorigin
darkgrey
datauri
deepmerge
docgen
Expand Down

0 comments on commit 91c23ca

Please sign in to comment.