solid2-agent-kit 0.18.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/baseline.mjs CHANGED
@@ -10,13 +10,22 @@
10
10
  // and @solidjs/diagnostics `latest` is an older 2.0 rc than `next`;
11
11
  // @solidjs/vite-plugin's `next` is behind `latest`), so a bare install can
12
12
  // resolve a release older than the baseline.
13
+ //
14
+ // The JSX compilers (@solidjs/compiler, native and the default;
15
+ // @solidjs/babel-plugin under `compiler: "babel"`) are listed because compiled
16
+ // output carries behavior the guidance states (lowercase `on*` is a plain
17
+ // attribute since rc.14). Projects rarely declare them — @solidjs/vite-plugin
18
+ // pulls them in — so doctor also checks an undeclared install of any package
19
+ // here, which is where a lockfile holds an older compiler.
13
20
  export const BASELINE = {
14
- 'solid-js': { line: 2, baseline: '2.0.0-rc.9' },
15
- '@solidjs/web': { line: 2, baseline: '2.0.0-rc.9' },
16
- '@solidjs/signals': { line: 2, baseline: '2.0.0-rc.9' },
17
- '@solidjs/diagnostics': { line: 2, baseline: '2.0.0-rc.9' },
18
- '@solidjs/router': { line: 2, baseline: '2.0.0-next.27' },
19
- '@solidjs/vite-plugin': { line: 3, baseline: '3.0.0-next.44' },
21
+ 'solid-js': { line: 2, baseline: '2.0.0-rc.14' },
22
+ '@solidjs/web': { line: 2, baseline: '2.0.0-rc.14' },
23
+ '@solidjs/signals': { line: 2, baseline: '2.0.0-rc.14' },
24
+ '@solidjs/diagnostics': { line: 2, baseline: '2.0.0-rc.14' },
25
+ '@solidjs/compiler': { line: 2, baseline: '2.0.0-rc.14' },
26
+ '@solidjs/babel-plugin': { line: 2, baseline: '2.0.0-rc.14' },
27
+ '@solidjs/router': { line: 2, baseline: '2.0.0-next.37' },
28
+ '@solidjs/vite-plugin': { line: 3, baseline: '3.0.0-next.47' },
20
29
  '@solidjs/testing-library': { line: 1, baseline: '1.0.0-beta.3' },
21
30
  };
22
31
 
