Skip to content

Commit a3cb04d

Browse files
authored
fix: devtools redesign (#14)
1 parent 8231839 commit a3cb04d

35 files changed

Lines changed: 1585 additions & 833 deletions

.changeset/toolbar-redesign.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@solidjs/start-devtools': patch
3+
---
4+
5+
Sync the dev toolbar redesign from Solid Start: panel layout with a call list beside a detail pane, collapsible sections, body-first content viewers, request timing, unhandled rejection capture, drag limited to the toolbar pill, and source map tracing through `@jridgewell/trace-mapping`. Stack parsing moves from `error-stack-parser` to `error-stack-parser-es/lite`, dropping the `stackframe` transitive dependency.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,11 @@
5757
"@arethetypeswrong/cli": "^0.18.5",
5858
"@changesets/cli": "^2.30.0",
5959
"@dom-expressions/compiler": "^0.50.0-next.43",
60+
"@jridgewell/trace-mapping": "^0.3.31",
6061
"@playwright/test": "^1.62.1",
6162
"@solidjs/web": "^2.0.0-rc.0",
6263
"@types/node": "^24.0.0",
63-
"error-stack-parser": "^2.1.4",
64+
"error-stack-parser-es": "^2.0.1",
6465
"html-to-image": "^1.11.13",
6566
"oxfmt": "^0.64.0",
6667
"publint": "^0.3.23",
@@ -69,7 +70,6 @@
6970
"seroval": "^1.6.0",
7071
"shiki": "^4.3.1",
7172
"solid-js": "^2.0.0-rc.0",
72-
"source-map-js": "^1.2.1",
7373
"terracotta": "2.0.0-next.6",
7474
"typescript": "^7.0.2",
7575
"vite": "^8.2.1",

pnpm-lock.yaml

Lines changed: 23 additions & 16 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/dev-toolbar/error-viewer/CodeView.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export interface CodeViewProps {
2929
line: number;
3030
}
3131

32-
const RANGE = 8;
32+
const RANGE = 15;
3333

3434
export function CodeView(props: CodeViewProps): JSX.Element | null {
3535
const lines = () =>
@@ -50,9 +50,16 @@ export function CodeView(props: CodeViewProps): JSX.Element | null {
5050
.join('\n');
5151
const highlighter = await loadHighlighter();
5252
const fileExtension = props.fileName.split(/[#?]/)[0]!.split('.').pop()?.trim();
53-
let lang = fileExtension ?? 'text';
54-
if (fileExtension === 'mjs' || fileExtension === 'cjs') {
55-
lang = 'js';
53+
// Only these grammars are loaded — anything else would make shiki
54+
// throw. Fall back to plain JS highlighting for unknown sources.
55+
let lang: 'js' | 'jsx' | 'ts' | 'tsx' = 'js';
56+
if (
57+
fileExtension === 'jsx' ||
58+
fileExtension === 'ts' ||
59+
fileExtension === 'tsx' ||
60+
fileExtension === 'js'
61+
) {
62+
lang = fileExtension;
5663
}
5764
return highlighter.codeToHtml(value, {
5865
theme: 'dark-plus',

src/dev-toolbar/error-viewer/create-stack-frame.ts

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { originalPositionFor, sourceContentFor } from '@jridgewell/trace-mapping';
2+
import type { StackFrameLite } from 'error-stack-parser-es/lite';
13
import { type Accessor, createMemo } from 'solid-js';
24
import getSourceMap from './get-source-map.js';
35

@@ -26,30 +28,38 @@ function getActualFileSource(path: string): string {
2628
return path;
2729
}
2830

29-
export function createStackFrame(stackframe: StackFrame, isCompiled: () => boolean) {
31+
export function createStackFrame(stackframe: StackFrameLite, isCompiled: () => boolean) {
3032
const data = createMemo(async () => {
3133
const source = {
32-
fileName: stackframe.fileName,
33-
line: stackframe.lineNumber,
34-
column: stackframe.columnNumber,
35-
functionName: stackframe.functionName,
34+
fileName: stackframe.file,
35+
line: stackframe.line,
36+
column: stackframe.col,
37+
functionName: stackframe.function,
3638
};
3739
if (!source.fileName) {
3840
return null;
3941
}
40-
const response = await fetch(getActualFileSource(source.fileName));
41-
if (!response.ok) {
42+
// Sources can be unreachable — node internals, extension scripts,
43+
// files outside the dev server's allowlist. Treat any failure as
44+
// "no source" instead of throwing into the error boundary.
45+
try {
46+
const url = getActualFileSource(source.fileName);
47+
const response = await fetch(url);
48+
if (!response.ok) {
49+
return null;
50+
}
51+
const content = await response.text();
52+
const sourceMap = await getSourceMap(url, content);
53+
return {
54+
source,
55+
content,
56+
sourceMap,
57+
isServer: isServerSource(source.fileName),
58+
};
59+
} catch (error) {
60+
console.warn('[solid dev toolbar] failed to load source for stack frame', error);
4261
return null;
4362
}
44-
const content = await response.text();
45-
const url = getActualFileSource(source.fileName);
46-
const sourceMap = await getSourceMap(url, content);
47-
return {
48-
source,
49-
content,
50-
sourceMap,
51-
isServer: isServerSource(source.fileName),
52-
};
5363
});
5464

5565
const info = createMemo(() => {
@@ -61,9 +71,12 @@ export function createStackFrame(stackframe: StackFrame, isCompiled: () => boole
6171

6272
if (!isCompiled() && source.line && source.column && sourceMap) {
6373
if (isServer) {
64-
const originalContent = sourceMap.sources.length
65-
? sourceMap.sourceContentFor(sourceMap.sources[0]!, true)
66-
: null;
74+
// The position is already original; only the original content needs
75+
// to be pulled out of the source map.
76+
const originalContent =
77+
sourceMap.sources.length && sourceMap.sources[0] != null
78+
? sourceContentFor(sourceMap, sourceMap.sources[0])
79+
: null;
6780
if (originalContent) {
6881
return {
6982
source: source.fileName,
@@ -74,14 +87,14 @@ export function createStackFrame(stackframe: StackFrame, isCompiled: () => boole
7487
} as StackFrameSource;
7588
}
7689
} else {
77-
const result = sourceMap.originalPositionFor({
90+
const result = originalPositionFor(sourceMap, {
7891
line: source.line,
7992
column: source.column,
8093
});
8194
if (result.source) {
8295
return {
8396
...result,
84-
content: sourceMap.sourceContentFor(result.source, true),
97+
content: sourceContentFor(sourceMap, result.source),
8598
} as StackFrameSource;
8699
}
87100
}

src/dev-toolbar/error-viewer/get-source-map.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
1-
import { type RawSourceMap, SourceMapConsumer } from 'source-map-js';
1+
import { AnyMap, type SourceMapInput, type TraceMap } from '@jridgewell/trace-mapping';
22

33
const INLINE_SOURCEMAP_REGEX = /^data:application\/json[^,]+base64,/;
44
const SOURCEMAP_REGEX =
55
/(?:\/\/[@#][ \t]+sourceMappingURL=([^\s'"]+?)[ \t]*$)|(?:\/\*[@#][ \t]+sourceMappingURL=([^*]+?)[ \t]*(?:\*\/)[ \t]*$)/;
66

7-
export default async function getSourceMap(
8-
url: string,
9-
content: string,
10-
): Promise<SourceMapConsumer | null> {
7+
export default async function getSourceMap(url: string, content: string): Promise<TraceMap | null> {
118
const lines = content.split('\n');
129
let sourceMapUrl: string | undefined;
1310
for (let i = lines.length - 1; i >= 0 && !sourceMapUrl; i--) {
@@ -27,6 +24,7 @@ export default async function getSourceMap(
2724
sourceMapUrl = parsedURL.join('/');
2825
}
2926
const response = await fetch(sourceMapUrl);
30-
const rawSourceMap: RawSourceMap = await response.json();
31-
return new SourceMapConsumer(rawSourceMap);
27+
const rawSourceMap: SourceMapInput = await response.json();
28+
// AnyMap also handles indexed ("sections") source maps
29+
return new AnyMap(rawSourceMap);
3230
}

0 commit comments

Comments
 (0)