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/babel-source-names-locations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solidjs/babel-plugin": patch
---

`sourceNames` gains a `locations` kind (and the plugin a `root` option): component and binding labels carry a `file:line:col` for diagnostics. `createComponent(Home, props, "Home", "src/App.tsx:12:5")` points at the `<Home` opening element (DOM and SSR output); a binding's options gain `loc` after `name` — the attribute's start for an attribute effect (a merged effect points at the first attribute it lists), the hole's `{` for an insert — and a spread passes it after the tag (`spread(el, props, false, undefined, "div", "src/App.tsx:20:12")`). The path is relative to `root` (default: the working directory) with `/` separators; line and column are 1-based, so a dev server links the location straight to the source. Like the other kinds, `locations` follows `dev` when unset, `true` turns every kind on, and the object form picks; a location is only emitted beside its label, and never when Babel has no `filename` for the module.
7 changes: 7 additions & 0 deletions .changeset/compiler-source-names-locations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@solidjs/compiler": patch
---

`sourceNames` gains a `locations` kind and `transform()` a `root` option: component and binding labels carry a `file:line:col` for diagnostics, printed the same as `@solidjs/babel-plugin`'s. `createComponent(Home, props, "Home", "src/App.tsx:12:5")` points at the `<Home` opening element (DOM and SSR output); a binding's options gain `loc` after `name` — the attribute's start for an attribute effect (a merged effect points at the first attribute it lists; a property split out of `style={{…}}` / `class={{…}}` at that attribute), the hole's `{` for an insert — and a spread passes it after the tag (`spread(el, props, false, undefined, "div", "src/App.tsx:20:12")`). The path is relative to `root` (default: the working directory) with `/` separators; line and column are 1-based, so a dev server links the location straight to the source. Like the other kinds, `locations` follows `dev` when unset, `true` turns every kind on, and the object form picks; a location is only emitted beside its label, and never without a `filename`.

`transformSourceNames` gains `locations` (default `true`) and `root`: it writes `loc` beside `name` in every options literal it writes or completes — `createSignal(0, { name: "count", loc: "src/store.ts:3:22" })`, the call's start — keeping an explicit `name` (and still adding `loc`) and never overriding an explicit `loc`.
5 changes: 5 additions & 0 deletions .changeset/diagnostics-source-locations-diagnostics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solidjs/diagnostics": patch
---

Captured findings carry `loc` (the source location the runtime resolved, `file:line:col`) as a plain event field through artifacts and the JSONL form; assertion failures print it with the rest of the offending event.
5 changes: 5 additions & 0 deletions .changeset/diagnostics-source-locations-signals.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solidjs/signals": patch
---

Diagnostics carry a source location. `NodeOptions.loc` and `StoreOptions.loc` (`file:line:col`, root-relative — written by the compiler's `sourceNames` `locations` kind, not by hand) land in a `_loc` slot beside `_name` on the observe/dev node literals; store property nodes carry their store's. `DiagnosticEvent.loc` is the innermost located node on the subject's chain (its own `loc`, else the first up the owners — a signal through its registering owner), found in `emitDiagnostic` the way `ownerPath` is; a host event that sets it keeps it. `reportDiagnostic` prints it as an `at` line after the `in` line — `at src/App.tsx:18:3`, or in a browser `at http://localhost:5173/src/App.tsx:18:3`, which the devtools link to the Sources panel. `DEV.guideUrl(code)` now returns the code's page on the docs site (`https://v2.solidjs.com/reference/solid-js/diagnostics/<slug>`).
5 changes: 5 additions & 0 deletions .changeset/diagnostics-source-locations-solid.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"solid-js": patch
---

`createComponent(Comp, props, name?, loc?)` takes the JSX site the compiler emits beside the tag (`"src/App.tsx:12:5"`); the dev and observe component owner carries it on both the client and the server, so a finding raised inside a component reports `at src/App.tsx:12:5` (and `event.loc`) when nothing closer was located. The console footer is now two lines: the code's docs page for a human (`what it means and how to fix it: https://v2.solidjs.com/reference/solid-js/diagnostics/<slug>`), then the repair skill for an agent (installed path and GitHub URL, anchored to the code's section). `diagnostics.json` links each code to its docs page, and the `reactivity-diagnostics` skill documents the `at` line.
5 changes: 5 additions & 0 deletions .changeset/diagnostics-source-locations-web.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solidjs/web": patch
---

