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
5 changes: 5 additions & 0 deletions .changeset/toolbar-redesign.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@solidjs/start-devtools': patch
---

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.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,11 @@
"@arethetypeswrong/cli": "^0.18.5",
"@changesets/cli": "^2.30.0",
"@dom-expressions/compiler": "^0.50.0-next.43",
"@jridgewell/trace-mapping": "^0.3.31",
"@playwright/test": "^1.62.1",
"@solidjs/web": "^2.0.0-rc.0",
"@types/node": "^24.0.0",
"error-stack-parser": "^2.1.4",
"error-stack-parser-es": "^2.0.1",
"html-to-image": "^1.11.13",
"oxfmt": "^0.64.0",
"publint": "^0.3.23",
Expand All @@ -69,7 +70,6 @@
"seroval": "^1.6.0",
"shiki": "^4.3.1",
"solid-js": "^2.0.0-rc.0",
"source-map-js": "^1.2.1",
"terracotta": "2.0.0-next.6",
"typescript": "^7.0.2",
"vite": "^8.2.1",
Expand Down
39 changes: 23 additions & 16 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 11 additions & 4 deletions src/dev-toolbar/error-viewer/CodeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export interface CodeViewProps {
line: number;
}

const RANGE = 8;
const RANGE = 15;

export function CodeView(props: CodeViewProps): JSX.Element | null {
const lines = () =>
Expand All @@ -50,9 +50,16 @@ export function CodeView(props: CodeViewProps): JSX.Element | null {
.join('\n');
const highlighter = await loadHighlighter();
const fileExtension = props.fileName.split(/[#?]/)[0]!.split('.').pop()?.trim();
let lang = fileExtension ?? 'text';
if (fileExtension === 'mjs' || fileExtension === 'cjs') {
lang = 'js';
// Only these grammars are loaded — anything else would make shiki
// throw. Fall back to plain JS highlighting for unknown sources.
let lang: 'js' | 'jsx' | 'ts' | 'tsx' = 'js';
if (
fileExtension === 'jsx' ||
fileExtension === 'ts' ||
fileExtension === 'tsx' ||
fileExtension === 'js'
) {
lang = fileExtension;
}
return highlighter.codeToHtml(value, {
theme: 'dark-plus',
Expand Down
55 changes: 34 additions & 21 deletions src/dev-toolbar/error-viewer/create-stack-frame.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { originalPositionFor, sourceContentFor } from '@jridgewell/trace-mapping';
import type { StackFrameLite } from 'error-stack-parser-es/lite';
import { type Accessor, createMemo } from 'solid-js';
import getSourceMap from './get-source-map.js';

Expand Down Expand Up @@ -26,30 +28,38 @@ function getActualFileSource(path: string): string {
return path;
}

export function createStackFrame(stackframe: StackFrame, isCompiled: () => boolean) {
export function createStackFrame(stackframe: StackFrameLite, isCompiled: () => boolean) {
const data = createMemo(async () => {
const source = {
fileName: stackframe.fileName,
line: stackframe.lineNumber,
column: stackframe.columnNumber,
functionName: stackframe.functionName,
fileName: stackframe.file,
line: stackframe.line,
column: stackframe.col,
functionName: stackframe.function,
};
if (!source.fileName) {
return null;
}
const response = await fetch(getActualFileSource(source.fileName));
if (!response.ok) {
// Sources can be unreachable — node internals, extension scripts,
// files outside the dev server's allowlist. Treat any failure as
// "no source" instead of throwing into the error boundary.
try {
const url = getActualFileSource(source.fileName);
const response = await fetch(url);
if (!response.ok) {
return null;
}
const content = await response.text();
const sourceMap = await getSourceMap(url, content);
return {
source,
content,
sourceMap,
isServer: isServerSource(source.fileName),
};
} catch (error) {
console.warn('[solid dev toolbar] failed to load source for stack frame', error);
return null;
}
const content = await response.text();
const url = getActualFileSource(source.fileName);
const sourceMap = await getSourceMap(url, content);
return {
source,
content,
sourceMap,
isServer: isServerSource(source.fileName),
};
});

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

if (!isCompiled() && source.line && source.column && sourceMap) {
if (isServer) {
const originalContent = sourceMap.sources.length
? sourceMap.sourceContentFor(sourceMap.sources[0]!, true)
: null;
// The position is already original; only the original content needs
// to be pulled out of the source map.
const originalContent =
sourceMap.sources.length && sourceMap.sources[0] != null
? sourceContentFor(sourceMap, sourceMap.sources[0])
: null;
if (originalContent) {
return {
source: source.fileName,
Expand All @@ -74,14 +87,14 @@ export function createStackFrame(stackframe: StackFrame, isCompiled: () => boole
} as StackFrameSource;
}
} else {
const result = sourceMap.originalPositionFor({
const result = originalPositionFor(sourceMap, {
line: source.line,
column: source.column,
});
if (result.source) {
return {
...result,
content: sourceMap.sourceContentFor(result.source, true),
content: sourceContentFor(sourceMap, result.source),
} as StackFrameSource;
}
}
Expand Down
12 changes: 5 additions & 7 deletions src/dev-toolbar/error-viewer/get-source-map.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import { type RawSourceMap, SourceMapConsumer } from 'source-map-js';
import { AnyMap, type SourceMapInput, type TraceMap } from '@jridgewell/trace-mapping';

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

export default async function getSourceMap(
url: string,
content: string,
): Promise<SourceMapConsumer | null> {
export default async function getSourceMap(url: string, content: string): Promise<TraceMap | null> {
const lines = content.split('\n');
let sourceMapUrl: string | undefined;
for (let i = lines.length - 1; i >= 0 && !sourceMapUrl; i--) {
Expand All @@ -27,6 +24,7 @@ export default async function getSourceMap(
sourceMapUrl = parsedURL.join('/');
}
const response = await fetch(sourceMapUrl);
const rawSourceMap: RawSourceMap = await response.json();
return new SourceMapConsumer(rawSourceMap);
const rawSourceMap: SourceMapInput = await response.json();
// AnyMap also handles indexed ("sections") source maps
return new AnyMap(rawSourceMap);
}
Loading
Loading