@@ -480,6 +480,29 @@ function reactKeyPropFindings(content) {
480
480
  // reads it from package.json / node_modules.
481
481
  const TANSTACK_ROUTER_MODULE = /^@tanstack\/(?:solid-router|router-core)$/;
482
482
 
483
+ // `<Loading on={id}>` hands the boundary an accessor: `on` is a tracked
484
+ // expression whose value is irrelevant, so a bare accessor is never read and
485
+ // the boundary never re-arms. `on={id()}`, `on={props.id}` and
486
+ // `on={[a(), b()]}` do not match. The opening tag is scanned to its own `>`
487
+ // (braces balanced) so a `fallback={<p>…</p>}` before `on` does not end it.
488
+ function loadingOnAccessorFindings(content) {
489
+ const matches = [];
490
+ for (const tag of content.matchAll(/<Loading\b/g)) {
491
+ let index = tag.index + tag[0].length;
492
+ let depth = 0;
493
+ while (index < content.length) {
494
+ const ch = content[index];
495
+ if (ch === '{') depth += 1;
496
+ else if (ch === '}') depth -= 1;
497
+ else if (ch === '>' && depth === 0) break;
498
+ index += 1;
499
+ }
500
+ const bare = content.slice(tag.index, index).match(/\son=\{\s*[A-Za-z_$][\w$]*\s*\}/);
501
+ if (bare) matches.push({ index: tag.index + bare.index });
502
+ }
503
+ return matches;
504
+ }
505
+
483
506
  // Local names bound by `import [Default,] { a, b as c } from "<module>"`.
484
507
  function importedNames(content, moduleName) {
485
508
  const names = new Set();
@@ -672,7 +695,7 @@ const CHECKS = [
672
695
  pattern: /<(?:HashRouter|MemoryRouter|Route|Navigate|A|FileRoutes|StartClient|StartServer)\b/g,
673
696
  exemptImportsFrom: TANSTACK_ROUTER_MODULE,
674
697
  message:
675
- 'Solid Router 0.x/1.x or SolidStart JSX. Define routes with createRouter({ routes }) / fileRoutes(pageRoutes) and plain <a href={Router.paths...}> (link state: automatic aria-current/data-active/data-pending + CSS, or useLinkState).',
698
+ 'Solid Router 0.x/1.x or SolidStart JSX. Define routes with createRouter({ routes }) / fileRoutes(pageRoutes) and plain <a href={Router.paths...}> (link state: automatic aria-current/data-active + CSS, data-pending via createRouter({ links: pendingLinks }), or useLinkState).',
676
699
  },
677
700
  {
678
701
  id: 'meta-provider',
@@ -727,6 +750,23 @@ const CHECKS = [
727
750
  message:
728
751
  'React DOM prop. Native dblclick is onDblClick; keypress is onKeyDown; Solid has no suppressHydrationWarning.',
729
752
  },
753
+ {
754
+ // Only `on` + uppercase binds an event: since rc.14 a lowercase `on*`
755
+ // attribute is a plain attribute (dev warns [LOWERCASE_EVENT_ATTRIBUTE]),
756
+ // and a 1.x `on:click` is a plain namespaced attribute — neither runs the
757
+ // handler. A real event name in lowercase is always a leftover. Anchored
758
+ // on `={` (a JSX expression attribute): `const onmessage = ...`,
759
+ // `ws.onerror = fn`, and HTML strings (`onerror="…"`) are not matches,
760
+ // and neither is a string expression (`onclick={"track()"}`) — a string
761
+ // inline-handler attribute is the one legitimate lowercase use. The names
762
+ // are the GlobalEventHandlers / WindowEventHandlers set, so a custom
763
+ // attribute that merely starts with `on` (`one={…}`) is never flagged.
764
+ id: 'solid1-lowercase-event',
765
+ pattern:
766
+ /(?<=\s)on(?:abort|afterprint|animation(?:cancel|end|iteration|start)|auxclick|beforeinput|beforematch|beforeprint|beforetoggle|beforeunload|blur|cancel|canplay|canplaythrough|change|click|close|command|contextlost|contextmenu|contextrestored|copy|cuechange|cut|dblclick|drag|dragend|dragenter|dragleave|dragover|dragstart|drop|durationchange|emptied|ended|error|focus|focusin|focusout|formdata|fullscreenchange|fullscreenerror|gotpointercapture|hashchange|input|invalid|keydown|keypress|keyup|languagechange|load|loadeddata|loadedmetadata|loadstart|lostpointercapture|message|messageerror|mousedown|mouseenter|mouseleave|mousemove|mouseout|mouseover|mouseup|offline|online|pagehide|pagereveal|pageshow|pageswap|paste|pause|play|playing|pointercancel|pointerdown|pointerenter|pointerleave|pointermove|pointerout|pointerover|pointerrawupdate|pointerup|popstate|progress|ratechange|rejectionhandled|reset|resize|scroll|scrollend|search|securitypolicyviolation|seeked|seeking|select|selectionchange|selectstart|slotchange|stalled|storage|submit|suspend|timeupdate|toggle|touchcancel|touchend|touchmove|touchstart|transitioncancel|transitionend|transitionrun|transitionstart|unhandledrejection|unload|volumechange|waiting|wheel)\s*=\s*\{(?!\s*["'`])/g,
767
+ message:
768
+ 'Lowercase `on*` is a plain attribute in Solid 2 (dev warns [LOWERCASE_EVENT_ATTRIBUTE]), not an event binding. Use the camelCase prop (onClick).',
769
+ },
730
770
  {
731
771
  id: 'history-nav',
732
772
  pattern: /\b(?:history\.pushState|history\.replaceState|window\.location\.href\s*=)/g,
@@ -749,6 +789,13 @@ const CHECKS = [
749
789
  message:
750
790
  'Pass the accessor to isPending/latest, not the call: isPending(user), not isPending(user()). Calling it first evaluates the read before the helper runs.',
751
791
  },
792
+ {
793
+ // `<Loading on={id}>` — the accessor is never read, so `on` never re-arms.
794
+ id: 'loading-on-accessor',
795
+ find: loadingOnAccessorFindings,
796
+ message:
797
+ '`on` is a tracked dependency list, not a key: a bare accessor is never read, so the boundary never re-arms. Read it — on={id()} — or list several inputs: on={[a(), b()]}.',
798
+ },
752
799
  {
753
800
  id: 'dynamic-jsx',
754
801
  pattern: /<Dynamic[\s/>]/g,
@@ -1263,7 +1310,7 @@ const SOLID2_LINE_PACKAGES = [
1263
1310
  // Packages doctor checks the version of: the Solid 1.x lines above, then any
1264
1311
  // release older than the kit's baseline (bin/baseline.mjs) — a bare
1265
1312
  // `npm i @solidjs/web` installs `latest`, 2.0.0-rc.0, which the current
1266
- // @solidjs/vite-plugin's `@solidjs/web ^2.0.0-rc.9` peer rejects.
1313
+ // @solidjs/vite-plugin's `@solidjs/web ^2.0.0-rc.13` peer rejects.
1267
1314
  const VERSION_CHECKED = [...new Set([...SOLID2_LINE_PACKAGES.map((p) => p.name), ...Object.keys(BASELINE)])];
1268
1315
 
1269
1316
  // The lower bound of a single-comparator range ("^2.0.0-rc.0", "~2.0.0-rc.8",
@@ -1285,6 +1332,19 @@ function versionProblem(name, version, declared) {
1285
1332
  return null;
1286
1333
  }
1287
1334
 
1335
+ // An undeclared package on the baseline's major that a dependency pulled in
1336
+ // (the JSX compilers under @solidjs/vite-plugin): only the floor applies — a
1337
+ // lockfile keeps the version it first resolved. Another major is someone
1338
+ // else's copy, not the one the kit's guidance describes.
1339
+ function transitiveProblem(name, installed) {
1340
+ const { line, baseline } = BASELINE[name];
1341
+ if (!LOWER_BOUND.test(installed) || compareVersions(installed, `${line}.0.0-0`) < 0 || compareVersions(installed, `${line + 1}.0.0-0`) >= 0) {
1342
+ return null;
1343
+ }
1344
+ if (compareVersions(installed, baseline) >= 0) return null;
1345
+ return `older than ${baseline}, the release this kit's guidance is verified against. It is not in package.json, so a dependency installed it and the lockfile holds the old version: update it (\`npm update ${name}\`, \`pnpm update ${name}\`, or reinstall without the lockfile entry).`;
1346
+ }
1347
+
1288
1348
  function installedVersion(target, name) {
1289
1349
  try {
1290
1350
  return JSON.parse(readFileSync(join(target, 'node_modules', name, 'package.json'), 'utf8')).version;
@@ -1312,7 +1372,12 @@ function doctorFindings(target) {
1312
1372
  for (const name of VERSION_CHECKED) {
1313
1373
  const id = SOLID2_LINE_PACKAGES.find((p) => p.name === name)?.id ?? `${name.replace('@solidjs/', '')}-version`;
1314
1374
  const range = deps[name];
1315
- if (typeof range !== 'string') continue;
1375
+ if (typeof range !== 'string') {
1376
+ const installed = BASELINE[name] && installedVersion(target, name);
1377
+ const transitive = installed && transitiveProblem(name, installed);
1378
+ if (transitive) report(id, `node_modules has ${name} ${installed} (not in package.json) — ${transitive}`);
1379
+ continue;
1380
+ }
1316
1381
  const declared = versionProblem(name, range, true);
1317
1382
  if (declared) {
1318
1383
  report(id, `package.json pins ${name} "${range}" — ${declared}`);
@@ -14,7 +14,8 @@ Steps:
14
14
  4. Review every changed `.tsx`/`.jsx`/`.ts` file against the checklist, prioritizing what
15
15
  regexes cannot see:
16
16
  - reactive reads at component-body top level, or extracted into plain `const`s;
17
- - async computations reading reactive inputs **after** the first `await`;
17
+ - async computations reading reactive inputs **after** the first `await` (dev reports
18
+ `[UNTRACKED_READ_AFTER_AWAIT]` on V8);
18
19
  - effects that copy state instead of deriving it, or reading stores in the apply phase;
19
20
  - `<For>` over server/refetched rows without a stable-id key function;
20
21
  - core `action` bodies that are not generators (plain `async` functions) or that write
@@ -23,7 +24,8 @@ Steps:
23
24
  - `<Loading>`/`<Errored>` wrapping page chrome instead of the data slot, or
24
25
  `const u = user()` extracted then passed (a real parent-side read). Passing
25
26
  `user={user()}` is the colorless form — do not "fix" it into accessors or
26
- `Promise<User>` props;
27
+ `Promise<User>` props; `<Loading on={id}>` handed the accessor instead of a read
28
+ (`on={id()}`), or the boundary's data also read outside it (`[LOADING_ON_OUTSIDE_HOLD]`);
27
29
  - `latest(selectedId)` as the default highlight (hold is the default);
28
30
  `isPending` treated as a global spinner instead of a per-expression question;
29
31
  - treating `<Errored>` as a terminal ErrorBoundary, or expecting it to catch
@@ -66,15 +68,17 @@ Steps:
66
68
  (`filter` for removal is fine — survivors keep identity; only fresh-tree wholesale
67
69
  replacement needs `reconcile` / the derived form);
68
70
  - hand-rolled link-active comparisons (`location.pathname === ...`) instead of the
69
- automatic `aria-current` / `data-active` / `data-pending` attributes (+ CSS) or
71
+ automatic `aria-current` / `data-active` attributes (+ CSS) — `data-pending` is
72
+ opt-in via `createRouter({ links: pendingLinks })` — or
70
73
  `useLinkState` / `useIsRouting`; hand-rolled query parsing instead of
71
- `useSearchParams`;
74
+ `useSearchParams`; <!-- upstream:pending-links-opt-in -->
72
75
  - search inputs bound to a held value without `latest`, effect-based debounce, fully
73
76
  controlled forms without `name`s, `{ success: false }` result objects instead of
74
77
  thrown failures, or router forms without server-side validation (`throw respond(...)`)
75
78
  and `useSubmissions` inline errors;
76
79
  - browser-only *values* handled with a `clientOnly` component split or an `isServer`
77
- branch instead of `ssrSource: "client"` / `"hybrid"` on the memo/signal/derived store;
80
+ branch instead of `ssrSource: "client"` on the memo/signal/derived store (`"hybrid"`
81
+ only continues an async-iterable source after hydration <!-- upstream:ssr-source-hybrid-stream -->);
78
82
  - held writes with no feedback (`isPending` / `latest` / optimistic value / `affects()`)
79
83
  — the `[SILENT_HOLD]` shape — and dev-console diagnostics (`[STRICT_READ_UNTRACKED]`,
80
84
  attribution-only store/list/effect costs) left unaddressed;
@@ -5,4 +5,4 @@
5
5
  - After editing TSX, run the `solid2-kit check` mechanical gate — it fails on React/Solid 1.x tokens and props destructuring. Use the project's `lint:solid` script if one exists; otherwise `npx solid2-agent-kit check` (with the kit as a devDependency) or `npx github:lightsound/solid2-agent-kit check`. An edit-time hook may also run this gate automatically after each file edit — treat its findings as errors to fix immediately, not warnings. Do not install `eslint-plugin-solid` (built for Solid 1.x; misreads Solid 2 idioms).
6
6
  - After editing `package.json`, `tsconfig*.json`, or root config files (Vite, ESLint), run `solid2-kit doctor` — it fails on React/Solid 1.x wiring: `react`/`vite-plugin-solid`/`eslint-plugin-solid`/SolidStart dependencies, 1.x `solid-js` ranges, and `jsx`/`jsxImportSource` values other than `preserve`/`@solidjs/web`.
7
7
  - The kit's guidance and gate files (`.cursor/rules/solid-2.mdc`, the `solid-2` skill directories, hook entries, managed blocks in this file and `CLAUDE.md`) are guardrails, not obstacles. Never edit, weaken, bypass, or delete them to make a failure go away — fix the flagged code. If a guardrail seems wrong, stop and tell the user.
8
- - Solid 2.0 shipped recently and breaks with 1.x — never trust pre-2.0 Solid docs, tutorials, or training knowledge. Verify APIs against https://v2-rebuild--solid-docs-v2.netlify.app/llms.txt (markdown mirror of https://v2.solidjs.com/, which blocks non-browser fetchers). If the project has `@solidjs/router`, server functions, or `@solidjs/meta`, use those official pages (see the skill's official-docs index) — not React Router, Next.js, SolidStart 1.x, or Solid Router 0.x/1.x JSX `<Route>`/`<A>`. Core `action` from `solid-js` is a generator transaction; router `action`/`query` from `@solidjs/router` are POST forms and a read cache. Core `refresh(source)`, router `revalidate(key)`, and server-function `return reload(...)` are three different APIs. Pass the accessor to `isPending(user)`, not `isPending(user())`. Pass async values as values (`user={user()}`) — JSX props are lazy; types stay `User`, not `Promise<User>`. `<Loading>` wraps the read, not the memo. Nested fetches run in parallel (components do not suspend). `isPending` is a per-expression question (`isPending(() => props.story)` / `isPending(selectedId)`), not a global spinner; `latest(selectedId)` is opt-in so highlight and content stay consistent by default. `<Errored>` heals when the source succeeds. Pass a function to `render`/`hydrate`: `render(() => <App />, root)`. Component teardown is `onSettled`, not `onCleanup` — return the cleanup; `onCleanup`, memo/effect creation, and `flush()` throw inside the `onSettled` callback in dev. Effect `apply` functions use block bodies (`(t) => { document.title = t; }`): any return value other than a cleanup function halts reactivity. Stores are written only through the setter's draft — writes to the store itself are silently ignored, and `Map`/`Set`/`Date` inside a store are not tracked. Shared state, app-wide included, is created in a provider at the root of `App` (`createContext<T>()` + `useX()`), never a module-level signal/store (one instance serves every SSR request). Errors thrown out of an `action` never reach `<Errored>` — catch them in the action or `.catch` the call. One router instance; in-app navigation is `useNavigate` / `<a href>`, not `window.location`. Types: `solid-js` exports no `JSX` namespace — markup types are `JSX.Element` from `@solidjs/web` or `Element` from `solid-js` (alias it `SolidElement` next to DOM code) <!-- upstream:jsx-namespace-absent -->. Reset-a-signal-on-change is a writable derivation (`createSignal(() => { source(); return initial; })`), never an effect calling the setter. Client mode mounts with `render()` into an empty body — no hydration mismatch exists, so never defer initial values to post-mount React-style (that can overwrite persisted state). Client → server is additive (same setters + `action` + server functions): do not rewrite App with loading/error branches, disable optimistic rows until ack, invent tRPC, or refetch in `hydrate`/`onSettled`. The graph continues across the wire (`live()` owns the client connection; `<Reveal>` not a delayed stream). A server bundle uses `handleRequest` / Fetchable `fetch`; client-only start is static `dist/client`. One browser-only value takes `ssrSource` on the memo/signal/derived store (`client`/`hybrid`), not a `clientOnly` component split. Router link state is automatic (`aria-current`/`data-active`/`data-pending` + CSS; `useLinkState`/`useIsRouting` in JSX); search params via `useSearchParams`; router forms validate on the server (`throw respond({issues},{400})`) with inline errors from `useSubmissions`. Confirmations on another channel — fire-and-forget sends, and mutations whose result is read through `live()` — wait with `yield until(predicate, {timeout})`, not `refresh()`. Declare `live(GET(fn))` (`live()` outermost). Debounce at the handler; inputs feeding a request bind `latest(query)`. Dev diagnostics are errors: `[STRICT_READ_UNTRACKED]` means a read outside a tracking scope, `[ASYNC_STORE_SETTER]` means a store setter callback awaited (the draft is a synchronous transaction — await first, then write) <!-- upstream:async-store-setter-throws -->, and a held write needs `isPending`/`latest`/optimistic/`affects` feedback or dev + attribution reports `[SILENT_HOLD]`. Error monitoring is `configureClientErrors` / `configureServerErrors` (or per root `render(fn, root, undefined, { onError })` — options are the 4th argument), not a side effect inside `<Errored fallback>`; the server hook must return a reference or nothing, never the error itself. `<Dynamic>` is deprecated — use `dynamic()`. <!-- upstream:dynamic-deprecated --> Observability is on by default in development: whenever a change touches stores, lists, async, actions, or effects, keep the dev console free of `[CODE]` findings and run an attribution session (`/__solid/diagnostics` `begin` → interact → `whyDidRun` / `costs` → `end` with `@solidjs/diagnostics` installed, otherwise `isDev`-guarded `attribution.enable({ log: false })` + `why()` / `costs()`) before finishing. `vite dev` is always the dev build; `solid({ observe: true })` is a production opt-in.
8
+ - Solid 2.0 shipped recently and breaks with 1.x — never trust pre-2.0 Solid docs, tutorials, or training knowledge. Verify APIs against https://v2-rebuild--solid-docs-v2.netlify.app/llms.txt (markdown mirror of https://v2.solidjs.com/, which blocks non-browser fetchers). If the project has `@solidjs/router`, server functions, or `@solidjs/meta`, use those official pages (see the skill's official-docs index) — not React Router, Next.js, SolidStart 1.x, or Solid Router 0.x/1.x JSX `<Route>`/`<A>`. Core `action` from `solid-js` is a generator transaction; router `action`/`query` from `@solidjs/router` are POST forms and a read cache. Core `refresh(source)`, router `revalidate(key)`, and server-function `return reload(...)` are three different APIs. Pass the accessor to `isPending(user)`, not `isPending(user())`. Pass async values as values (`user={user()}`) — JSX props are lazy; types stay `User`, not `Promise<User>`. `<Loading>` wraps the read, not the memo. Nested fetches run in parallel (components do not suspend). `isPending` is a per-expression question (`isPending(() => props.story)` / `isPending(selectedId)`), not a global spinner; `latest(selectedId)` is opt-in so highlight and content stay consistent by default. `<Errored>` heals when the source succeeds. Pass a function to `render`/`hydrate`: `render(() => <App />, root)`. Component teardown is `onSettled`, not `onCleanup` — return the cleanup; `onCleanup`, memo/effect creation, and `flush()` throw inside the `onSettled` callback in dev. Creating a memo/effect/root inside an effect's `apply` callback throws in dev too (`[PRIMITIVE_IN_EFFECT_CALLBACK]`) — create it in the body or compute phase. <!-- upstream:primitive-in-effect-callback --> Effect `apply` functions use block bodies (`(t) => { document.title = t; }`): any return value other than a cleanup function halts reactivity. Stores are written only through the setter's draft — writes to the store itself are silently ignored, and `Map`/`Set`/`Date` inside a store are not tracked. Shared state, app-wide included, is created in a provider at the root of `App` (`createContext<T>()` + `useX()`), never a module-level signal/store (one instance serves every SSR request). Errors thrown out of an `action` never reach `<Errored>` — catch them in the action or `.catch` the call. One router instance; in-app navigation is `useNavigate` / `<a href>`, not `window.location`. Types: `solid-js` exports no `JSX` namespace — markup types are `JSX.Element` from `@solidjs/web` or `Element` from `solid-js` (alias it `SolidElement` next to DOM code) <!-- upstream:jsx-namespace-absent -->. Reset-a-signal-on-change is a writable derivation (`createSignal(() => { source(); return initial; })`), never an effect calling the setter. Client mode mounts with `render()` into an empty body — no hydration mismatch exists, so never defer initial values to post-mount React-style (that can overwrite persisted state). Client → server is additive (same setters + `action` + server functions): do not rewrite App with loading/error branches, disable optimistic rows until ack, invent tRPC, or refetch in `hydrate`/`onSettled`. The graph continues across the wire (`live()` owns the client connection; `<Reveal>` not a delayed stream). A server bundle uses `handleRequest` / Fetchable `fetch`; client-only start is static `dist/client`. One browser-only value takes `ssrSource: "client"` on the memo/signal/derived store, not a `clientOnly` component split. Router link state is `aria-current`/`data-active` + CSS (`data-pending` is opt-in via `createRouter({ links: pendingLinks })`; `useLinkState`/`useIsRouting` in JSX) <!-- upstream:pending-links-opt-in -->; search params via `useSearchParams`; router forms validate on the server (`throw respond({issues},{400})`) with inline errors from `useSubmissions`. Confirmations on another channel — fire-and-forget sends, and mutations whose result is read through `live()` — wait with `yield until(predicate, {timeout})`, not `refresh()`. Declare `live(GET(fn))` (`live()` outermost). Debounce at the handler; inputs feeding a request bind `latest(query)`. Dev diagnostics are errors: `[STRICT_READ_UNTRACKED]` means a read outside a tracking scope, `[ASYNC_STORE_SETTER]` means a store setter callback awaited (the draft is a synchronous transaction — await first, then write) <!-- upstream:async-store-setter-throws -->, and a held write needs `isPending`/`latest`/optimistic/`affects` feedback or dev + attribution reports `[SILENT_HOLD]`. Error monitoring is `configureClientErrors` / `configureServerErrors` (or per root `render(fn, root, undefined, { onError })` — options are the 4th argument), not a side effect inside `<Errored fallback>`; the server hook must return a reference or nothing, never the error itself. `<Dynamic>` is deprecated — use `dynamic()`. <!-- upstream:dynamic-deprecated --> Observability is on by default in development: whenever a change touches stores, lists, async, actions, or effects, keep the dev console free of `[CODE]` findings and run an attribution session (`/__solid/diagnostics` `begin` → interact → `whyDidRun` / `costs` → `end` with `@solidjs/diagnostics` installed, otherwise `isDev`-guarded `attribution.enable({ log: false })` + `why()` / `costs()`) before finishing. `vite dev` is always the dev build; `solid({ observe: true })` is a production opt-in.
@@ -32,7 +32,11 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
32
32
  Never write "a signal plus an effect that syncs it". That includes **reset**: to clear
33
33
  a signal when another reactive value changes, use a writable derivation —
34
34
  `createSignal(() => { source(); return initial; })` (re-runs when `source()` changes;
35
- the setter still works) — never a `createEffect` whose apply calls the setter. The
35
+ the setter still works) — never a `createEffect` whose apply calls the setter. A write
36
+ lands at once; on the next source change the function re-runs receiving the write as
37
+ `prev` (a store receives it as the draft), so a function that ignores `prev` resets on
38
+ every source change — including a write staged in the same update — while one that
39
+ honors `prev` can keep the local value. <!-- upstream:derived-write-prev --> The
36
40
  derivation also moves the `source()` read to the read site, under its boundaries.
37
41
  (React needs `useState(props.x)` plus a `previousX` comparison reset for the same
38
42
  derive-override-reset behavior.)
@@ -59,6 +63,9 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
59
63
  `event.currentTarget` (the element that owns the handler), not `event.target`
60
64
  (a nested child). Never pass a setter as the handler (`onClick={setCount}`) —
61
65
  that writes the event object; wrap it: `onClick={() => setCount((c) => c + 1)}`.
66
+ Only `on` + uppercase binds an event: lowercase `onclick` is a plain attribute
67
+ (dev warns `[LOWERCASE_EVENT_ATTRIBUTE]`), not a handler.
68
+ <!-- upstream:lowercase-event-attribute -->
62
69
  Debounce at the handler (`onInput={debounce(...)}`), never as an effect copying
63
70
  one signal into another.
64
71
  7. **Lists use `<For>`**, never `{list().map(...)}` in reactive JSX and never `key` props (only `@solidjs/meta` tags take `key`, as the tag's identity).
@@ -125,14 +132,18 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
125
132
  must `refresh()` that source — type that prop `SourceAccessor<User>` (`Accessor<User>`
126
133
  is TS2345 at `refresh`) — `isPending(() => props.user)` works on the
127
134
  value. Read every reactive input **before the first
128
- `await`** — post-`await` reads do not subscribe, and in production the computation can
135
+ `await`** — post-`await` reads do not subscribe (dev reports `[UNTRACKED_READ_AFTER_AWAIT]`
136
+ on V8 engines), and in production the computation can
129
137
  sit pending with no retry. No `useEffect` + `setState` fetching, no `createResource`.
130
138
  `<Loading>` wraps the data slot, not page chrome. After first paint it keeps content
131
- during refetch (`isPending` for the indicator). Use `on={id()}` (the *value*, not the
132
- accessor) only when that identity change should show the fallback again. `on` compares
133
- with `!==` (several keys: one string such as `` `${a()}/${b()}` ``, never a fresh array)
134
- and takes effect only when no reader outside the boundary waits on the same write
135
- (that is how one slow panel stops holding the page). Do not start `fetch` (or any request) at component-body top
139
+ during refetch (`isPending` for the indicator). Add `on={id()}` (a tracked *read*, not
140
+ the accessor `on={id}`) only when that change of subject should show the fallback again.
141
+ `on` is a dependency list, not a compared key: its value is irrelevant, a change to
142
+ anything it reads re-arms the boundary (several inputs: `on={[a(), b()]}`), and the
143
+ fallback lands with the frame of the change that caused it. <!-- upstream:loading-on-dependency-list -->
144
+ It can never show while the same data is also read outside the boundary (dev reports
145
+ `[LOADING_ON_OUTSIDE_HOLD]`) — move that read under the boundary so one hold owns the
146
+ data; that is how one slow panel stops holding the page. Do not start `fetch` (or any request) at component-body top
136
147
  level — that runs once at mount and is not a reactive source. Do not `try/catch` `NotReadyError` around a read, and do not
137
148
  use `loadingValue` / `seedLoadingValue` as the default first-flight UI — those skip
138
149
  `<Loading>`. `{latest(() => x())}` is a preview, not the visible answer, and not a
@@ -196,7 +207,11 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
196
207
  updated *synchronous* state or DOM. Waiting on an async memo is
197
208
  `await resolve(() => value())` (or Testing Library async queries), not `flush()`.
198
209
  Never call `flush()` inside an `action`, an `onSettled` callback (throws in dev), or an
199
- effect callback (a no-op, `[FLUSH_IN_EFFECT_CALLBACK]`).
210
+ effect callback (a no-op, `[FLUSH_IN_EFFECT_CALLBACK]`). Never create a memo, effect,
211
+ or root inside an effect callback either — it throws in dev
212
+ (`[PRIMITIVE_IN_EFFECT_CALLBACK]`, `runWithOwner(null, …)` included); create it in the
213
+ body or compute phase, or under `runWithOwner(owner, …)` with an owner captured in
214
+ the body. <!-- upstream:primitive-in-effect-callback -->
200
215
  14. Do not port these React tools — they have no Solid equivalent because the problems they
201
216
  solve don't exist: `useCallback`, `React.memo`, `forwardRef`, `useSyncExternalStore`,
202
217
  dependency arrays, `startTransition`/`useTransition` (updates are held and coordinated
@@ -240,7 +255,9 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
240
255
  from `@solidjs/web` — do not call `captureException` inside `<Errored fallback>`.
241
256
  The server hook's return value replaces the sanitized error on the wire: return
242
257
  nothing or a reference id, never the error itself (message/stack/secrets would
243
- reach the browser).
258
+ reach the browser). Failures outside render and server functions (middleware,
259
+ request setup) reach it via `reportRequestFailure(error, event)` as
260
+ `kind: "request"`. <!-- upstream:report-request-failure -->
244
261
  Making a client store "real" is additive: same setters, wrap mutations in
245
262
  `action`, swap in the **function form** `createOptimisticStore(() => api.list(), [])`
246
263
  (with `refresh` after the `yield`) and a file of server functions — do
@@ -323,9 +340,10 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
323
340
  render. `NoHydration` / `Hydration` split hydration *ownership*; they do not choose
324
341
  visible content — `clientOnly` is for components that must never run on the server.
325
342
  A single browser-only *value* takes `ssrSource` on the memo/signal/derived store
326
- (`"client"` skips the server value and computes after hydration, e.g. `localStorage`;
327
- `"hybrid"` re-runs after adopting the serialized value, e.g. window size mixed with
328
- server data) — do not split a component via `clientOnly` for one value. An
343
+ (`"client"` skips the server value and computes after hydration, e.g. `localStorage`,
344
+ or server data mixed with the window size; `"hybrid"` adopts the serialized value and
345
+ then continues an async-iterable source on the client — for a sync or promise compute
346
+ it is identical to `"server"`, no re-run <!-- upstream:ssr-source-hybrid-stream -->) — do not split a component via `clientOnly` for one value. An
329
347
  `onSettled` fill-in remains a valid alternative.
330
348
  Pass a **function** to `render` / `hydrate` / `renderToString` / `renderToStream`
331
349
  (`render(() => <App />, root)`), never `render(<App />, root)`. `hydrate` when the
@@ -348,7 +366,8 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
348
366
  link previews) takes `{ deferStream: true }` on its memo; keep the boundary. `pipe` / `pipeTo` / `readable` each consume
349
367
  a stream render — use exactly one. `createRoot` is for tests, libraries, and
350
368
  non-render entry points; inside a component let `render` / the component owner
351
- own the scope. Async reads inside `<Portal>` start on the client — hoist the
369
+ own the scope (a root created under an owner is disposed with it — detach on purpose
370
+ with `runWithOwner(null, () => createRoot(...))`). Async reads inside `<Portal>` start on the client — hoist the
352
371
  read above the portal. Navigation-shaped updates (a setter, then async computeds holding previous values)
353
372
  do not need core `action` — reach for it only when writes happen *after* async work.
354
373
  Invoke actions from handlers, not from memos or effects.
@@ -381,9 +400,14 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
381
400
  the route is matched. One router instance —
382
401
  Solid Router does not support nested `<Router>` / nested `createRouter`.
383
402
  In-app navigation is `useNavigate` or `<a href={Router.paths...}>`, not
384
- `window.location` / `history.pushState`. Link state is automatic
385
- (`aria-current` / `data-active` / `data-pending` + CSS; `useLinkState` /
386
- `useIsRouting` in JSX) — never hand-rolled `location.pathname` comparisons. Trusted identity is
403
+ `window.location` / `history.pushState`. Link state needs no component code:
404
+ `aria-current` (same path and same query) and `data-active` (path prefix, the
405
+ root exact-only) are automatic; `data-pending` is opt-in via
406
+ `createRouter({ links: pendingLinks })`. Style them in CSS — nav highlighting on
407
+ `data-active` (a nav link to `/products` loses `aria-current` on
408
+ `/products?page=2`); `useLinkState` /
409
+ `useIsRouting` in JSX — never hand-rolled `location.pathname` comparisons.
410
+ <!-- upstream:pending-links-opt-in --> Trusted identity is
387
411
  `getRequestEvent()`, never a caller-supplied user id. Unscripted (no-JS) POST
388
412
  forms use a router `action` over a `(form: FormData)` server function (the post
389
413
  arrives as one `FormData`; other signatures make `action={save}` TS2322) —
@@ -437,9 +461,11 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
437
461
  experimental `serverFunctions.components` flag. Server-function arguments are JSON:
438
462
  send `Date` / `Map` / `Set` as ISO strings / arrays (they throw otherwise; one `File` /
439
463
  `Blob` / `FormData` argument travels natively). `enableRichArguments()` from
440
- `@solidjs/web/server-functions/rich-args` (at `src/App.tsx` module scope) lifts that,
441
- but in rc.9 importing it fails `vite build` (`"./client" is not exported`) <!-- upstream:rich-args-vite-build https://github.com/solidjs/solid/issues/3627 -->. Declare live reads as `live(GET(fn))` —
442
- `live()` outermost. A plain `async function*` server function is an event stream on
464
+ `@solidjs/web/server-functions/rich-args` (at `src/App.tsx` module scope) lifts that
465
+ (its import builds cleanly since 2.0.0-rc.10 — a `resolve.dedupe: ["@solidjs/web"]`
466
+ workaround left from rc.9 is no longer needed) <!-- upstream:rich-args-vite-build https://github.com/solidjs/solid/issues/3627 -->. Declare live reads as `live(GET(fn))` —
467
+ `live()` outermost; an undeclared async iterable still producing while a document
468
+ renders is `[SSR_UNDECLARED_LIVE_SOURCE]` in dev. A plain `async function*` server function is an event stream on
443
469
  one connection (no reconnect when it drops); `live()` is one value that changes over
444
470
  time and reconnects. `live()` connection state is
445
471
  `source.onstatus` (`"connected"` / `"reconnecting"` / `"closed"`), never a
@@ -493,22 +519,27 @@ directory (default `src/`).
493
519
  | `startTransition`, `useTransition` | automatic held updates + `isPending` |
494
520
  | `onError` / `catchError` | `<Errored>` or effect bundle `{ effect, error }` |
495
521
  | `from(...)` / `observable(...)` (the solid-js helpers) | inbound: async iterable from a memo; outbound: split effect |
496
- | `createDynamic(...)`, `<Dynamic component={...}>` (deprecated in 2.0) <!-- upstream:dynamic-deprecated --> | `dynamic(source)` from `@solidjs/web` |
522
+ | `createDynamic(...)`, `<Dynamic component={...}>` (deprecated in 2.0) <!-- upstream:dynamic-deprecated --> | `dynamic(source)` from `@solidjs/web` — `dynamicComponent(source)` when the source only answers with a component <!-- upstream:dynamic-component-export --> |
497
523
  | `renderToStringAsync(...)` | `await renderToStream(() => <App />)` (one consumer: `pipe` / `pipeTo` / `readable`) |
498
524
  | `clearDelegatedEvents` | delete — delegated listeners are scoped to each render root |
499
525
  | `vite-plugin-solid` / `jsxImportSource: "solid-js"` | `@solidjs/vite-plugin` / `"jsxImportSource": "@solidjs/web"` |
500
- | JSX `<Route>` / `<A>` / `<HashRouter>` / `<Navigate>` / `<FileRoutes>` | `createRouter({ routes })`, `fileRoutes(pageRoutes)` (`fileRoutes` from `@solidjs/router/fs`, `pageRoutes` from `virtual:file-routes`), plain `<a href={Router.paths...}>`, `hashHistory()`; link state via automatic `aria-current` / `data-active` / `data-pending` + CSS (`useLinkState` in JSX) |
526
+ | JSX `<Route>` / `<A>` / `<HashRouter>` / `<Navigate>` / `<FileRoutes>` | `createRouter({ routes })`, `fileRoutes(pageRoutes)` (`fileRoutes` from `@solidjs/router/fs`, `pageRoutes` from `virtual:file-routes`), plain `<a href={Router.paths...}>`, `hashHistory()`; link state via automatic `aria-current` / `data-active` + CSS and opt-in `data-pending` (`links: pendingLinks`), or `useLinkState` in JSX <!-- upstream:pending-links-opt-in --> |
501
527
  | `useCurrentMatches`, `MemoryRouter` / `createMemoryHistory`, `redirect` / `reload` from `@solidjs/router`, `indexArray` | `useRouteMatches`, `createRouter({ routes, history: memoryHistory("/x") })`, `redirect` / `reload` from `@solidjs/web`, `mapArray(list, fn, { keyed: false })` |
502
528
  | `createAsync` / `createAsyncStore` / `useSubmission` / router `json()` / `cache()` | `createMemo(() => getUser(id()))`, `useSubmissions`, `respond()` from `@solidjs/web`, `query` |
503
529
  | `import ... from "@solidjs/start"` / `vinxi` / `h3` / `"use client"` | `GET` from `@solidjs/web/server-functions`; Solid has no `"use client"` |
504
530
  | `render(<App />, root)` | `render(() => <App />, root)` (same for `hydrate` / `renderToString` / `renderToStream`) |
505
531
  | `<MetaProvider>` | no provider — render `<Title>` / `<Meta>` / `<Link>` from `@solidjs/meta` anywhere |
506
- | `use:directive`, `on:`/`oncapture:`, `attr:`/`bool:`, `/*@once*/` | `ref` callbacks, camelCase event props, standard attributes, keep values reactive; `prop:` remains for DOM properties (`prop:indeterminate`, objects/arrays on custom elements — plain props are attributes and stringify) |
532
+ | `use:directive`, `on:`/`oncapture:`, `attr:`/`bool:`, `/*@once*/` (also lowercase `onclick`-style attributes — plain attributes, not events <!-- upstream:lowercase-event-attribute -->) | `ref` callbacks, camelCase `on`-capitalized event props, standard attributes, keep values reactive; `prop:` remains for DOM properties (`prop:indeterminate`, objects/arrays on custom elements — plain props are attributes and stringify) |
507
533
  | `resource.loading` / `resource.error` | `<Loading>` boundary / `<Errored>` boundary |
508
534
 
509
535
  When unsure about any API, verify against the official Solid 2.0 docs — fetchable URLs are
510
536
  listed in `references/official-docs.md` next to the `solid-2` skill. Do not guess from
511
- Solid 1.x or React memory.
537
+ Solid 1.x or React memory. Two official pages still describe the previous release —
538
+ these rules win there: the router navigation page lists `data-pending` as automatic
539
+ (it needs `links: pendingLinks`) <!-- upstream:docs-pending-links-stale -->, and the
540
+ `createSignal` reference says a write to a writable derivation wins over a same-tick
541
+ recompute (a source change in the same update now re-runs it with the write as
542
+ `prev`). <!-- upstream:docs-derived-write-stale -->
512
543
 
513
544
  `createRenderEffect` exists in Solid 2 but is not a default. `storePath`
514
545
  (the 1.x path-setter migration helper) is no longer exported from `solid-js` as of
@@ -39,7 +39,7 @@ Practical consequences:
39
39
  | Object that is replaced wholesale (fetch response, selected item, `User \| null`) | `createSignal(objectValue)` |
40
40
  | Value derived from other reactive values | plain function `() => ...` (never a signal synced by an effect) |
41
41
  | Expensive derivation, multiple consumers, or equality boundary needed | `createMemo(() => ...)` |
42
- | Follows a prop but user can locally override; prop change resets | writable derivation: `createSignal(() => props.value)` / `createStore(() => props.value, fallback)` |
42
+ | Follows a prop but user can locally override; prop change resets | writable derivation: `createSignal(() => props.value)` / `createStore(() => props.value, fallback)` — a write lands at once and the function receives it as `prev` (the draft on a store) on re-run; honoring `prev` keeps the edit across source changes <!-- upstream:derived-write-prev --> |
43
43
  | Shared by a subtree, independent per provider instance | signal/store created inside a provider component, passed via context |
44
44
  | App-wide state (theme, session, cart, locale) | the same provider, placed at the root of `App` — not a module-level signal/store (under SSR one module instance serves every request) |
45
45
  | Constants (a category list, a formatter, the `createContext` call) | module scope |
@@ -69,12 +69,12 @@ Most React `useEffect` code should NOT become `createEffect`:
69
69
  | Inspect, count, or iterate children | `children(() => props.children)` then `.toArray()` |
70
70
  | Code-split a component | `lazy(() => import("./X"))` read under `<Loading>` |
71
71
  | Named export from a lazy module | `lazy(() => import("./pages"), { export: "About" })` |
72
- | Pick a component/tag from reactive state | `dynamic(() => ...)` from `@solidjs/web` (stable identity; `<Dynamic>` is deprecated) <!-- upstream:dynamic-deprecated --> |
72
+ | Pick a component/tag from reactive state | `dynamic(() => ...)` from `@solidjs/web` (stable identity; `<Dynamic>` is deprecated) <!-- upstream:dynamic-deprecated --> — `dynamicComponent(() => ...)` when the source only answers with a component, never a tag <!-- upstream:dynamic-component-export --> |
73
73
  | Overlay / modal | `<Portal>` — hoist async reads *above* the portal (reads inside start on the client) |
74
74
  | Async value used several layers down | Create the memo high; pass `value={memo()}` through intermediates (they do not wait); put `<Loading>` around the leaf read |
75
75
  | Nested child with its own fetch | Leave it nested — it runs in parallel. Sequential only when the second call needs the first response (`fetchAuthor(story().authorId)`) |
76
76
  | Browser-only widget (charts, maps, `window`) | `clientOnly(() => import("./Chart"))` from `@solidjs/web` (`{ lazy: true }` defers the import until first render; `{ export: "Chart" }` for a named export). Placeholder: `<Chart fallback={<Skeleton />} />` — SSR renders the fallback; a `<Loading>` around it leaves the server HTML empty |
77
- | Browser-only *value* (`localStorage`, viewport size) | `createMemo(..., { ssrSource: "client" })` — per-value policy at the data source, no component split (`"hybrid"` when server data mixes with client signals) |
77
+ | Browser-only *value* (`localStorage`, viewport size) | `createMemo(..., { ssrSource: "client" })` — per-value policy at the data source, no component split (`"hybrid"` only to keep consuming an async-iterable source after hydration <!-- upstream:ssr-source-hybrid-stream -->) |
78
78
  | Server vs browser code path | `isServer` / `isDev` from `@solidjs/web` (build-time constants), not `typeof window` — never to choose markup (`{isServer ? <A /> : <B />}` is a hydration mismatch; use `clientOnly` / `ssrSource`) |
79
79
  | SSR-stable `id` / `for` / `aria-*` pairing | `createUniqueId()` |
80
80
 
@@ -256,7 +256,11 @@ const Result = dynamic(() => (detailed() ? Detailed : Compact));
256
256
  ```
257
257
 
258
258
  `dynamic()` returns a **stable** component whose source can be a component, an
259
- intrinsic tag name, a promise, or empty. Prefer it over swapping a component
259
+ intrinsic tag name, a promise, or empty. An async source (a promise, or a `live()`
260
+ server-component iterable) may resolve to a server component or a tag name; a promise
261
+ of a *client* component function is a dev error on the server
262
+ (`[DYNAMIC_ASYNC_COMPONENT]`) — resolve the async upstream, or `lazy()` for code.
263
+ Prefer it over swapping a component
260
264
  variable in JSX (`const View = tab() ? A : B` freezes the choice at setup).
261
265
  `<Dynamic component={...}>` is the same primitive with a worse shape (the tag travels
262
266
  in the props bag, so every instance merges and omits it and rebuilds a factory) and
@@ -264,6 +268,13 @@ is **deprecated** since 2.0.0-rc.9 — `@deprecated` in the types, no runtime wa
264
268
  still shipped in 2.0. <!-- upstream:dynamic-deprecated --> Hoist a `dynamic()` per component instance (or per module for a
265
269
  constant tag) instead.
266
270
 
271
+ When the source only ever answers with a component — never a tag name — use
272
+ `dynamicComponent()` instead: same contract and hydration shape minus the tag arm, so
273
+ a tag name is a compile error and a page whose only dynamic mounts are components
274
+ retains no element runtime (`createElement` / `spread` / the namespace tables). It
275
+ is the documented way to mount a server component: `dynamicComponent(() =>
276
+ getStory(props.id))`. <!-- upstream:dynamic-component-export -->
277
+
267
278
  ### Two-phase effect (imperative boundary only)
268
279
 
269
280
  ```tsx
@@ -327,7 +338,12 @@ const [signInError, setSignInError] = createSignal<string | null>(() => {
327
338
  ```
328
339
 
329
340
  The writable form spells exactly this: derive from the source, allow a local
330
- override, reset when the source changes. (React needs `useState(props.x)` plus a
341
+ override, reset when the source changes. The mechanics: a write lands at once,
342
+ and on the next source change the function re-runs receiving the write as `prev`
343
+ (a store receives it as the draft). A function that ignores `prev` resets — this
344
+ pattern — and also discards a write staged in the same update as a source change;
345
+ one that honors `prev` can keep the local value. <!-- upstream:derived-write-prev -->
346
+ (React needs `useState(props.x)` plus a
331
347
  `previousX` comparison reset during render for the same behavior.)
332
348
 
333
349
  ### Async data — fetch high, block low
@@ -399,8 +415,12 @@ return <StoryDetail story={story} />;
399
415
  Lifting the fetch does **not** mean lifting the boundary — that is consume glued
400
416
  back onto block. After first paint the
401
417
  boundary keeps settled content during refetch; `isPending(results)` (pass the accessor,
402
- not `isPending(results())`) is the indicator. Use `on={query()}` (the **value**, not the
403
- accessor `on={query}`) only when that identity should put the fallback back on screen.
418
+ not `isPending(results())`) is the indicator. Add `on={query()}` (a tracked **read** —
419
+ the accessor `on={query}` reads nothing) only when that change of subject should put the
420
+ fallback back on screen. `on` is a dependency list, not a compared key: its value is
421
+ irrelevant, several inputs go in as `on={[query(), page()]}`, and a change to anything it
422
+ reads re-arms the boundary. <!-- upstream:loading-on-dependency-list --> `[LOADING_ON_OUTSIDE_HOLD]` in dev means the same data is
423
+ also read outside the boundary, so the fallback can never show — move that read under it.
404
424
  Do not use `isPending` as the first-load spinner.
405
425
  - **Nesting is not a waterfall.** `Comments` above fetches in parallel with `story`
406
426
  because it reads `props.storyId` (already known), not `props.story`. Passing
@@ -426,7 +446,8 @@ return <StoryDetail story={story} />;
426
446
  the same rule: bind `value={latest(query)}` so a controlled search box reflects
427
447
  keystrokes while its write is held (an uncontrolled input needs nothing).
428
448
  - Read every reactive input **before the first `await`** in an async computation. A read
429
- after `await` does not subscribe; production can sit pending with no retry.
449
+ after `await` does not subscribe (dev reports `[UNTRACKED_READ_AFTER_AWAIT]` on V8 —
450
+ Chromium, Node, Deno, Bun); production can sit pending with no retry.
430
451
  - `<Errored>` function fallbacks receive an error **accessor** and a `reset` callback:
431
452
  `fallback={(error, reset) => ...}`. The boundary **heals** when the source succeeds
432
453
  again (refresh, live reconnect, input change) — it is graph status, not a terminal
@@ -498,7 +519,11 @@ rendered), `render` / `client` (a `<Loading>` fragment rejected, client re-rende
498
519
  hydration value would not serialize), `server-function` / `thrown` (`direct: true` for
499
520
  an in-process call during SSR), `server-function` / `channel` (a rejection or throw
500
521
  escaping through the result graph — a promise, an iterable, or a stream — after the
501
- head committed). A monitoring SDK's `init()` that
522
+ head committed), and `request` / `failed` — a failure outside render and server
523
+ functions (middleware, request setup) reported via
524
+ `reportRequestFailure(error, event)` from `@solidjs/web`. A synchronous throw out of
525
+ `renderToString` / `renderToStream`'s first pass is reported `render` / `failed`
526
+ before rethrow. <!-- upstream:report-request-failure --> A monitoring SDK's `init()` that
502
527
  registers this hook must load before the server graph — put it in the plugin's
503
528
  `start: { instrument: "./src/instrument.ts" }`, not at the top of an entry (see
504
529
  [Production observability](#production-observability-the-observe-build)).
@@ -956,7 +981,12 @@ onSettled(() => {
956
981
 
957
982
  Effect callbacks follow the same shape: return the cleanup from `apply` (an
958
983
  `onCleanup` there never runs), and `flush()` inside one is a no-op
959
- (`[FLUSH_IN_EFFECT_CALLBACK]`).
984
+ (`[FLUSH_IN_EFFECT_CALLBACK]`). Creating a memo, an effect, or a root inside `apply`
985
+ throws in dev (`[PRIMITIVE_IN_EFFECT_CALLBACK]` — the effect phase has no owner to
986
+ dispose them; `runWithOwner(null, …)` does not lift it): create them in the
987
+ component body or the compute phase, or attach them to an owner captured with
988
+ `getOwner()` in the body via `runWithOwner(owner, …)`.
989
+ <!-- upstream:primitive-in-effect-callback -->
960
990
 
961
991
  ### Per-value SSR policy: `ssrSource`
962
992
 
@@ -988,8 +1018,8 @@ function DraftPreview(props: { documentId: string }) {
988
1018
  | Policy | Meaning | Use when |
989
1019
  |---|---|---|
990
1020
  | `"server"` (default) | Client adopts the serialized server value; compute does not re-run | Compute is deterministic from server-available inputs |
991
- | `"hybrid"` | Client adopts the serialized value, then re-runs the compute to take over | Server data mixes with client signals (window size, user locale) |
992
- | `"client"` | Server value skipped; compute runs after hydration as if first-mounted | Serialization is meaningless (`localStorage`, viewport) |
1021
+ | `"hybrid"` | Client adopts the serialized value, then — for an async-iterable source — continues the stream on the client; for a sync or promise compute it is identical to `"server"` (no re-run) <!-- upstream:ssr-source-hybrid-stream --> | A stream (`live()`, a subscription) the client should keep consuming after hydration |
1022
+ | `"client"` | Server value skipped; compute runs after hydration as if first-mounted | Serialization is meaningless (`localStorage`, viewport), or server data mixes with a browser-only input |
993
1023
 
994
1024
  With `"client"`, the bare form suspends on the server and the nearest `<Loading>`
995
1025
  fallback renders into the HTML; a declared first value (`loadingValue` on
@@ -1068,19 +1098,24 @@ see [Production observability](#production-observability-the-observe-build) —
1068
1098
  dev setting. Diagnostics come in two layers: **findings** (facts about a render, present
1069
1099
  in observe and dev) and **checks** (dev-only guidance printed by the console reporter).
1070
1100
  Always-on dev checks include misplaced reads
1071
- (`[STRICT_READ_UNTRACKED]`, `[PENDING_ASYNC_UNTRACKED_READ]`), misplaced writes
1101
+ (`[STRICT_READ_UNTRACKED]`, `[PENDING_ASYNC_UNTRACKED_READ]`, `[UNTRACKED_READ_AFTER_AWAIT]`
1102
+ for a first read made after an `await` in an async compute — V8 engines only), misplaced writes
1072
1103
  (`[REACTIVE_WRITE_IN_OWNED_SCOPE]` — since rc.9 also for store setters called in a
1073
1104
  component or root body <!-- upstream:store-write-owned-scope -->, `[ASYNC_STORE_SETTER]` for a store setter callback that returns
1074
1105
  a promise <!-- upstream:async-store-setter-throws -->, `[FLUSH_IN_ACTION]`, `[SERVER_WRITE]`), and
1075
- `[ASYNC_OUTSIDE_LOADING_BOUNDARY]` when a tracked read has no boundary above it.
1106
+ `[ASYNC_OUTSIDE_LOADING_BOUNDARY]` when a tracked read has no boundary above it, and
1107
+ `[LOADING_ON_OUTSIDE_HOLD]` when a `<Loading on>` boundary's data is also read outside it.
1076
1108
  Each entry carries a stable `[CODE]`, what the runtime observed, and an owner chain
1077
1109
  (`in <App> › <Cart> › <LineItem>`); never silence a code before understanding it.
1078
1110
  Since rc.9 the server render reports on the same channel with the same owner chains:
1079
1111
  `[SSR_RENDER_ERROR_CONTAINED]` (a boundary routed a render error; `ownerPath` is where
1080
- it was thrown, `boundaryPath` where it was met) <!-- upstream:ssr-render-error-contained -->, `[SSR_ERROR_SANITIZED]` (the original
1081
- behind a production-sanitized error), `[SSR_CLIENT_CONTENT_MASKED]` (client-only content
1082
- that surfaced only after a real server wait — the wait was wasted), server-side
1083
- `[ASYNC_WATERFALL]`, and `[REVEAL_IN_RENDER_TO_STRING]`.
1112
+ it was thrown, `boundaryPath` where it was met) <!-- upstream:ssr-render-error-contained -->, `[SERVER_ERROR_SANITIZED]` (the original
1113
+ behind a production-sanitized error, from a server function or an SSR render),
1114
+ `[SSR_CLIENT_CONTENT_MASKED]` (client-only content that surfaced only after a real
1115
+ server wait — the wait was wasted), `[SSR_BOUNDARY_WATERFALL]` (sequential waits inside
1116
+ one server `<Loading>` boundary), `[SSR_UNDECLARED_LIVE_SOURCE]` (an async iterable
1117
+ still pumping into a document render — declare it `live()`), and
1118
+ `[REVEAL_IN_RENDER_TO_STRING]`.
1084
1119
  For the repair behind any code, read the `reactivity-diagnostics` skill Solid ships
1085
1120
  in the repo (`node_modules/solid-js/skills/reactivity-diagnostics/SKILL.md`) — every
1086
1121
  code maps to what the runtime observed and what to change.
@@ -1091,13 +1126,18 @@ every scope that re-ran, what changed to cause it, and how long it took. Since
1091
1126
  no longer exists <!-- upstream:dev-attribution-removed --> (it is a type error; `DEV` holds only devtools hooks, graph
1092
1127
  traversal, and console reporting). `OBSERVE` is the public observability surface on
1093
1128
  the observe and dev builds — `OBSERVE.records`, `OBSERVE.diagnostics`,
1094
- `OBSERVE.exclude`, `OBSERVE.server.trace` are for app and tool code; only the
1095
- `OBSERVE.attribution` hook slot (`install` / `withInteraction` / `withOrigin`) belongs
1096
- to engines, routers, and devtools. Since 2.0.0-rc.9 the folds, queries, and
1097
- formatters are **named exports** of that subpath, not methods of `attribution`
1098
- (`attribution.why` / `.costs` / `.feedback` / `.subscriptions` / `.format` are type
1099
- errors) <!-- upstream:attribution-named-exports -->; `attribution` keeps `enable`, `disable`, `subscribe`, `markFlight`, and the
1100
- record ring buffers (`history`, `holds`, `waterfalls`, `navigations`, `interactions`):
1129
+ `OBSERVE.exclude` / `OBSERVE.include`, `OBSERVE.ownerPath`, `OBSERVE.server.trace` are
1130
+ for app and tool code; only the `OBSERVE.attribution` slot (`withInteraction` /
1131
+ `withOrigin`) belongs to engines, routers, and devtools. Since 2.0.0-rc.9 the folds,
1132
+ queries, and formatters are **named exports** of that subpath, not methods of
1133
+ `attribution` (`attribution.why` / `.costs` / `.feedback` / `.subscriptions` / `.format`
1134
+ are type errors) <!-- upstream:attribution-named-exports -->; `attribution` keeps `enable` (a hold on the one shared engine — it
1135
+ returns the release), `disable` (full teardown), `markFlight`, and one ring-buffer reader,
1136
+ `attribution.history(type)` (`"rerun"` / `"hold"` / `"waterfall"` / `"navigation"` /
1137
+ `"interaction"`). Live records — the engine's included — arrive on
1138
+ `OBSERVE.records.subscribe(type, (event, live) => …)`; `attribution.subscribe` and the
1139
+ `holds()` / `waterfalls()` / `navigations()` / `interactions()` methods are gone since
1140
+ 2.0.0-rc.10. <!-- upstream:attribution-history-records -->
1101
1141
 
1102
1142
  ```ts
1103
1143
  import { attribution, formatRerun, why } from 'solid-js/attribution';
@@ -1116,13 +1156,15 @@ to query the tables yourself. Name the scopes you intend to interrogate
1116
1156
  (`createMemo(..., { name: "total" })`) — chains refer to nodes by name, and
1117
1157
  anonymous nodes print as `computed` / `effect` / `signal`; component labels
1118
1158
  (`<Checkout>`) are always present under `vite dev` and survive minification only with
1119
- the compiler's `componentNames` option (`solid({ observe: true })` turns it on). On the
1159
+ the compiler's `sourceNames` option (components, binding effects, primitives — the Vite
1160
+ plugin turns it on under `vite dev` and `observe: true`; `componentNames` was its rc.9 name
1161
+ and is rejected now) <!-- upstream:source-names-option -->. On the
1120
1162
  **prod build** the import resolves to an inert same-surface twin whose `enable()` is a
1121
1163
  no-op, so the import can stay in the code. On the **observe build** it is the real
1122
1164
  engine and an unguarded `enable()` records in production — keep the `isDev` guard
1123
1165
  unless recording in production is the intent. The split is pay-for-use: importing
1124
1166
  `costs` or `feedback` is what turns their tables on, so a records-only consumer
1125
- (`attribution.subscribe(...)`) ships neither.
1167
+ (`OBSERVE.records.subscribe("rerun", …)`) ships neither.
1126
1168
 
1127
1169
  While attribution is enabled, Solid also warns on its own about over-subscription,
1128
1170
  chained async waterfalls (`[ASYNC_WATERFALL]`), memos whose fresh-but-equal output
@@ -1136,10 +1178,18 @@ ran while held; info from 100ms, warning from 200ms, tunable via
1136
1178
  `enable({ holds: { infoMs, warnMs } })`). A hold that *was* acknowledged but still ran
1137
1179
  long is `[LONG_HOLD]` (info from 500ms, warning from 1000ms;
1138
1180
  `enable({ longHolds: { infoMs, warnMs } })`) — the suggestion is a `<Loading on={...}>`
1139
- boundary, never removing the hold. `costs()`, `feedback()` (named imports), and
1140
- `attribution.holds()` rank the session worst-first; `attribution.waterfalls()`,
1141
- `attribution.navigations()`, and `attribution.interactions()` hold the fact tables
1142
- behind the verdicts.
1181
+ boundary, never removing the hold. Beside them: `[UNTRACKED_ASYNC_HANDLER]` (an `async`
1182
+ handler that awaits with no write before the `await` and no `action` — no hold exists, so
1183
+ the click looks dead; make it an `action`, or write a `createOptimistic(false)` flag
1184
+ first), `[ABANDONED_FLIGHTS]` (a source re-asked on every keystroke, answers thrown away
1185
+ — debounce at the handler), `[STACKED_HOLDS]` (repeated clicks piling into one hold),
1186
+ `[FALLBACK_FLASH]` (a fallback shown under 150ms), `[WASTED_RECOMPUTE]` (a scope whose
1187
+ runs mostly compared equal — narrow its reads upstream), `[GRAPH_GROWTH]` (the graph grows
1188
+ on every visit to a route — an undisposed root or subscription), and `[OPTIMISTIC_REVERTED]`
1189
+ (info: a guess the screen showed was replaced). `costs()` and `feedback()` (named imports)
1190
+ rank the session worst-first; `attribution.history("hold")` / `("waterfall")` /
1191
+ `("navigation")` / `("interaction")` hold the fact tables behind the verdicts (each hold
1192
+ carries its `silent` / `long` verdict). <!-- upstream:attribution-history-records -->
1143
1193
  `subscriptions(scope)` (named import) lists a scope's current dependencies to compare
1144
1194
  against what it uses.
1145
1195
 
@@ -1202,11 +1252,19 @@ effects** — a pure markup or styling change does not need it.
1202
1252
  `attribution.enable({ log: false })` in the client entry, then `why(scope)` /
1203
1253
  `costs()` / `feedback()` from the console or a scratch effect — never `log: true`
1204
1254
  left on (one collapsed `[why-run]` group per re-run) and never unguarded (on the
1205
- observe build it records in production).
1255
+ observe build it records in production). Under `vite dev` the plugin also paints the
1256
+ same records as tracks in the Chrome Performance panel (`performanceTracks`, on by
1257
+ default; `false` opts out) — record there when a timeline reads better than tables.
1258
+ The six attribution cost checks (`hotRuns`, `hotTime`, `wideDeps`, `unstableMemos`,
1259
+ `fanOut`, `wastedRecompute`) are opt-in there: `performanceTracks: { attribution:
1260
+ { checks: true } }` (or `enablePerformanceTracks({ attribution: { checks: true } })`,
1261
+ or your own `attribution.enable()` hold — the diagnostics-session path above already
1262
+ turns them on). <!-- upstream:perf-tracks-cost-checks -->
1206
1263
  4. **Read the verdicts, then the tables.** Fix every coded warning the session raised
1207
1264
  (`[SILENT_HOLD]`, `[IMMUTABLE_UPDATE_IN_STORE]`, `[UNSTABLE_LIST_IDENTITY]`,
1208
1265
  `[EFFECT_RELAY_TEAR]`, `[EFFECT_WRITES_OWN_SOURCE]`, `[ASYNC_WATERFALL]`,
1209
- `[HOT_SCOPE_RERUNS]`). Then `costs().scopes` (self time, `wastedMs` for runs whose
1266
+ `[HOT_SCOPE_RERUNS]`, `[WASTED_RECOMPUTE]`, `[ABANDONED_FLIGHTS]`,
1267
+ `[UNTRACKED_ASYNC_HANDLER]`). Then `costs().scopes` (self time, `wastedMs` for runs whose
1210
1268
  result did not change), `costs().writes` (root writes by downstream work),
1211
1269
  `feedback().sources` / `.interactions` / `.flights` / `.fallbacks` (silent holds,
1212
1270
  held time per interaction, abandoned requests, fallback flashes). A scope that
@@ -1233,14 +1291,16 @@ export default { plugins: [solid({ ssr: true, observe: true })] };
1233
1291
  ```
1234
1292
 
1235
1293
  `observe: true` adds the `observe` condition to every environment (client and server,
1236
- `vite build` and preview) and turns on the compiler's `componentNames` option so
1237
- `ownerPath` / `boundaryPath` name components instead of `computed`. Without the plugin,
1238
- set `resolve.conditions` (or `node --conditions=observe` for an unbundled server) and
1239
- `componentNames` yourself. Cost in Solid's size suite: ~16.8 KB against ~15.3 KB brotli
1294
+ `vite build` and preview) and turns on the compiler's `sourceNames` option (components,
1295
+ binding effects, and primitives) so `ownerPath` / `boundaryPath` name components instead
1296
+ of `computed` <!-- upstream:source-names-option -->. Without the plugin, set `resolve.conditions` (or `node --conditions=observe`
1297
+ for an unbundled server) and `sourceNames` yourself. Cost in Solid's size suite: ~16.8 KB against ~15.3 KB brotli
1240
1298
  for a small client app; importing the attribution engine on top raises the cap to
1241
1299
  ~27 KB (a separate entry — an observe build that never imports it never ships it). The
1242
1300
  two error hooks are **not** part of this: they fire on every build, prod included, only
1243
- the paths are `undefined` there.
1301
+ the paths are `undefined` there. `enablePerformanceTracks()` from
1302
+ `@solidjs/web/performance-tracks` paints the records on the Chrome Performance panel in
1303
+ production too (observe build; a no-op on the prod build).
1244
1304
 
1245
1305
  - **Instrumentation order.** A monitoring SDK's `init()` (and `configureServerErrors`)
1246
1306
  must run before the modules it patches load. `import "./instrument"` at the top of an
@@ -1257,22 +1317,31 @@ the paths are `undefined` there.
1257
1317
  `OBSERVE?.server.trace.provide((request) => ({ traceId, spanId, entries }))`, once per
1258
1318
  request. Do not write middleware to inject trace headers.
1259
1319
  - **Records.** `OBSERVE?.records.subscribe(type, (event, live) => …)` delivers one plain
1260
- record per `"boundary"` (a `<Loading>` that waited on the server), `"invocation"`
1261
- (server-function execution), `"call"` (server-function call from the page — shares
1262
- `id` with its invocation), and `"frame"` (produced / applied). Records are
1320
+ record per `"render"` (a server render: `mode`, `shellMs`, `durationMs`, `boundaries`,
1321
+ `outcome`, the matched `route`), `"boundary"` (a `<Loading>` that waited on the server),
1322
+ `"invocation"` (server-function execution), `"request"` / `"call"` (a server-function
1323
+ call from the page, at send and at settle — `call` shares `id` with its invocation),
1324
+ `"frame"` (produced / applied), and `"recovery"` (a boundary the client re-rendered
1325
+ after a server failure); the attribution engine's own records (`"rerun"`, `"hold"`,
1326
+ `"interaction"`, `"navigation"`, …) ride the same channel while it is enabled. Records are
1263
1327
  serializable data; live handles (request, args, the error as thrown) ride in the
1264
1328
  second argument. Listeners run synchronously inside the runtime, **must not write
1265
1329
  signals**, and should subscribe from a module that loads before the app.
1266
- - **Interactions.** `attribution.enable()` then
1267
- `attribution.subscribe("interaction", (event) => …)` says what each click waited on
1330
+ - **Interactions.** `attribution.enable()` (keep the release it returns) then
1331
+ `OBSERVE?.records.subscribe("interaction", (event) => …)` <!-- upstream:attribution-history-records --> says what each click waited on
1268
1332
  (`event.settledMs`, `event.holds[].blockers`, `event.holds[].holdMs`); a hold nothing acknowledged
1269
1333
  is the dead click. The fold tables stay separate imports.
1270
1334
  - **What leaves the process.** Beyond names, four record fields carry page data —
1271
1335
  `target` (element as `tag#id "text"`, 30 chars), `prev` / `value` previews (40 chars),
1272
- navigation `to` / `from` / `params`, and `data.error` on server render findings — an
1273
- exporter decides what to do with each. A tool that renders inside the app it watches
1336
+ navigation `to` / `from` / `params`, and `data.error` on server render findings. The
1337
+ engine's `values` option governs the first three at the source — `"full"` (the dev
1338
+ default), `"labels"` (no previews; element text only on buttons and links), `"none"`
1339
+ (the observe-build default) — so an exporter on the observe build passes its level
1340
+ explicitly (`attribution.enable({ values: "labels" })`) and decides what to do with the
1341
+ rest. A tool that renders inside the app it watches
1274
1342
  (devtools panel) marks its root with `OBSERVE?.exclude(getOwner()!)` inside its
1275
- `createRoot` so its own effects and stores never become findings; do not route its
1343
+ `createRoot` so its own effects and stores never become findings (`OBSERVE?.include(owner)`
1344
+ re-admits the app's root under an excluded shell); do not route its
1276
1345
  writes through `runWithOwner` (that is a write in an owned scope).
1277
1346
 
1278
1347
  ## Client mode: no server HTML, no hydration reflexes
@@ -1355,7 +1424,8 @@ export default function App() {
1355
1424
 
1356
1425
  The `<Loading>` covers the first load of the lazy pages (and lets SSR stream the shell);
1357
1426
  later navigations hold the current page while the next one loads (links get
1358
- `data-pending`). Create the instance at **module scope**. Nested layouts are `children` arrays
1427
+ `data-pending` when the router is created with `links: pendingLinks`
1428
+ <!-- upstream:pending-links-opt-in -->). Create the instance at **module scope**. Nested layouts are `children` arrays
1359
1429
  on the route objects, not nested `<Route>` / nested routers. Solid Router does
1360
1430
  **not** support nested `<Router>` instances — compose one route tree (or a lazy
1361
1431
  `children` thunk). Navigate with
@@ -1418,13 +1488,20 @@ Link warming: `preload="false"` skips that link's *data* preload; `preloadLinks:
1418
1488
  disables automatic link preloading. Intent values are `"initial"` / `"navigate"` /
1419
1489
  `"native"` / `"preload"`. One router per app.
1420
1490
 
1421
- Link state needs no component code: the router sets `aria-current="page"` (exact
1422
- match), `data-active` (exact or descendant — the `/` link is active only on exact
1423
- match), and `data-pending` (in-flight navigation target) on the anchors it handles;
1424
- style them in CSS. For non-anchors, or state needed in JSX, `useLinkState(() => href,
1425
- { end })` returns the same three as accessors (`active` / `current` / `pending`);
1426
- `useIsRouting()` is the page-level pending flag for progress bars. Hand-rolled
1491
+ Link state needs no component code: the router sets `aria-current="page"` (same path
1492
+ and same query — parameter order and hash aside), `data-active` (exact or descendant
1493
+ on the path only — the `/` link is active only on exact match), and, with the opt-in
1494
+ `createRouter({ links: pendingLinks })` plugin, `data-pending` (in-flight navigation
1495
+ target) on the anchors it handles; style them in CSS. Highlight nav links on
1496
+ `data-active`, not `aria-current`: a nav link to `/products` is not current on
1497
+ `/products?page=2`. An `aria-current` you write
1498
+ yourself is yours — the router only manages the attribute on links where it set it.
1499
+ For non-anchors, or state needed in JSX, `useLinkState(() => href,
1500
+ { end })` returns the same three as accessors (`active` / `current` / `pending` —
1501
+ `pending` works without the plugin); `useIsRouting()` is the page-level pending
1502
+ flag for progress bars. Hand-rolled
1427
1503
  `location.pathname === ...` comparisons are the wrong tool.
1504
+ <!-- upstream:pending-links-opt-in -->
1428
1505
 
1429
1506
  Build URLs from `paths`: static segments are properties (`Router.paths.about`, never
1430
1507
  `Router.paths.about()`), parameterized segments are calls (`paths.products(id)`),
@@ -1642,10 +1719,10 @@ status, so `return respond({ error }, { status: 400 })` reads as success to the
1642
1719
  is success metadata a scripted caller unwraps. Arguments are JSON: send `Date` / `Map` /
1643
1720
  `Set` as ISO strings / arrays (they throw otherwise; one `File` / `Blob` / `FormData`
1644
1721
  argument travels natively). `enableRichArguments()` from
1645
- `@solidjs/web/server-functions/rich-args` (at `src/App.tsx` module scope) lifts that, but
1646
- in rc.9 importing it fails `vite build` (`"./client" is not exported` — the dev server
1647
- works, so the break shows only at build); `resolve: { dedupe: ["@solidjs/web"] }` in the
1648
- Vite config works around it. <!-- upstream:rich-args-vite-build https://github.com/solidjs/solid/issues/3627 -->
1722
+ `@solidjs/web/server-functions/rich-args` (at `src/App.tsx` module scope) lifts that. Its
1723
+ import failed `vite build` in rc.9 (`"./client" is not exported`); that is fixed since
1724
+ 2.0.0-rc.10, so a `resolve: { dedupe: ["@solidjs/web"] }` workaround left in a Vite config
1725
+ is no longer needed (the plugin dedupes the runtime packages itself under `vite dev`). <!-- upstream:rich-args-vite-build https://github.com/solidjs/solid/issues/3627 -->
1649
1726
  `GET()` is only for idempotent reads (URLs leak into logs/history);
1650
1727
  import it from `@solidjs/web/server-functions`, never `@solidjs/start`.
1651
1728
  Reads are function calls too: `GET(async (id) => { "use server"; ... })` is invoked
@@ -1673,7 +1750,10 @@ export const fetchStory = GET(async (id: number) => {
1673
1750
  A value that changes over time is `live(GET(fn))` — **`live()` outermost**: `GET()`
1674
1751
  chooses the read transport and `live()` wraps the call behavior around it. `live()`
1675
1752
  does not imply `GET()`; without it the source streams over POST, which is fine when a
1676
- cacheable URL is of no use.
1753
+ cacheable URL is of no use. A server component rendered into a document that reads an
1754
+ *undeclared* async iterable still producing after 5s is `[SSR_UNDECLARED_LIVE_SOURCE]` in
1755
+ dev — declare it `live()` (the document takes the first value and closes it; the client
1756
+ connects after hydration) or bound the source.
1677
1757
 
1678
1758
  ```ts
1679
1759
  // src/data/auction.ts
@@ -1806,7 +1886,7 @@ how to reuse. Prefer the form on the right.
1806
1886
  | `createEffect(() => user, (u) => { log(u.name); })` | `createEffect(() => user.name, (name) => { log(name); })` |
1807
1887
  | `createEffect(() => title(), (t) => (document.title = t))` / `(v) => setX(v)` | `(t) => { document.title = t; }` — apply returns a cleanup or nothing; any other return value halts reactivity. An apply that only calls a local setter is rule 4 even with braces (`solid2-kit check` flags it): use a (writable) derivation |
1808
1888
  | `<Loading fallback={<PageSkeleton />}>{/* header + data */}</Loading>` | wrap only the data slot; chrome stays outside |
1809
- | `<Loading on={id} fallback={...}>` (the accessor) | `on={id()}` — a value, so identity changes can show fallback |
1889
+ | `<Loading on={id} fallback={...}>` (the accessor), or `` on={`${a()}/${b()}`} `` to key several inputs | `on={id()}` / `on={[a(), b()]}` — `on` is a tracked dependency list, not a compared key: a bare accessor is never read (`solid2-kit check` flags it), and an array is the several-inputs form <!-- upstream:loading-on-dependency-list --> |
1810
1890
  | `setCount(count() + 1)` when writes can batch | `setCount((c) => c + 1)` |
1811
1891
  | `setHandler(fn)` to store a function | `setHandler(() => fn)` (otherwise `fn` is an updater) |
1812
1892
  | `{user() ? <P user={user()!} /> : <SignIn />}` | `<Show when={user()}>{(u) => <P user={u()} />}</Show>` |
@@ -1814,6 +1894,7 @@ how to reuse. Prefer the form on the right.
1814
1894
  | `lazy(() => import("./p").then((m) => ({ default: m.About })))` | `lazy(() => import("./p"), { export: "About" })` |
1815
1895
  | `dangerouslySetInnerHTML={{ __html }}` | `innerHTML={html()}` (sanitized); never with JSX children |
1816
1896
  | `onClick={setCount}` | `onClick={() => setCount((c) => c + 1)}` |
1897
+ | `onclick={fn}` / `onmouseover={fn}` (lowercase handlers) | `onClick={fn}` — a lowercase `on*` name is a plain attribute (dev warns `[LOWERCASE_EVENT_ATTRIBUTE]`), not an event <!-- upstream:lowercase-event-attribute --> |
1817
1898
  | `event.target.value` | `event.currentTarget.value` |
1818
1899
  | `createContext(emptyStore)` for reactive state | `createContext<Todos>()` — dummy defaults silently no-op |
1819
1900
  | `<For keyed={(t) => t.id}>{(todo) => todo.title}` | `todo().title` — key-function items are accessors |
@@ -1838,7 +1919,7 @@ how to reuse. Prefer the form on the right.
1838
1919
  | `renderToString(() => <App />)` for an async or lazy-route tree | `await renderToStream(() => <App />)` (one consumer: `pipe` / `pipeTo` / `readable`). String render emits `<Loading>` fallbacks |
1839
1920
  | `clientOnly(() => import("./Map"))` for a rarely shown widget | same + `{ lazy: true }` so the import waits until first render (default starts at declaration) |
1840
1921
  | `<NoHydration><Chart /></NoHydration>` to skip SSR of a widget | `clientOnly(() => import("./Chart"))` — `NoHydration`/`Hydration` split ownership, they do not choose visible content |
1841
- | `clientOnly` split (or `isServer` branch) for one browser-only *value* | `createMemo(() => localStorage.getItem(k), { ssrSource: "client" })` — `"hybrid"` when server data mixes with client signals |
1922
+ | `clientOnly` split (or `isServer` branch) for one browser-only *value* | `createMemo(() => localStorage.getItem(k), { ssrSource: "client" })` — `"hybrid"` only to keep consuming an async-iterable source after hydration <!-- upstream:ssr-source-hybrid-stream --> |
1842
1923
  | `await flush()` / `flush()` to wait on a pending memo | `await resolve(() => value())` (or Testing Library async queries); `flush()` only drains staged *sync* writes |
1843
1924
  | `action(function* () { setX(v); })` around a navigation-shaped setter | plain setter; async computeds hold previous values. Core `action` only when writes happen *after* async work |
1844
1925
  | `export const [cart, setCart] = createStore(...)` for app-wide state | `createCart()` called by a `CartProvider` at the root of `App`, read with `useCart()` over `createContext<T>()` — module state has no owner and, under SSR, one instance serves every request (`[SERVER_WRITE]`). Module scope is for constants and `createContext` |
@@ -1865,7 +1946,7 @@ how to reuse. Prefer the form on the right.
1865
1946
  | `action={"/_server/" + id}` / `action={createTodo.url}` on a POST form | a router action: `const save = action(createTodo, "create-todo")` + `<form action={save} method="post">` over a `(form: FormData)` server function (other signatures are TS2322) — `.url` on a bare `"use server"` function is TS2339; it is typed only on `GET()` / `live()` references (GET search forms) |
1866
1947
  | `<form method="get" action={update.url}>` for a mutation | GET forms only for idempotent search; mutations are POST |
1867
1948
  | `window.location.href = ...` / `history.pushState` | `useNavigate()` or `<a href={Router.paths...}>` |
1868
- | `class={{ active: location.pathname === "/about" }}` hand-rolled link state | CSS on automatic `aria-current` / `data-active` / `data-pending`; `useLinkState` in JSX, `useIsRouting()` for progress bars |
1949
+ | `class={{ active: location.pathname === "/about" }}` hand-rolled link state | CSS on `aria-current` / `data-active` (+ `data-pending` via `links: pendingLinks`); `useLinkState` in JSX, `useIsRouting()` for progress bars <!-- upstream:pending-links-opt-in --> |
1869
1950
  | `new URLSearchParams(location.search)` hand-rolled query parsing | `useSearchParams()` (setter merges, no scroll); typed via a route `search` schema + `useSearchParams(Router.paths.search)` |
1870
1951
  | `Router.paths.about()` / string-concatenated URLs | static nodes are properties (`Router.paths.about`); calls bind params, then search object and hash |
1871
1952
  | `httpStatus(404)` in a click handler | call it bare in the component / error-fallback body (a scope declaration, not a mutation) |
@@ -1901,7 +1982,7 @@ how to reuse. Prefer the form on the right.
1901
1982
  | `latest(user) ?? placeholder` in a handler as a null-safe read | `latest()` throws `NotReadyError` before the first value in every scope; read settled values under `<Loading>`, or `await until(() => user())` <!-- upstream:latest-throws-unsettled --> |
1902
1983
  | `attribution.why(total)` / `attribution.format(e)` / `attribution.costs()` | named imports from `solid-js/attribution`: `why(total)`, `formatRerun(e)`, `costs()`, `feedback()`, `subscriptions(scope)` (rc.9) <!-- upstream:attribution-named-exports --> |
1903
1984
  | `attribution.enable()` unguarded in app code, or `enable()` with the default `log: true` left on | `if (isDev) attribution.enable({ log: false })` — on the observe build an unguarded `enable()` records in production; prefer the `/__solid/diagnostics` session (`begin` enables, `end` disables) so app code carries nothing |
1904
- | `solid({ observe: true })` added "to see diagnostics in dev" | `vite dev` is always the dev build; `observe: true` is the production-observability opt-in (observe condition + `componentNames` for `vite build`/preview) |
1985
+ | `solid({ observe: true })` added "to see diagnostics in dev" | `vite dev` is always the dev build; `observe: true` is the production-observability opt-in (observe condition + `sourceNames` for `vite build`/preview <!-- upstream:source-names-option -->) |
1905
1986
  | `configureServerErrors({ onError: (e) => { report(e); return e; } })` | return nothing or a reference (`new Error(\`ref ${id}\`)`) — the return value replaces the sanitized wire value; the error itself leaks message/stack/secrets |
1906
1987
  | `import "./instrument"` at the top of the server entry for a monitoring SDK | `start: { instrument: "./src/instrument.ts" }` — ESM hoists static imports in dependency order, so the entry's own deps load first |
1907
1988
  | Express/middleware injecting `traceparent` / trace headers into responses | the runtime carries a sampled or provided trace on `Server-Timing` + `<meta>`; forward downstream with `getTraceContext()?.entries`; SDKs use `OBSERVE?.server.trace.provide` |
@@ -2046,6 +2127,16 @@ agent use the mirror, which serves every page as plain markdown:
2046
2127
 
2047
2128
  A curated URL list is in [references/official-docs.md](references/official-docs.md).
2048
2129
 
2130
+ Known stale pages — this skill is verified against the installed release and wins:
2131
+
2132
+ - `routing/solid-router/navigation` lists `data-pending` among the attributes the router
2133
+ sets on its own; it needs `createRouter({ links: pendingLinks })`, and `aria-current`
2134
+ also compares the query (the link-state paragraph under Solid Router 2 above).
2135
+ <!-- upstream:docs-pending-links-stale -->
2136
+ - `reference/solid-js/reactivity/create-signal` says a write to a writable derivation
2137
+ wins over a same-tick recompute; a source change in the same update re-runs the
2138
+ function, which receives the write as `prev`. <!-- upstream:docs-derived-write-stale -->
2139
+
2049
2140
  **Never** consult Solid 1.x sources (docs.solidjs.com, pre-2.0 tutorials, old Stack Overflow).
2050
2141
  Solid 2.0 is a breaking rewrite; 1.x answers are wrong here. The banned-API table is in the
2051
2142
  always-applied rules installed alongside this skill.
@@ -2112,7 +2203,7 @@ always-applied rules installed alongside this skill.
2112
2203
  memo/effect. Effect `apply` functions have block bodies and return only a
2113
2204
  cleanup or nothing.
2114
2205
  - [ ] Dev console shows no diagnostics: reads in tracking scopes (no
2115
- `[STRICT_READ_UNTRACKED]`), store setters synchronous and never called at
2206
+ `[STRICT_READ_UNTRACKED]`, no `[UNTRACKED_READ_AFTER_AWAIT]`), store setters synchronous and never called at
2116
2207
  body level (no `[ASYNC_STORE_SETTER]` <!-- upstream:async-store-setter-throws --> /
2117
2208
  `[REACTIVE_WRITE_IN_OWNED_SCOPE]` <!-- upstream:store-write-owned-scope -->), held
2118
2209
  writes paired with `isPending` / `latest` / optimistic values / `affects()`
@@ -2130,8 +2221,8 @@ always-applied rules installed alongside this skill.
2130
2221
  core `action`/`refresh`. Forms: `<form action={save} method="post">`. Core `action`
2131
2222
  bodies are generators (`function*` / `async function*`) suspending on `yield`,
2132
2223
  never plain `async` functions. Link state is automatic attributes + CSS
2133
- (`aria-current` / `data-active` / `data-pending`; `useLinkState` / `useIsRouting`
2134
- in JSX). Search params via `useSearchParams`. Forms validate on the server
2224
+ (`aria-current` / `data-active`, `data-pending` via `links: pendingLinks`;
2225
+ `useLinkState` / `useIsRouting` in JSX). <!-- upstream:pending-links-opt-in --> Search params via `useSearchParams`. Forms validate on the server
2135
2226
  (`throw respond({ issues }, { status: 400 })`) with inline errors from
2136
2227
  `useSubmissions`.
2137
2228
  - [ ] `render(() => <App />, root)` — a function, not `render(<App />)`. `hydrate` when
@@ -26,6 +26,7 @@ The skill does not cover these; fetch the pages when the task needs them.
26
26
  - Row selection without `createSelector` (id-keyed projection): https://v2-rebuild--solid-docs-v2.netlify.app/guides/lists.md, https://v2-rebuild--solid-docs-v2.netlify.app/guides/performance.md
27
27
  - Infinite scroll, polling, sharing one request: https://v2-rebuild--solid-docs-v2.netlify.app/guides/data-fetching-patterns.md
28
28
  - Writing a custom primitive (called under an owner, `MaybeAccessor`, `getOwner` / `runWithOwner` / `isDisposed` after async): https://v2-rebuild--solid-docs-v2.netlify.app/guides/custom-primitives.md, https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/advanced/owner-introspection/get-owner.md, https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/advanced/owner-introspection/run-with-owner.md, https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/advanced/owner-introspection/is-disposed.md
29
+ - Library code that must know whether it is hydrating, or write a keyed server-to-client value (`isHydrating`, `isHydratable`, `getHydrationWriter` / `takeHydrationValue`): https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/advanced/manual-hydration/is-hydrating.md, https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/advanced/manual-hydration/is-hydratable.md, https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-web/rendering-ssr/get-hydration-writer.md, https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-web/rendering-ssr/take-hydration-value.md
29
30
  - Wrapping a non-Solid library, push feeds with a reactive argument, web components (`prop:`, event names): https://v2-rebuild--solid-docs-v2.netlify.app/guides/integrate-non-solid-code.md
30
31
  - Typed env (`env.ts` schema, `VITE_` client keys), `import "server-only"`, what compiles `"use server"`: https://v2-rebuild--solid-docs-v2.netlify.app/building-apps/environment.md, https://v2-rebuild--solid-docs-v2.netlify.app/reference/vite-plugin-solid/modules.md, https://v2-rebuild--solid-docs-v2.netlify.app/reference/vite-plugin-solid/server-functions.md
31
32
  - Sessions and cookies (`parseCookieHeader` / `serializeCookie`, `RequestEventLocals` typing, when a cookie write commits): https://v2-rebuild--solid-docs-v2.netlify.app/building-apps/sessions-and-auth.md, https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-web/request-response/cookies.md
@@ -158,6 +159,7 @@ the error hooks fire on every build.
158
159
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/advanced/diagnostics-dev-hooks/configure-client-errors.md
159
160
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-web/request-response/configure-server-errors.md
160
161
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-web/request-response/get-trace-context.md
162
+ - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-web/performance-tracks/enable-performance-tracks.md
161
163
 
162
164
  ## Reference: components and JSX (solid-js)
163
165
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "solid2-agent-kit",
3
- "version": "0.18.0",
3
+ "version": "0.20.0",
4
4
  "description": "Teach AI coding agents (Cursor, Claude Code) to write correct Solid 2.0 instead of React or Solid 1.x — always-applied rules, an agent skill, and a mechanical pattern gate.",
5
5
  "type": "module",
6
6
  "license": "MIT",