Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 4 additions & 12 deletions packages/core/src/client/hmr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -111,7 +103,7 @@ function handleErrors({
}

if (createOverlay) {
createOverlay(overlay, html);
createOverlay(html);
}
}

Expand Down
45 changes: 11 additions & 34 deletions packages/core/src/client/overlay.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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();
});

Expand Down Expand Up @@ -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() {
Expand Down
144 changes: 144 additions & 0 deletions packages/core/src/server/overlay.ts
Original file line number Diff line number Diff line change
@@ -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 `</span>`, it means the file path contains ANSI codes.
// We need to move the `</span>` to the end of the file path.
const hasClosingSpan = file.includes('</span>') && !file.includes('<span');
const filePath = hasClosingSpan ? file.replace('</span>', '') : file;

return `<a class="file-link" data-file="${filePath}">${filePath}</a>${
hasClosingSpan ? '</span>' : ''
}`;
});
}

// HTML template for error overlay
export function genOverlayHTML(errors: string[]) {
const htmlItems = errors.map((item) =>
convertLinksInHtml(ansiHTML(escapeHtml(item))),
);
return `
<style>
.root {
position: fixed;
z-index: 9999;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow-y: scroll;
margin: 0;
background: rgba(0, 0, 0, 0.66);
cursor: pointer;
}
.container {
font-family: Menlo, Consolas, monospace;
line-height: 1.6;
width: 960px;
max-width: 85%;
color: #d8d8d8;
margin: 32px auto;
padding: 32px 40px;
position: relative;
background: #181818;
border-radius: 24px;
box-shadow: 0 19px 38px rgba(0,0,0,0.30), 0 15px 12px rgba(0,0,0,0.22);
overflow: hidden;
direction: ltr;
text-align: left;
box-sizing: border-box;
cursor: default;
}
.title {
margin: 0 0 20px;
padding-bottom: 12px;
font-size: 17px;
font-weight: 600;
color: #fb6a6a;
border-bottom: 2px solid rgba(252,94,94,.66);
}
.content {
margin: 0;
font-size: 14px;
font-family: inherit;
overflow-x: scroll;
scrollbar-width: none;
}
.content::-webkit-scrollbar {
display: none;
}
.file-link {
cursor: pointer;
color: #6eecf7;
text-decoration: underline;
text-underline-offset: 3px;
&:hover {
opacity: 0.8;
}
&:active {
opacity: 0.6;
}
}
.close {
position: absolute;
top: 27px;
right: 32px;
width: 32px;
height: 32px;
cursor: pointer;
}
.close:hover {
opacity: 0.8;
}
.close:active {
opacity: 0.6;
}
.close:before,
.close:after {
position: absolute;
left: 16px;
top: 8px;
content: ' ';
height: 18px;
width: 2px;
border-radius: 4px;
background-color: #b8b8b8;
}
.close:before {
transform: rotate(45deg);
}
.close:after {
transform: rotate(-45deg);
}
.footer {
font-size: 12px;
color: #7e6a92;
margin-top: 20px;
padding-top: 12px;
border-top: 2px solid rgba(126,106,146,.6);
}
.footer p {
margin: 4px 0 0;
}
.footer span {
color: #a88dc3;
}
</style>

<div class="root">
<div class="container">
<div class="close"></div>
<p class="title">Build failed</p>
<pre class="content">${htmlItems.join('\n\n').trim()}</pre>
<footer class="footer">
<p><span>Fix error</span>, click outside, or press Esc to close the overlay.</p>
<p>Disable overlay by setting Rsbuild's <span>dev.client.overlay</span> config to false.<p>
</footer>
</div>
</div>
`;
}
Loading
Loading