`effect` and `insert` take `loc` beside `name` in their options, and `spread` a trailing `loc` after its tag — the source site the compiler emits under `sourceNames` `locations` — so a diagnostic about a compiled binding reports `at src/App.tsx:14:7` and carries `event.loc`. Dev and observe builds keep it on the node; production ignores it.
5 changes: 3 additions & 2 deletions documentation/solid-2.0/08-dev-diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ Every console report is one entry built for a human to act on:

- The message, with the code in brackets and the repair in the text.
- An `in` line naming the owners enclosing the subject, root first — component roots as `<Name>`, computations by their `name` option or the `effect`/`computed` default (`in <App> › <TodoList> › <TodoRow> › effect`) — a compiled binding effect reads as what it writes (`span.textContent`, `div.class:active`, a hole `div.children`) when the JSX compiler's `sourceNames.bindings` is on (both `@solidjs/babel-plugin` and `@solidjs/compiler` take the same `sourceNames` option, on by default in dev builds alongside `components`); a primitive reads as the identifier it was declared as (`count`, `doubled`, `todos.title`, `createCounter.value` inside a composed primitive) when `@solidjs/compiler`'s standalone `transformSourceNames` pass has run — primitive naming is not a JSX-transform feature: the build tool applies that pass to every module, `.ts`/`.js` and JSX alike, independently of which JSX compiler handles the file (the Vite plugin's `sourceNames.primitives`, on by default in its dev and `observe` postures; solid-vite-plugin #371) — or as its `name` option otherwise. The same chain is `event.ownerPath` on the structured event. A component's name is the tag as written in source when the JSX compiler's `sourceNames.components` is on (`createComponent(Home, props, "Home")` — on by default in dev builds; the Vite plugin also enables it for the `observe` posture, so minified observe builds still read `<Home>`), otherwise the function's `.name`, which a minifier rewrites and a `lazy()` wrapper hides. Components a library invokes by value rather than by tag (a router rendering a route's `component`) carry only the function name.
- An `at` line when the build wrote source locations (the compiler's `sourceNames` `locations` kind — on by default in dev builds, an opt-in for `observe` ones): the innermost located node's `file:line:col`, relative to the project root — the binding, memo, effect, signal or store the finding is about (`loc` in its options literal), else the JSX site of the nearest enclosing component (`createComponent(Home, props, "Home", "src/App.tsx:12:5")`). In a browser it prints as the dev server's URL (`at http://localhost:5173/src/App.tsx:18:3`), which Chrome and Firefox link to the Sources panel; on the server, the bare path. The same string is `event.loc` on the structured event. Absent when nothing on the chain was located.
- For a compiled JSX binding effect (attribute, class, style, property, spread, insert), the element it writes as a second console argument — hover highlights it on the page, click jumps to it in the Elements panel. The web runtime tags binding effects with their element in dev; the core prints whatever the subject knows.
- The first report of each code ends with a footer `solid-js` registers through an internal seam of the engine (not a public `DEV` method): the installed repair skill path (`node_modules/solid-js/skills/reactivity-diagnostics/SKILL.md`) and the same file's stable GitHub URL anchored to the code's section — `DEV.guideUrl(code)`, the one place that URL is built, so a profiler track's Insights link (in dev) and the console footer agree. Perf, graph, and responsiveness codes add a second line pointing at `attribution.enable()` from `solid-js/attribution` and the `agent-loops` skill in `@solidjs/diagnostics`.
- The first report of each code ends with a footer `solid-js` registers through an internal seam of the engine (not a public `DEV` method): a line for a human — the code's page on the docs site, what it means and how to fix it (`DEV.guideUrl(code)`, the one place that URL is built, so a profiler track's Insights link (in dev) and the console footer agree) — and a line for an agent, the installed repair skill path (`node_modules/solid-js/skills/reactivity-diagnostics/SKILL.md`) and the same file's stable GitHub URL, anchored to the code's section. Perf, graph, and responsiveness codes add a second line pointing at `attribution.enable()` from `solid-js/attribution` and the `agent-loops` skill in `@solidjs/diagnostics`.

Attribution's own output (`[why-run]` chains) prints as collapsed console groups — one headline per re-run, the cause chain and dependency delta inside.

Expand Down Expand Up @@ -764,7 +765,7 @@ const events = capture.stop();

### `DEV.guideUrl(code)`

Dev builds only. The repair guide's section for a code — the `reactivity-diagnostics` skill's stable GitHub URL, anchored to the code (`…/SKILL.md#strict_read_untracked`). One place builds it: the console footer's "learn more" line and the profiler track's Insights link (`learnMoreUrl`, in dev) both read it, so a tool that renders findings elsewhere links to the same text. Dev-tier rather than observe because it is guidance for a developer, and the URL string on a retained object would be a cost every observe build paid.
Dev builds only. The code's page on the docs site — `https://v2.solidjs.com/reference/solid-js/diagnostics/<slug>`, the slug being the code lowercased with `-` for `_` (`…/diagnostics/strict-read-untracked`). One place builds it: the console footer's first line and the profiler track's Insights link (`learnMoreUrl`, in dev) both read it, so a tool that renders findings elsewhere links to the same page. Dev-tier rather than observe because it is guidance for a developer, and the URL string on a retained object would be a cost every observe build paid.

### `OBSERVE.ownerPath(subject)`

Expand Down
10 changes: 9 additions & 1 deletion packages/babel-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,16 +129,24 @@ Development output. With `hydratable`, emits the hydration walk validation helpe

### sourceNames

- Type: `boolean | { components?: boolean; bindings?: boolean }`
- Type: `boolean | { components?: boolean; bindings?: boolean; locations?: boolean }`
- Default: follows `dev` — every kind on when `dev: true`, off otherwise

Names as written in source, carried into the output so the dev and observe runtimes can label the reactive graph — in diagnostic `ownerPath`s, attribution chains, and the Performance panel tracks — even after a minifier renames everything. Unset, the option follows `dev`; `true`/`false` sets every kind (`sourceNames: false` opts a dev build out); the object form picks, and each kind it leaves unspecified follows `dev`. The production runtimes ignore the names, and production output (`dev: false`) is byte-identical whether the option is set or not. `@solidjs/compiler`'s `transform()` takes the same option with the same shape and defaults.

- `components`: emit the tag as written as a third `createComponent` argument — `<Home />` compiles to `createComponent(Home, props, "Home")`, `<Ui.Button />` to `"Ui.Button"` — so each component's owner reads `<Home>` even when a `lazy()`/HMR wrapper hides the function. Applies to DOM and SSR output; for SSR the compiler keeps the `createComponent` call it otherwise inlines to `Comp(props)`, so the server runtime labels the owner the same way (prod SSR output, without the option, is unchanged). Universal and dynamic output are unaffected.
- `bindings`: every compiled binding effect is named by what it writes, as a trailing options argument the dev and observe runtimes read and production ignores. An attribute effect gets `<tag>.<attribute>` as written — `<span textContent={label()} />` compiles to `effect(() => label(), v => …, { name: "span.textContent" })`, `class:active` to `div.class:active`, a `style={{ color: c() }}` property to `div.style:color` — and a template's merged effect lists all of its bindings (`"button.class, span.textContent"`). A hole's insert is named for the parent it fills: `<div>{count()}</div>` compiles to `insert(el, count, undefined, undefined, { name: "div.children" })`; a static child (a component call, a literal) creates no effect and gets no name. A spread passes the tag as its trailing argument (`spread(el, props, false, undefined, "div")`), and the runtime labels its attribute effect `div.spread` and its children insert `div.children`. DOM output only.
- `locations`: where the JSX is written, beside each label the other two kinds emit, as `file:line:col` — the module path relative to [`root`](#root) with `/` separators, line and column 1-based (the stack-trace convention, so a dev server links `http://localhost:5173/src/App.tsx:18:3` to the right place). A component call gets it as a fourth argument: `<Home />` compiles to `createComponent(Home, props, "Home", "src/App.tsx:12:5")`, the location of `<Home`. A binding gets `loc` after `name` in its options: an attribute effect points at the attribute (`{ name: "div.class", loc: "src/App.tsx:14:7" }`; a merged effect at the first attribute it lists; a property split out of a `style={{…}}` / `class={{…}}` object at that object attribute), a hole's insert at its `{`, and a spread passes it after the tag (`spread(el, props, false, undefined, "div", "src/App.tsx:20:12")`), the first spread attribute's start. A location is only emitted where its label is, and only when Babel knows the module's `filename` (a file outside `root` gets a `../` path). The dev and observe runtimes print it in diagnostic reports; production ignores it.

These are the JSX-level kinds — what a JSX compiler can see. Naming the primitives themselves (`createSignal(0, { name: "count" })`, `createCounter.value` inside a composed primitive) is not a JSX-transform feature and this plugin does not do it: it is `@solidjs/compiler`'s standalone `transformSourceNames` pass, which the build tool runs on every module — `.ts`, `.js`, and JSX files alike — independently of which JSX compiler handles the file. `@solidjs/vite-plugin` (its `solid: { sourceNames }` option, solid-vite-plugin #371) wires both: the JSX-level kinds through this option and primitive names through the standalone pass, for its dev and `observe` postures.

### root

- Type: `string`
- Default: the working directory (Babel's `cwd`)

Project root that `sourceNames.locations` makes the module path relative to — the build tool passes its project root so the location is a dev-server path. A relative `root` is resolved against the working directory.

### delegateEvents

- Type: `boolean`
Expand Down
28 changes: 25 additions & 3 deletions packages/babel-plugin/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ export interface SourceNamesConfig {
components?: boolean;
/** Binding effect labels: the element and attribute (or hole) each compiled effect writes. */
bindings?: boolean;
/**
* Source locations beside the labels: a `file:line:col` string (the module
* path relative to `root`, 1-based line and column) as a fourth
* `createComponent` argument and as `loc` after `name` in a binding's
* options, so a diagnostic can point at the JSX that created the node. Only
* emitted where the corresponding label is, and only when Babel knows the
* file's `filename`.
*/
locations?: boolean;
}

export interface PluginConfig {
Expand All @@ -36,14 +45,23 @@ export interface PluginConfig {
* binding effect named by what it writes — `span.textContent`,
* `div.class:active`, `div.style:color`, a hole `div.children`, a spread
* `div.spread` — as an options argument on `effect`/`insert`/`spread`;
* DOM output only. The production runtimes ignore the names. Defaults to
* DOM output only. `locations`: a `file:line:col` beside each label the
* other two kinds emit (`createComponent(Home, props, "Home",
* "src/App.tsx:12:5")`, `{ name: "div.class", loc: "src/App.tsx:14:7" }`)
* — the path relative to `root`, the line and column 1-based — so a
* diagnostic can point at the JSX; nothing when Babel has no `filename`
* for the module. The production runtimes ignore the names. Defaults to
* `dev`: unset, every kind is on in dev and off otherwise; `true`/`false`
* sets every kind; an object picks, and each kind it leaves unspecified
* follows `dev`. Primitive names (`createSignal(0, { name: "count" })`)
* are not a kind here — they come from `@solidjs/compiler`'s standalone
* `transformSourceNames` pass, which the build tool runs on every module
* independently of the JSX compiler. */
sourceNames?: boolean | SourceNamesConfig;
/** Project root the `sourceNames.locations` path is made relative to;
* defaults to the working directory (Babel's `cwd`). A relative `root` is
* resolved against the working directory. */
root?: string;
delegateEvents: boolean;
delegatedEvents: string[];
builtIns: string[];
Expand Down Expand Up @@ -115,8 +133,12 @@ const config: PluginConfig = {
*/
export function sourceNames(config: PluginConfig): Required<SourceNamesConfig> {
const value = config.sourceNames ?? config.dev;
if (typeof value === "boolean") return { components: value, bindings: value };
return { components: value.components ?? config.dev, bindings: value.bindings ?? config.dev };
if (typeof value === "boolean") return { components: value, bindings: value, locations: value };
return {
components: value.components ?? config.dev,
bindings: value.bindings ?? config.dev,
locations: value.locations ?? config.dev
};
}

/**
Expand Down
Loading
Loading