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/diagnostic-guide-sections.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"solid-js": patch
---

Add the missing diagnostic guide sections so every diagnostic code links to its repair guidance.
31 changes: 30 additions & 1 deletion packages/signals/tests/diagnostics.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { readFileSync } from "node:fs";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
createEffect,
Expand All @@ -17,7 +18,13 @@ import {
DEV,
OBSERVE
} from "../src/index.js";
import { emitDiagnostic, ownerPath, reportDiagnostic, setConsoleFooter } from "../src/core/dev.js";
import {
emitDiagnostic,
ownerPath,
reportDiagnostic,
setConsoleFooter,
type DiagnosticCode
} from "../src/core/dev.js";

// Several diagnostics are escaping errors, which halt the reactive system.
afterEach(() => {
Expand Down Expand Up @@ -274,6 +281,28 @@ describe("diagnostics", () => {
});

describe("diagnostics console footer", () => {
it("links every diagnostic code to an existing guide section", () => {
const source = readFileSync(new URL("../src/core/dev.ts", import.meta.url), "utf8");
const declaration = source.match(/export type DiagnosticCode =([\s\S]*?);/);
expect(declaration).not.toBeNull();
const codes = Array.from(
declaration![1].matchAll(/"([A-Z][A-Z0-9_]*)"/g),
match => match[1] as DiagnosticCode
);
expect(codes.length).toBeGreaterThan(0);
const guide = readFileSync(
new URL("../../solid/skills/reactivity-diagnostics/SKILL.md", import.meta.url),
"utf8"
);
const anchors = Array.from(guide.matchAll(/^#{3,4} ([A-Z][A-Z0-9_]*)$/gm), match =>
match[1].toLowerCase()
);
const missing = codes.filter(
code => !anchors.includes(new URL(DEV!.guideUrl(code)).hash.slice(1))
);
expect(missing).toEqual([]);
});

it("DEV.guideUrl is the repair guide's section for a code", () => {
const url = DEV!.guideUrl("STRICT_READ_UNTRACKED");
expect(url).toMatch(
Expand Down
72 changes: 66 additions & 6 deletions packages/solid/skills/reactivity-diagnostics/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,17 @@ Delete the call; observe results after the action resolves.

### NO_OWNER_EFFECT / NO_OWNER_BOUNDARY

An effect or boundary was created outside any reactive context (no root, no
component). It will never be disposed — a leak. Create it under a component
or `createRoot`, or use `runWithOwner` to attach it to an existing owner.
#### NO_OWNER_EFFECT

An effect was created outside any reactive context (no root, no component).
Create it under a component or `createRoot`, or use `runWithOwner` to attach
it to an existing owner so it can be disposed.

#### NO_OWNER_BOUNDARY

A boundary was created outside any reactive context. Like an unowned effect,
it needs an owner to dispose it. Create it under a component or `createRoot`,
or attach it to an existing owner with `runWithOwner`.

### NO_OWNER_CLEANUP

Expand Down Expand Up @@ -167,6 +175,14 @@ stored past its lifetime — re-capture the owner at call time or guard with
work: `createEffect(() => signal(), value => doWork(value))`. For a derived
value use `createMemo`; for a one-shot side effect just call the function.

### PRIMITIVE_IN_EFFECT_CALLBACK

A memo, effect, or root was created without an owner inside an effect
callback or its cleanup. The effect phase has no ambient owner, so those
computations cannot be disposed or held by a parent. Create them in the
compute phase, or attach them to an existing owner with `runWithOwner`.
Creating a signal alone does not trigger this error.

### PRIMITIVE_IN_FORBIDDEN_SCOPE

Reactive primitives cannot be created inside `createTrackedEffect` or
Expand All @@ -190,6 +206,28 @@ AsyncIterable; the value would be stored as-is, never awaited, in
production. Remove `sync: true` to use async-aware behavior, or unwrap
before returning.

## Loading boundaries

### ASYNC_OUTSIDE_LOADING_BOUNDARY

On the client, an async value was read outside a Loading boundary during
mounting. The root mount waits until the pending async work settles. Put the
pending content inside `<Loading>` and provide a fallback if the page should
show something while it waits.

During SSR, reading a source declared with `ssrSource: "client"` outside
`<Loading>` throws an error because the server cannot run that source.
Wrap the read in `<Loading>` to provide a fallback, or declare
`loadingValue`/`seedLoadingValue` to render a provisional value.

### LOADING_ON_OUTSIDE_HOLD

`on` re-armed a Loading boundary, but a source it is waiting on is also read
outside the boundary and holds the frame. The frame waits for that same
source, so the fallback cannot appear. Move the outside read under the boundary.
If the fallback should appear immediately beside the held frame, use a
`latest()` read in `on`.

## Hard failures

### REACTIVITY_HALTED
Expand All @@ -199,6 +237,15 @@ ignored. Do not treat this code as the bug — find the original error above
it (or add an error boundary with `<Errored>`) and fix
that.

### SETTLE_WALK_UNINITIALIZED_SOURCE

The runtime tried to settle a source before it produced a value or an
error. Waking its parked readers at that point would expose its initial
value instead of settled data. This is an internal consistency check,
reported in development. Keep the full diagnostic and reduce the async
sequence to a reproduction when reporting it; adding `flush()` or silencing
the diagnostic does not repair the missing value.

### INVARIANT_VIOLATION

The reactive system contradicted itself — an internal bug, not user error.
Expand All @@ -224,9 +271,22 @@ derived from other state, `createProjection` builds it.

### HUGE_FAN_IN / WIDE_SCOPE_DEPS

One computation reads very many sources, so it re-runs when any of them
change. Narrow its reads or split it into smaller memos that each track only
what they need. The message lists the sources — start with those.
#### HUGE_FAN_IN

The always-on backstop: one computation tracked 2000 or more sources in a
single pass, so it re-runs when any of them change. The message names the
computation and the count (`data: { count }`); it does NOT list the sources.
To see which sources, enable the attribution engine and read
[WIDE_SCOPE_DEPS](#wide_scope_deps), which fires far earlier (30 sources)
and names up to 12 of them. The repair is the same: narrow the reads or
split the derivation into smaller memos that each track only what they need.

#### WIDE_SCOPE_DEPS

Attribution engine only: a scope is subscribed to 30 or more sources (it
re-warns on 50% further growth). The message lists up to 12 of them — start
there, and narrow the reads or split the scope as described under
[HUGE_FAN_IN](#huge_fan_in).

### GRAPH_GROWTH

Expand Down
Loading