solid2-agent-kit 0.22.0 → 0.23.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/README.md CHANGED
@@ -16,7 +16,7 @@ Supported agents: **Cursor** and **Claude Code**.
16
16
  | Always-applied hard rules | `.cursor/rules/solid-2.mdc` (glob-attached to `*.tsx`/`*.jsx`) | managed block in `CLAUDE.md` | 23 hard rules + banned Solid 1.x API table + eslint-plugin-solid lint-heritage table. Injected without the agent having to decide to read anything |
17
17
  | Agent skill | `.cursor/skills/solid-2/` | `.claude/skills/solid-2/` | Execution-model primer, decision tables, canonical patterns, a “still runs / write the other form” table, review checklist, official-doc URL index |
18
18
  | Shared agent context | managed block in `AGENTS.md` | same (Claude Code users can reference it from `CLAUDE.md`) | Core principles and pointers, always in context |
19
- | Mechanical gate | `solid2-kit check` | same | Comment-stripped regex guard over `src/**/*.{ts,tsx,jsx}` (or explicit `[paths...]` — files are checked, directories are walked, a missing path fails): fails on props destructuring (including multi-line signatures), `{ ...props }` rest copies, `{list().map(...)}` in JSX, React imports/hooks/JSX props/`React.lazy`/`useOptimistic`/`onDoubleClick`, Solid 1.x imports/APIs/components/JSX namespaces (valueless `use:x` included), Solid 1.x `createEffect(fn)` / `createEffect(fn, init)` / `createMemo(fn, init)` signatures, `const [x] = createProjection(...)` (it returns the store, not a tuple), `vite-plugin-solid`, old-router `<Route>`/`<A>`/`<HashRouter>`/`<FileRoutes>`, `MetaProvider`, `Context.Provider`, camelCase style keys, `key` props (except on `@solidjs/meta` tags), `value()!` hand-narrowing, hand-rolled loading signals, `=== undefined` readiness branches, SolidStart leftovers (`@solidjs/start`, `h3`, `createAsync`, `useSubmission`, `"use client"`), Next.js imports/`useRouter`/`typeof window`, `history.pushState`, `render(<App />)` (pass a function), `isPending(user())` / `latest(user())` (pass the accessor), `<Dynamic>` (use `dynamic()`), `import type { JSX } from "solid-js"` (no `JSX` namespace there — import `JSX` from `@solidjs/web`, or use an aliased `Element` from `solid-js`), two-phase effects whose apply only writes a local signal (use a writable derivation), core `action(async ...)` with a plain async function instead of a generator (only `yield` re-enters the transaction), and `setStore(async ...)` on a `createStore` / `createOptimisticStore` setter (the draft is a synchronous transaction; dev throws `[ASYNC_STORE_SETTER]`) |
19
+ | Mechanical gate | `solid2-kit check` | same | Comment-stripped regex guard over `src/**/*.{ts,tsx,jsx}` (or explicit `[paths...]` — files are checked, directories are walked, a missing path fails): fails on props destructuring (including multi-line signatures), `{ ...props }` rest copies, `{list().map(...)}` in JSX, React imports/hooks/JSX props/`React.lazy`/`useOptimistic`/`onDoubleClick`, Solid 1.x imports/APIs/components/JSX namespaces (valueless `use:x` included), Solid 1.x `createEffect(fn)` / `createEffect(fn, init)` / `createMemo(fn, init)` signatures, `const [x] = createProjection(...)` (it returns the store, not a tuple), `vite-plugin-solid`, old-router `<Route>`/`<A>`/`<HashRouter>`/`<FileRoutes>`, `MetaProvider`, `Context.Provider`, camelCase style keys, `key` props (except on `@solidjs/meta` tags), `value()!` hand-narrowing, hand-rolled loading signals, `=== undefined` readiness branches, SolidStart leftovers (`@solidjs/start`, `h3`, `createAsync`, `useSubmission`, `"use client"`), Next.js imports/`useRouter`/`typeof window`, `history.pushState`, `render(<App />)` (pass a function), `isPending(user())` / `latest(user())` (pass the accessor), `<Dynamic>` (use `dynamic()`), `import type { JSX } from "solid-js"` (no `JSX` namespace there — import `JSX` from `@solidjs/web`, or use an aliased `Element` from `solid-js`), two-phase effects whose apply only writes a local signal (use a writable derivation), core `action(async ...)` with a plain async function instead of a generator (only `yield` re-enters the transaction), the router's deprecated `action` / `Action` aliases and `useAction` (use `formAction`; call server functions directly), and `setStore(async ...)` on a `createStore` / `createOptimisticStore` setter (the draft is a synchronous transaction; dev throws `[ASYNC_STORE_SETTER]`) |
20
20
  | Edit-time hook | `postToolUse` entry in `.cursor/hooks.json` | `PostToolUse` entry in `.claude/settings.json` (matcher includes `Bash`) | Runs the mechanical gate automatically on **every agent file edit — including edits made through shell commands** (sed, heredocs, codemods; source paths mentioned in the command are checked after it ran) — and feeds findings straight back into the conversation (Cursor: `additional_context`; Claude Code: stderr + exit 2), so enforcement does not depend on the agent remembering to run `check`. Wired only when the kit is a local devDependency; opt out with `--no-hooks` |
21
21
  | Turn-end gate | — | `Stop` entry in `.claude/settings.json` | Whole-project `check` + `doctor` when the agent tries to end its turn: with findings in place the stop is blocked (exit 2) and the findings are fed back, so a turn cannot finish with React/Solid 1.x patterns left behind. Loop-safe via `stop_hook_active` (one forced continuation, never an infinite loop) |
22
22
  | Pre-execution guard | `preToolUse` + `beforeShellExecution` entries in `.cursor/hooks.json` | `PreToolUse` entry in `.claude/settings.json` | Denies actions **before they run**: edits/deletes of kit-owned guardrail files (an agent blocked by a gate will try to remove the gate), edits that would strip the kit's hook entries or managed blocks, uninspectable shell rewrites of those files, and `npm/pnpm/yarn/bun install` of banned dependencies (`react`, `vite-plugin-solid`, `eslint-plugin-solid`, SolidStart, ...) — caught proactively instead of post-hoc by `doctor` |
package/bin/baseline.mjs CHANGED
@@ -14,19 +14,20 @@
14
14
  // The JSX compilers (@solidjs/compiler, native and the default;
15
15
  // @solidjs/babel-plugin under `compiler: "babel"`) are listed because compiled
16
16
  // output carries behavior the guidance states (lowercase `on*` is a plain
17
- // attribute since rc.14). Projects rarely declare them — @solidjs/vite-plugin
17
+ // attribute since rc.14; the removed 1.x attribute forms are compile errors
18
+ // since rc.17). Projects rarely declare them — @solidjs/vite-plugin
18
19
  // pulls them in — so doctor also checks an undeclared install of any package
19
20
  // here, which is where a lockfile holds an older compiler.
20
21
  export const BASELINE = {
21
- 'solid-js': { line: 2, baseline: '2.0.0-rc.15' },
22
- '@solidjs/web': { line: 2, baseline: '2.0.0-rc.15' },
23
- '@solidjs/signals': { line: 2, baseline: '2.0.0-rc.15' },
24
- '@solidjs/diagnostics': { line: 2, baseline: '2.0.0-rc.15' },
25
- '@solidjs/compiler': { line: 2, baseline: '2.0.0-rc.15' },
26
- '@solidjs/babel-plugin': { line: 2, baseline: '2.0.0-rc.15' },
27
- '@solidjs/router': { line: 2, baseline: '2.0.0-next.39' },
28
- '@solidjs/vite-plugin': { line: 3, baseline: '3.0.0-next.50' },
29
- '@solidjs/testing-library': { line: 1, baseline: '1.0.0-beta.3' },
22
+ 'solid-js': { line: 2, baseline: '2.0.0-rc.17' },
23
+ '@solidjs/web': { line: 2, baseline: '2.0.0-rc.17' },
24
+ '@solidjs/signals': { line: 2, baseline: '2.0.0-rc.17' },
25
+ '@solidjs/diagnostics': { line: 2, baseline: '2.0.0-rc.17' },
26
+ '@solidjs/compiler': { line: 2, baseline: '2.0.0-rc.17' },
27
+ '@solidjs/babel-plugin': { line: 2, baseline: '2.0.0-rc.17' },
28
+ '@solidjs/router': { line: 2, baseline: '2.0.0-next.42' },
29
+ '@solidjs/vite-plugin': { line: 3, baseline: '3.0.0-next.51' },
30
+ '@solidjs/testing-library': { line: 1, baseline: '1.0.0-rc.2' },
30
31
  };
31
32
 
32
33
  export function parseVersion(version) {
@@ -395,9 +395,10 @@ function effectSyncFindings(content) {
395
395
  }
396
396
 
397
397
  // Core `action` (solid-js) takes a generator (`function*` / `async function*`)
398
- // so `yield` can re-enter the transaction; router `action` (@solidjs/router)
399
- // legitimately takes `async (form) => ...`. Only flag `action(async` when this
400
- // file's `action` comes from "solid-js" — a router import (or none) stays silent.
398
+ // so `yield` can re-enter the transaction; router `formAction` (@solidjs/router,
399
+ // and its deprecated `action` alias) legitimately takes `async (form) => ...`.
400
+ // Only flag `action(async` when this file's `action` comes from "solid-js" — a
401
+ // router import (or none) stays silent.
401
402
  function actionAsyncFindings(content) {
402
403
  if (!/import\s*\{[^}]*\baction\b[^}]*\}\s*from\s*['"]solid-js['"]/.test(content)) return [];
403
404
  if (/import\s*\{[^}]*\baction\b[^}]*\}\s*from\s*['"]@solidjs\/router['"]/.test(content)) return [];
@@ -576,7 +577,8 @@ const CHECKS = [
576
577
  {
577
578
  id: 'solid1-classlist',
578
579
  pattern: /\bclassList=/g,
579
- message: 'Solid 1.x `classList` was removed. Use the object/array form of `class`.',
580
+ message:
581
+ 'Solid 1.x `classList` was removed (a compile error on native elements since 2.0.0-rc.17). Use the object/array form of `class`.',
580
582
  },
581
583
  {
582
584
  id: 'react-hook',
@@ -587,7 +589,7 @@ const CHECKS = [
587
589
  {
588
590
  id: 'solid1-api',
589
591
  pattern:
590
- /(?<![.\w])(?:createResource|onMount|createMutable|modifyMutable|mergeProps|splitProps|produce|unwrap|createComputed|createSelector|createDeferred|startTransition|batch|onError|catchError|createDynamic|renderToStringAsync|clearDelegatedEvents)\s*\(/g,
592
+ /(?<![.\w])(?:createResource|onMount|createMutable|modifyMutable|mergeProps|splitProps|produce|unwrap|createComputed|createSelector|startTransition|batch|onError|catchError|createDynamic|renderToStringAsync|clearDelegatedEvents)\s*\(/g,
591
593
  message:
592
594
  'Removed Solid 1.x API. Replacements: async createMemo, onSettled, createStore drafts, merge/omit/snapshot, automatic batching, Errored, dynamic(), renderToStream.',
593
595
  },
@@ -688,7 +690,7 @@ const CHECKS = [
688
690
  pattern:
689
691
  /(?<=\s)(?:use|on|oncapture|attr|bool):[A-Za-z][\w-]*(?==|\s*\/?>|\s+(?!(?:as|satisfies|in|instanceof)\b(?!\s*=))[A-Za-z_${])/g,
690
692
  message:
691
- 'Solid 1.x JSX namespace. Use ref callbacks (and directive factories), camelCase event props, and standard attributes.',
693
+ 'Solid 1.x JSX namespace — a compile error on native elements since 2.0.0-rc.17. Use ref callbacks (and directive factories), camelCase event props, and standard attributes; `prop:` remains for DOM properties.',
692
694
  },
693
695
  {
694
696
  id: 'solid1-router',
@@ -752,9 +754,9 @@ const CHECKS = [
752
754
  },
753
755
  {
754
756
  // 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
757
+ // attribute is a plain attribute (dev warns [LOWERCASE_EVENT_ATTRIBUTE])
758
+ // that never runs the handler; a 1.x `on:click` on a native element is a
759
+ // compile error since rc.17 (solid1-jsx-namespace, above). A real event name in lowercase is always a leftover. Anchored
758
760
  // on `={` (a JSX expression attribute): `const onmessage = ...`,
759
761
  // `ws.onerror = fn`, and HTML strings (`onerror="…"`) are not matches,
760
762
  // and neither is a string expression (`onclick={"track()"}`) — a string
@@ -772,9 +774,9 @@ const CHECKS = [
772
774
  // 2.0.0-next.38 and preloads nothing by default: `true` (the old on switch)
773
775
  // preloads nothing and only warns in dev, and `false` is a no-op.
774
776
  id: 'preload-links-boolean',
775
- pattern: /\bpreloadLinks\s*:\s*(?:true|false)\b/g,
777
+ pattern: /(?<![\w$])["']?preloadLinks["']?\s*:\s*(?:true|false)\b/g,
776
778
  message:
777
- '`preloadLinks` takes link preload strategies, not a boolean, and preloads nothing by default. Hover/focus warming is preloadLinks: intentPreload() (from "@solidjs/router"); to disable it, omit the option.',
779
+ '`preloadLinks` takes link preload strategies, not a boolean, and preloads nothing by default. Hover/focus warming is preloadLinks: intentPreload() (from "@solidjs/router"); for none, omit the option.',
778
780
  },
779
781
  {
780
782
  id: 'history-nav',
@@ -826,10 +828,44 @@ const CHECKS = [
826
828
  id: 'action-plain-async',
827
829
  find: actionAsyncFindings,
828
830
  message:
829
- 'Core `action` takes a generator (`function*` / `async function*`), not a plain async function — only `yield` re-enters the transaction, so writes after a bare `await` commit immediately. (Router `action` from "@solidjs/router" is the one that takes `async (form) => ...`.)',
831
+ 'Core `action` takes a generator (`function*` / `async function*`), not a plain async function — only `yield` re-enters the transaction, so writes after a bare `await` commit immediately. (Router `formAction` from "@solidjs/router" is the one that takes `async (form) => ...`.)',
832
+ },
833
+ {
834
+ // `action` / `Action` from "@solidjs/router" are deprecated aliases of
835
+ // `formAction` / `FormAction` since 2.0.0-next.41 (dev warns on the first
836
+ // call); `action` is solid-js's own primitive.
837
+ id: 'router-action-alias',
838
+ find: (content) => routerImportFindings(content, ['action', 'Action']),
839
+ message:
840
+ 'Router `action` / `Action` are deprecated aliases (@solidjs/router 2.0.0-next.41; dev warns). Import `formAction` / `FormAction` from "@solidjs/router" — `action` is solid-js\'s own primitive.',
841
+ },
842
+ {
843
+ // `useAction` is deprecated with no replacement: a mounted router applies a
844
+ // server function's revalidation and redirect on any call.
845
+ id: 'router-use-action',
846
+ find: (content) => routerImportFindings(content, ['useAction']),
847
+ message:
848
+ '`useAction` is deprecated (@solidjs/router 2.0.0-next.41; dev warns): outside a form, call the server function (or a core `action` from "solid-js") directly — the router applies its revalidation and redirect either way. Bind form actions to forms.',
830
849
  },
831
850
  ];
832
851
 
852
+ // Named imports from "@solidjs/router" among `names`, located at the name inside
853
+ // the import list. Only the imported name counts: `action as save` is flagged,
854
+ // `formAction as action` (the local name) is not.
855
+ function routerImportFindings(content, names) {
856
+ const findings = [];
857
+ for (const statement of content.matchAll(/import\s+(?:type\s+)?\{([^}]*)\}\s*from\s*['"]@solidjs\/router['"]/g)) {
858
+ const list = statement[1];
859
+ const offset = statement.index + statement[0].indexOf(list);
860
+ for (const name of names) {
861
+ for (const match of list.matchAll(new RegExp(`(?<![\\w$.])(?<!\\bas\\s+)(?:type\\s+)?${name}(?![\\w$])`, 'g'))) {
862
+ findings.push({ index: offset + match.index });
863
+ }
864
+ }
865
+ }
866
+ return findings;
867
+ }
868
+
833
869
  // .ts / .tsx / .jsx sources, excluding .d.ts declaration files.
834
870
  const SOURCE_FILE = /(?<!\.d)\.(?:tsx?|jsx)$/;
835
871
 
@@ -19,8 +19,9 @@ Steps:
19
19
  - effects that copy state instead of deriving it, or reading stores in the apply phase;
20
20
  - `<For>` over server/refetched rows without a stable-id key function;
21
21
  - core `action` bodies that are not generators (plain `async` functions) or that write
22
- after a bare `await` without re-entering on `yield` (router `action` from
23
- `@solidjs/router` is the one that takes `async (form) => ...`);
22
+ after a bare `await` without re-entering on `yield` (router `formAction` from
23
+ `@solidjs/router` is the one that takes `async (form) => ...`; its `action` alias
24
+ and `useAction` are deprecated <!-- upstream:router-action-deprecated -->);
24
25
  - `<Loading>`/`<Errored>` wrapping page chrome instead of the data slot, or
25
26
  `const u = user()` extracted then passed (a real parent-side read). Passing
26
27
  `user={user()}` is the colorless form — do not "fix" it into accessors or
@@ -36,18 +37,19 @@ Steps:
36
37
  `(v) => setX(v)`) — any return value but a cleanup halts reactivity; `onCleanup`
37
38
  inside `apply` (never runs) or inside `onSettled`, and memos/effects/`flush()`
38
39
  inside an `onSettled` callback (all throw);
39
- - writes to a store outside its setter (`todos.push(x)`, `todo.done = ...` — silently
40
- ignored) and `Map`/`Set`/`Date` fields mutated in a draft (not tracked);
40
+ - writes to a store outside its setter (`todos.push(x)`, `todo.done = ...` — ignored;
41
+ dev warns `[STORE_WRITE_OUTSIDE_SETTER]` <!-- upstream:store-write-outside-setter -->) and
42
+ `Map`/`Set`/`Date` fields mutated in a draft (not tracked);
41
43
  - app-wide state as a module-level signal/store instead of a provider at the root
42
44
  of `App` (shared across SSR requests);
43
45
  - value-form `createOptimisticStore([])` / `createOptimistic(v)` holding durable
44
46
  data (writes vanish on settle) <!-- upstream:optimistic-value-overlay --> instead of the function form refreshed after `yield`;
45
47
  - route `preload` returning data read as `props.data` (captured once per match)
46
48
  instead of `void getX(params.id)` + `createMemo(() => getX(props.params.id))`;
47
- - `throw redirect()` / `return reload()` in server functions called without Solid
48
- Router `action`/`query` (the caller gets a raw `Response` or `null`, never its
49
- data), and POST forms built on a bare function's `.url` (TS2339) instead of a
50
- router `action`;
49
+ - `throw redirect()` / `return reload()` in server functions of an app with no
50
+ mounted router (the caller gets a raw `Response`, never its data; under a router
51
+ the single-flight hook applies them on any call), and POST forms built on a bare
52
+ function's `.url` (TS2339) instead of a router `formAction`;
51
53
  error reporting done as a side effect inside `fallback` instead of
52
54
  `configureClientErrors` / `configureServerErrors` / `render(fn, root, undefined, { onError })`
53
55
  (options are `render`'s 4th argument — a 3rd-argument object is `init`);
@@ -74,6 +76,11 @@ Steps:
74
76
  `useSearchParams`; <!-- upstream:pending-links-opt-in --> route `preload`
75
77
  functions written to run on hover in a router without `preloadLinks: intentPreload()`
76
78
  (link preloading is opt-in; a boolean `preloadLinks` preloads nothing) <!-- upstream:preload-links-opt-in -->;
79
+ - `value` / `checked` bound to a committed value with no per-edit write
80
+ (`value={props.name} onChange={commit}`): the binding re-runs with its template and
81
+ overwrites uncommitted text (`[UNCOMMITTED_INPUT_OVERWRITTEN]`
82
+ <!-- upstream:uncommitted-input-overwritten -->) — a draft signal from `onInput`,
83
+ `defaultValue`, or an explicit write-back;
77
84
  - search inputs bound to a held value without `latest`, effect-based debounce, fully
78
85
  controlled forms without `name`s, `{ success: false }` result objects instead of
79
86
  thrown failures, or router forms without server-side validation (`throw respond(...)`)
@@ -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. 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 -->; link preloading is opt-in too (`preloadLinks: intentPreload()` for hover/focus; none by default) <!-- upstream:preload-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.
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 `formAction`/`query` from `@solidjs/router` are POST forms and a read cache (router `action` is a deprecated alias of `formAction`, and `useAction` is deprecated: call the server function directly — a mounted router applies its revalidation and redirect on any call <!-- upstream:router-action-deprecated -->). React `useDeferredValue` is `createDeferred(fn)`: an async memo that lags but never leads <!-- upstream:create-deferred-export -->. 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 ignored (dev warns `[STORE_WRITE_OUTSIDE_SETTER]` <!-- upstream:store-write-outside-setter -->), 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 -->; link preloading is opt-in too (`preloadLinks: intentPreload()` for hover/focus/touch; none by default; opt one link out with the string `preload="false"`) <!-- upstream:preload-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 reads `live(fn)` — it implies `GET` (`live(GET(fn))` is the same) <!-- upstream:live-implies-get -->. 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 -->, a held write needs `isPending`/`latest`/optimistic/`affects` feedback or dev + attribution reports `[SILENT_HOLD]`, and `[UNCOMMITTED_INPUT_OVERWRITTEN]` means a `value`/`checked` binding re-wrote the bound value over text the user typed but never committed (`value={props.name} onChange={commit}` with no draft signal) — bind a draft signal written from `onInput`, or `defaultValue` <!-- upstream:uncommitted-input-overwritten -->. 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.
@@ -64,7 +64,8 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
64
64
  (a nested child). Never pass a setter as the handler (`onClick={setCount}`) —
65
65
  that writes the event object; wrap it: `onClick={() => setCount((c) => c + 1)}`.
66
66
  Only `on` + uppercase binds an event: lowercase `onclick` is a plain attribute
67
- (dev warns `[LOWERCASE_EVENT_ATTRIBUTE]`), not a handler.
67
+ (dev warns `[LOWERCASE_EVENT_ATTRIBUTE]`), not a handler, and the 1.x `on:click`
68
+ namespace is a compile error on a native element since 2.0.0-rc.17.
68
69
  <!-- upstream:lowercase-event-attribute -->
69
70
  Debounce at the handler (`onInput={debounce(...)}`), never as an effect copying
70
71
  one signal into another.
@@ -94,7 +95,9 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
94
95
  from `apply` — `onCleanup` there never runs (`[NO_OWNER_CLEANUP]`).
95
96
  Single-argument `createEffect(fn)` is an error in Solid 2. Do not substitute
96
97
  `createTrackedEffect` for that — it is `@deprecated` since 2.0.0-rc.9 (kept only for
97
- 1.x migration) <!-- upstream:tracked-effect-deprecated -->. Skip the initial run with `{ defer: true }`. Most React `useEffect` code should not become an effect at all — see the skill.
98
+ 1.x migration) <!-- upstream:tracked-effect-deprecated -->. Skip the initial run with `{ defer: true }`. Effects never compare: `apply`
99
+ runs on every re-run of `compute`, equal value or not — read a `createMemo` in
100
+ `compute` when equal results should skip the work. Most React `useEffect` code should not become an effect at all — see the skill.
98
101
  9. **Stores update by mutating a draft**: `setStore(draft => { draft.user.name = "Ada" })`.
99
102
  Never rebuild with spreads — that destroys property-level subscriptions. The return
100
103
  form is for shapes where mutation is awkward — most commonly removal
@@ -107,8 +110,10 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
107
110
  `setState({ a: 1 })` throw `fn is not a function`, and an object *returned* from the
108
111
  callback replaces the whole store (keys it omits are deleted).
109
112
  The store itself is read-only: `todos.push(x)`, `state.count++`, or
110
- `todo.done = !todo.done` outside a setter is **silently ignored** (no throw, no
111
- warning, and TypeScript accepts it) — every write goes through the setter's draft.
113
+ `todo.done = !todo.done` outside a setter is **ignored** (no throw, TypeScript
114
+ accepts it; dev warns `[STORE_WRITE_OUTSIDE_SETTER]` once per store and key since
115
+ 2.0.0-rc.17, production stays silent) <!-- upstream:store-write-outside-setter --> —
116
+ every write goes through the setter's draft.
112
117
  `Map`, `Set`, and `Date` are stored as-is, not proxied: `draft.selected.add(id)`
113
118
  changes the object but notifies nobody. Model sets as `Record<id, true>` (or an
114
119
  array), or assign a new instance in the setter (`draft.tags = new Set([...draft.tags, t])`).
@@ -177,7 +182,10 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
177
182
  `createContext<T>()` without a default throws when read outside a provider (good) — use
178
183
  that for any reactive payload. `createContext("light")` is only for a primitive static
179
184
  fallback; a dummy default on a store/signal context *runs* and then silently no-ops
180
- without a provider. **Shared state lives in a provider, including app-wide state**
185
+ without a provider. `undefined` means *not provided*: `createContext(undefined)` is
186
+ the default-less form, and `<Ctx value={undefined}>` counts as no provider (the read
187
+ throws). An optional value is `createContext<T | null>(null)` and `value={null}`.
188
+ <!-- upstream:context-undefined-unprovided --> **Shared state lives in a provider, including app-wide state**
181
189
  (theme, session, cart, locale): a factory such as `createCart()` called by a provider
182
190
  component placed at the root of `App`, read through `createContext<T>()` (no default)
183
191
  and a `useCart()` hook. Module scope is for constants and the `createContext` call itself —
@@ -189,8 +197,9 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
189
197
  turned on.
190
198
  12. Refs: `let el!: HTMLDivElement` + `ref={el}` or `ref={(node) => (el = node)}`, or forward
191
199
  `props.ref`. Compose with arrays: `ref={[props.ref, (node) => (el = node)]}`. No
192
- `useRef`/`.current`. Ref callbacks run untracked and **without an owner**; their
193
- return values are ignored. Do not create effects or register cleanup inside a ref
200
+ `useRef`/`.current`. Ref callbacks run untracked and **without an owner**, before the
201
+ element is attached (`el.isConnected` is `false`; measure or focus in `onSettled`);
202
+ their return values are ignored. Do not create effects or register cleanup inside a ref
194
203
  callback. For reusable directives, create owned primitives (`onSettled`) in a
195
204
  factory and return only the element callback. Component setup/teardown is
196
205
  `onSettled(() => { ...; return cleanup })` — `onCleanup` is for custom
@@ -215,7 +224,12 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
215
224
  14. Do not port these React tools — they have no Solid equivalent because the problems they
216
225
  solve don't exist: `useCallback`, `React.memo`, `forwardRef`, `useSyncExternalStore`,
217
226
  dependency arrays, `startTransition`/`useTransition` (updates are held and coordinated
218
- automatically; use `isPending` for indicators).
227
+ automatically; use `isPending` for indicators). The one with a counterpart is
228
+ `useDeferredValue`: `createDeferred(() => filter(rows(), query()))` is an async memo
229
+ whose value may lag its inputs but never leads them — the UI stays interactive while an
230
+ expensive derivation catches up, and `isPending(deferred)` reports the catch-up. It is
231
+ a Solid 2.0 API, unrelated to the 1.x `createDeferred(source, { timeoutMs })`.
232
+ <!-- upstream:create-deferred-export -->
219
233
  15. **Components return once.** Never early-return or conditionally return based on reactive
220
234
  values — the branch is chosen once at setup and frozen forever. Put conditionals inside
221
235
  JSX (`<Show>`, ternary, `<Switch>`/`<Match>`). Early returns on genuinely non-reactive
@@ -295,6 +309,14 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
295
309
  React-style controlled input that ignores invalid keystrokes silently breaks. To reject
296
310
  input, write back explicitly:
297
311
  `onInput={(e) => { if (valid(e.currentTarget.value)) setV(e.currentTarget.value); else e.currentTarget.value = v(); }}`.
312
+ The converse is the committed-value binding: `value={props.name} onChange={commit}`
313
+ with no per-keystroke write re-applies `props.name` whenever anything that binding's
314
+ template reads changes, wiping text the user typed but had not committed — dev reports
315
+ `[UNCOMMITTED_INPUT_OVERWRITTEN]` since 2.0.0-rc.17
316
+ <!-- upstream:uncommitted-input-overwritten -->. Bind a draft signal written from
317
+ `onInput` (`createSignal(() => props.name)` resets it when the prop changes), use
318
+ `defaultValue` for an uncontrolled field, or write the next value back explicitly;
319
+ `equals: false` on the signal does not help (the binding, not the signal, re-runs).
298
320
  22. Miscellaneous defaults: SSR-stable element ids come from `createUniqueId()` (never
299
321
  `Math.random()` or hardcoded duplicates); pass store data to `structuredClone` /
300
322
  `postMessage` / logs via `snapshot(store)` (proxies fail or leak reactivity) — a
@@ -380,13 +402,27 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
380
402
  state with `dynamic(() => ...)` from `@solidjs/web`
381
403
  (stable identity); `<Dynamic component={...}>` is deprecated (`@deprecated` in the
382
404
  types since 2.0.0-rc.9, still shipped in 2.0) <!-- upstream:dynamic-deprecated -->. If the project has `@solidjs/router`, routes are `createRouter({ routes })`
383
- at module scope — not JSX `<Route>` / `<A>` / `<HashRouter>`. That package's `action` /
405
+ at module scope — not JSX `<Route>` / `<A>` / `<HashRouter>`. That package's `formAction` /
384
406
  `query` are URL-addressable POST forms and a read cache — not core `action` /
385
- `refresh` from `solid-js`. Router mutations: `<form action={save} method="post">`
407
+ `refresh` from `solid-js` (router `action` / `Action` are deprecated aliases of
408
+ `formAction` / `FormAction` since 2.0.0-next.41, with a dev warning; `useAction` is
409
+ deprecated with no replacement, below) <!-- upstream:router-action-deprecated -->.
410
+ Router mutations: `<form action={save} method="post">`
386
411
  (POST only; `.with(id)` binds args into the URL). Validate on the server
387
412
  (`throw respond({ issues }, { status: 400 })`); inline errors from `useSubmissions`,
388
413
  pending via `form[aria-busy]` CSS. Keep server functions in `src/data/`, not under
389
- `src/routes` (a `.ts` file there is an API route). `useAction` is JS-only. Cache
414
+ `src/routes` (a `.ts` file there is an API route). Outside a form, call the server
415
+ function (or a core `action`) directly — single flight is the router's: a mounted router
416
+ applies any server function's `revalidate` keys, `redirect()` / `reload()` and
417
+ flight data on every call, so `useAction` wrappers are unnecessary. Every POST
418
+ server-function call is a mutation to it: with no `revalidate` keys the server
419
+ re-collects the current page's reads and the client revalidates every query; a POST
420
+ that changed nothing returns `respond(value, { revalidate: "" })`, reads go through
421
+ `query()` (which switches the function to GET) or `GET(fn)`, and
422
+ `createRouter({ routes, singleFlight: false })` opts the whole app out
423
+ <!-- upstream:router-post-mutation -->. Params arrive decoded (`/users/%E7%8C%AB`
424
+ gives `params.id === "猫"`; `Router.paths` encodes on the way out); `useInRouter()`
425
+ tells a component whether a router is mounted. Cache
390
426
  reads: `query(fn, "name")` then `createMemo(() => getUser(id()))`; after a mutation
391
427
  `revalidate(getUser.key)`. Do not wrap mutations in `query` (an undeclared server
392
428
  function becomes GET). `Router.paths` is not the current location (`useLocation` /
@@ -399,7 +435,9 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
399
435
  `preload` makes it a Promise), so return a value only for data that stays fixed while
400
436
  the route is matched. Link warming is opt-in: route code and `preload` run ahead of
401
437
  the click only with a strategy — `createRouter({ routes, preloadLinks: intentPreload() })`
402
- for hover/focus (none by default; a boolean `preloadLinks` only warns in dev).
438
+ for hover/focus/touch (none by default; a boolean `preloadLinks` is TS2322 and preloads
439
+ nothing). Opt a link out with the string `preload="false"` — `preload={false}` removes
440
+ the attribute and opts nothing out.
403
441
  <!-- upstream:preload-links-opt-in --> One router instance —
404
442
  Solid Router does not support nested `<Router>` / nested `createRouter`.
405
443
  In-app navigation is `useNavigate` or `<a href={Router.paths...}>`, not
@@ -412,7 +450,7 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
412
450
  `useIsRouting` in JSX — never hand-rolled `location.pathname` comparisons.
413
451
  <!-- upstream:pending-links-opt-in --> Trusted identity is
414
452
  `getRequestEvent()`, never a caller-supplied user id. Unscripted (no-JS) POST
415
- forms use a router `action` over a `(form: FormData)` server function (the post
453
+ forms use a router `formAction` over a `(form: FormData)` server function (the post
416
454
  arrives as one `FormData`; other signatures make `action={save}` TS2322) —
417
455
  `<form action={save} method="post">` — not the
418
456
  function's `.url` and not a hand-built `/_server/` path: a bare `"use server"`
@@ -432,13 +470,13 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
432
470
  server render) <!-- upstream:ssr-error-sanitized --> — so intentional client-facing failures are `markSafeError(...)` or
433
471
  `throw respond(body, { status })`. Reads are function calls too (`await getUser(id)`
434
472
  on a `GET()`-declared read; the method mirrors the operation). `redirect()` /
435
- `reload()` are signals for an integration: Solid Router's `action` / `query` and the
436
- no-JS form post apply them. A direct caller — a core `action`, a handler, a plain
437
- memo — never gets your data: it receives the raw `Response` as the resolved value
438
- (typed as your data, no navigation); only when a mounted router's single-flight
439
- hook intercepts a direct POST call does it navigate, and the call then resolves to
440
- `null`. So guards `throw redirect("/sign-in")` only when router `action` /
441
- `query` call the function; without the router, `throw respond(null, { status: 401 })`
473
+ `reload()` are signals for an integration: Solid Router's `formAction` / `query`, the
474
+ no-JS form post, and (since router 2.0.0-next.41) the mounted router's single-flight
475
+ hook on any direct server-function call apply them. Without a mounted router a direct
476
+ caller — a core `action`, a handler, a plain memo — never gets your data: it receives
477
+ the raw `Response` as the resolved value (typed as your data, no navigation); with one,
478
+ the hook navigates and the call resolves to `null`. So guards `throw redirect("/sign-in")`
479
+ only in a routed app; without the router, `throw respond(null, { status: 401 })`
442
480
  (the call rejects) or return a value and navigate in the caller. `redirect()` /
443
481
  `respond()` take `revalidate` like `reload()`. Cache GET reads with
444
482
  `Cache-Control` on that response (the default is `no-store`), not a hand-rolled
@@ -459,15 +497,18 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
459
497
  reads `PORT` / `HOST`; its exported `listener` mounts in an existing Express /
460
498
  Fastify server) — `server.js` is only the handler module. Client-only start mode is static `dist/client` —
461
499
  there is no handler to wrap. Request middleware is
462
- `start: { middleware: "./src/middleware.ts" }`, not Express `app.use`. Do not return
500
+ `start: { middleware: "./src/middleware.ts" }`, not Express `app.use`: default export
501
+ `(event, next) => Response` (`StartMiddleware`), request at `event.request`, `next()`
502
+ with no arguments <!-- upstream:start-middleware-event -->. Do not return
463
503
  components from `"use server"` unless the project already enabled the
464
504
  experimental `serverFunctions.components` flag. Server-function arguments are JSON:
465
505
  send `Date` / `Map` / `Set` as ISO strings / arrays (they throw otherwise; one `File` /
466
506
  `Blob` / `FormData` argument travels natively). `enableRichArguments()` from
467
507
  `@solidjs/web/server-functions/rich-args` (at `src/App.tsx` module scope) lifts that
468
508
  (its import builds cleanly since 2.0.0-rc.10 — a `resolve.dedupe: ["@solidjs/web"]`
469
- 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))` —
470
- `live()` outermost; an undeclared async iterable still producing while a document
509
+ 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(fn)` —
510
+ it implies `GET` since 2.0.0-rc.17 (`live(GET(fn))` is the same declaration, `live()`
511
+ outermost) <!-- upstream:live-implies-get -->; an undeclared async iterable still producing while a document
471
512
  renders is `[SSR_UNDECLARED_LIVE_SOURCE]` in dev. A plain `async function*` server function is an event stream on
472
513
  one connection (no reconnect when it drops); `live()` is one value that changes over
473
514
  time and reconnects. `live()` connection state is
@@ -497,8 +538,8 @@ directory (default `src/`).
497
538
  | `solid/imports` | changed paths | banned-API table + `solid2-kit check` |
498
539
  | `solid/event-handlers` | still valid | rule 6 |
499
540
  | `solid/jsx-no-undef`, `jsx-no-duplicate-props` | covered by TypeScript | project typecheck |
500
- | `solid/prefer-classlist` | obsolete — `classList` removed | `solid2-kit check` bans `classList` |
501
- | `solid/no-unknown-namespaces` | obsolete — `on:`/`use:`/`attr:` removed in 2.0 (`prop:` remains) | banned-API table + `solid2-kit check` |
541
+ | `solid/prefer-classlist` | obsolete — `classList` removed (a compile error since 2.0.0-rc.17) | `solid2-kit check` bans `classList` |
542
+ | `solid/no-unknown-namespaces` | obsolete — `on:`/`use:`/`attr:` removed in 2.0 (`prop:` remains); compile errors on native elements since 2.0.0-rc.17 <!-- upstream:removed-attribute-compile-error --> | banned-API table + `solid2-kit check` |
502
543
  | `solid/no-proxy-apis` | obsolete — Solid 2 requires Proxy | not carried |
503
544
 
504
545
  ## Banned Solid 1.x APIs (Solid 2 replacements)
@@ -517,8 +558,9 @@ directory (default `src/`).
517
558
  | `setState("a", "b", value)` path setters, `setState({ a })` merges, `produce(...)` | draft setter: `setState(draft => { ... })` (the others throw `fn is not a function`) |
518
559
  | `mergeProps` / `splitProps` / `unwrap` | `merge` / `omit` / `snapshot` |
519
560
  | `classList={...}` | `class` object/array form |
520
- | `createMutable`, `modifyMutable` | `createStore` + draft setters (direct writes to the store are silently ignored) |
521
- | `createComputed`, `createSelector`, `createDeferred` | `createMemo`, `createProjection`, external scheduling |
561
+ | `createMutable`, `modifyMutable` | `createStore` + draft setters (direct writes to the store are ignored; dev warns `[STORE_WRITE_OUTSIDE_SETTER]`) |
562
+ | `createComputed`, `createSelector` | `createMemo`, `createProjection` |
563
+ | `createDeferred(source, { timeoutMs })` (1.x: a signal that lags by idle time) | `createDeferred(fn)` (2.0: an async memo that may lag its inputs but never leads them; `isPending(deferred)` while it catches up) <!-- upstream:create-deferred-export --> |
522
564
  | `startTransition`, `useTransition` | automatic held updates + `isPending` |
523
565
  | `onError` / `catchError` | `<Errored>` or effect bundle `{ effect, error }` |
524
566
  | `from(...)` / `observable(...)` (the solid-js helpers) | inbound: async iterable from a memo; outbound: split effect |
@@ -532,19 +574,12 @@ directory (default `src/`).
532
574
  | `import ... from "@solidjs/start"` / `vinxi` / `h3` / `"use client"` | `GET` from `@solidjs/web/server-functions`; Solid has no `"use client"` |
533
575
  | `render(<App />, root)` | `render(() => <App />, root)` (same for `hydrate` / `renderToString` / `renderToStream`) |
534
576
  | `<MetaProvider>` | no provider — render `<Title>` / `<Meta>` / `<Link>` from `@solidjs/meta` anywhere |
535
- | `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) |
577
+ | `use:directive`, `on:`/`oncapture:`, `attr:`/`bool:`, `/*@once*/` (the namespaces are compile errors on native elements since 2.0.0-rc.17 <!-- upstream:removed-attribute-compile-error -->; 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) |
536
578
  | `resource.loading` / `resource.error` | `<Loading>` boundary / `<Errored>` boundary |
537
579
 
538
580
  When unsure about any API, verify against the official Solid 2.0 docs — fetchable URLs are
539
581
  listed in `references/official-docs.md` next to the `solid-2` skill. Do not guess from
540
- Solid 1.x or React memory. Some official pages still describe the previous release —
541
- these rules win there: the router navigation page lists `data-pending` as automatic
542
- (it needs `links: pendingLinks`) <!-- upstream:docs-pending-links-stale -->, the
543
- `createSignal` reference says a write to a writable derivation wins over a same-tick
544
- recompute (a source change in the same update now re-runs it with the write as
545
- `prev`) <!-- upstream:docs-derived-write-stale -->, and the performance guide and
546
- attribution reference give `[WIDE_SCOPE_DEPS]` a threshold of 30 (the default
547
- `wideDeps` is 200). <!-- upstream:docs-wide-deps-stale -->
582
+ Solid 1.x or React memory.
548
583
 
549
584
  `createRenderEffect` exists in Solid 2 but is not a default. `storePath`
550
585
  (the 1.x path-setter migration helper) is no longer exported from `solid-js` as of
@@ -160,12 +160,15 @@ at component-body top level is a write in an owned scope — dev throws
160
160
  seed the shape through the `createStore` argument or a derivation.
161
161
 
162
162
  **The store itself is read-only; only the draft writes.** An assignment or mutating
163
- call on the store proxy outside a setter is *ignored* — no throw, no dev warning, the
164
- value does not change, and TypeScript accepts it (`Store<T>` is `T`). This is the
163
+ call on the store proxy outside a setter is *ignored* — no throw, the value does not
164
+ change, and TypeScript accepts it (`Store<T>` is `T`). Since 2.0.0-rc.17 dev warns
165
+ `[STORE_WRITE_OUTSIDE_SETTER]` once per store object and key (the property and whether
166
+ it was a set, define or delete); production stays silent
167
+ <!-- upstream:store-write-outside-setter -->. This is the
165
168
  MobX / Vue / Solid 1 `createMutable` reflex:
166
169
 
167
170
  ```tsx
168
- // WRONG — all silently ignored
171
+ // WRONG — all ignored (dev: [STORE_WRITE_OUTSIDE_SETTER])
169
172
  todos.push(todo);
170
173
  state.count++;
171
174
  todo.done = !todo.done;
@@ -241,7 +244,10 @@ exports must use `{ export: "About" }` — a `.then((m) => ({ default: m.About }
241
244
  wrapper hydrates incorrectly (the name is not a call-site literal). A module that is
242
245
  not a component (a route table, a parser) takes `lazyModule(() => import("./x"))` from
243
246
  `solid-js`: call it (or `.preload()`) for the cached promise, `.peek()` for the module
244
- once it has landed; the server preloads it before hydration by module id.
247
+ once it has landed (`undefined` before that, and on the client after a hydration
248
+ miss). A server render that reads `.peek()` registers the module by id so the client
249
+ has it before hydration, holding the shell while it loads; `.preload()` on the server
250
+ only hints assets.
245
251
 
246
252
  ### Dynamic component: `dynamic()` (canonical)
247
253
 
@@ -302,6 +308,10 @@ reactive system (`[REACTIVITY_HALTED]`). TypeScript reports it as TS2345; a JS p
302
308
  gets no warning. The cleanup of an effect is the `apply` return value — `onCleanup`
303
309
  inside `apply` has no owner and never runs (`[NO_OWNER_CLEANUP]`).
304
310
 
311
+ **Effects never compare.** `apply` runs on every re-run of `compute`, whether or not the
312
+ computed value changed; there is no `equals` on an effect. When equal results should skip
313
+ the imperative work, read a `createMemo` (the equality boundary) in `compute`.
314
+
305
315
  ```tsx
306
316
  // WRONG — returns the assigned string; halts reactivity (dev: first run, prod: next run)
307
317
  createEffect(() => title(), (t) => (document.title = t));
@@ -493,8 +503,8 @@ repeating `String(error())` at each boundary:
493
503
  error an `<Errored>` collected never reaches `window.onerror`; a `captureException`
494
504
  call inside `fallback` is a side effect in render that fires on every re-render of the
495
505
  fallback. Since 2.0.0-rc.9 the runtime has a seam for exactly this failure, called
496
- once per error object with where it was thrown (`ownerPath`) and where it was met
497
- (`boundaryPath`):
506
+ once per error object (on the server: once per error object, per `kind`, per request)
507
+ with where it was thrown (`ownerPath`) and where it was met (`boundaryPath`):
498
508
 
499
509
  ```ts
500
510
  import { configureClientErrors } from 'solid-js';
@@ -918,6 +928,28 @@ unless you rewind the DOM yourself:
918
928
  />
919
929
  ```
920
930
 
931
+ The opposite mistake is binding a **committed** value with no per-keystroke write —
932
+ `value={props.name} onChange={commit}`, the React "controlled by the saved value" shape.
933
+ The `value` binding re-runs whenever anything its template expression reads changes
934
+ (the prop, a store row, a `class` that shares the template), and re-writes the committed
935
+ value over text the user typed but has not committed yet. Since 2.0.0-rc.17 dev reports
936
+ `[UNCOMMITTED_INPUT_OVERWRITTEN]` with the element and the bound value
937
+ <!-- upstream:uncommitted-input-overwritten -->. Three repairs, in order of preference:
938
+
939
+ ```tsx
940
+ // 1. a draft signal written per keystroke; resets when the committed value changes
941
+ const [draft, setDraft] = createSignal(() => props.name);
942
+ <input value={draft()} onInput={(e) => setDraft(e.currentTarget.value)} onChange={() => props.onCommit(draft())} />
943
+
944
+ // 2. uncontrolled: the DOM owns the text, the prop only seeds it
945
+ <input defaultValue={props.name} onChange={(e) => props.onCommit(e.currentTarget.value)} />
946
+
947
+ // 3. explicit write-back when the field must follow an external value exactly
948
+ ```
949
+
950
+ `equals: false` on the committed signal changes nothing — the binding re-runs, not the
951
+ signal — and a `createEffect` that copies the prop into the field is rule 4 again.
952
+
921
953
  ### Context
922
954
 
923
955
  ```tsx
@@ -943,6 +975,14 @@ Pass accessors/setters/stores through context — never `value={theme()}` (a dea
943
975
  No `useMemo` for the value object, no context splitting: the object is created once and
944
976
  fine-grained updates flow through the signals inside it.
945
977
 
978
+ **`undefined` means "not provided".** `createContext<T>()` and `createContext(undefined)`
979
+ are the same default-less context, and a provider whose `value` is `undefined`
980
+ (`<ThemeContext value={maybeTheme()}>` while the memo has no value yet) counts as no
981
+ provider: `useContext` below it throws `ContextNotFoundError`, whose message points at
982
+ `createContext<T | null>(null)`. An intentionally optional payload is modelled with
983
+ `null` — `createContext<Session | null>(null)` and `value={session() ?? null}` — so the
984
+ read distinguishes "no session" from "no provider" <!-- upstream:context-undefined-unprovided -->.
985
+
946
986
  **App-wide state is the same provider, placed at the root of `App`** — not a
947
987
  module-level signal or store. Module scope is for constants and the `createContext`
948
988
  call itself:
@@ -1015,7 +1055,10 @@ function SearchField(props: { ref?: Ref<HTMLInputElement>; onInput: EventListene
1015
1055
 
1016
1056
  Ref arrays flatten recursively; each callback runs in order. No `forwardRef` needed.
1017
1057
 
1018
- **Ref callbacks run untracked and without an owner.** Their return values are
1058
+ **Ref callbacks run untracked, without an owner, and before the element is attached.**
1059
+ The node exists but `el.isConnected` is `false` and it has no layout yet — measuring,
1060
+ focusing or scrolling in a ref callback reads zeros; do that in `onSettled`, which fires
1061
+ after the tree is in the document. Their return values are
1019
1062
  ignored — do not create effects, memos, or `onCleanup` inside the callback, and
1020
1063
  do not `return () => cleanup`. Put owned setup in a **directive factory** (like
1021
1064
  `listen` above) and return only the element callback. Use this for
@@ -1138,11 +1181,16 @@ write and `flush()`: `const { setA, b, dispose } = createRoot((dispose) => { …
1138
1181
  { setA, b, dispose }; }); setA(2); flush();`. A hook that reads context runs under
1139
1182
  `renderHook(useCart, { wrapper: CartProvider })`. A routed component gets a router built in
1140
1183
  the test — `createRouter({ routes, history: memoryHistory("/orders/42") })` — because
1141
- Testing Library's `render(…, { location })` is typed but not implemented (1.0.0-beta.3). <!-- upstream:testing-library-location -->
1184
+ Testing Library's `render(…, { location })` is typed but not implemented (1.0.0-rc.2). <!-- upstream:testing-library-location -->
1142
1185
 
1143
1186
  An end-to-end test that must wait for hydration of an SSR page waits on a marker set from
1144
- `onHydrationEnd` (`solid-js`, `@experimental`): register it *after* `hydrate()`, e.g.
1145
- `onHydrationEnd(() => { document.documentElement.dataset.hydrated = ""; })`. It fires when
1187
+ `onHydrationEnd` (`solid-js`, `@experimental`), registered while hydration is running:
1188
+ after `hydrate()` in an authored entry, or — under `start: true`, where the plugin
1189
+ generates the entry and calls `hydrate()` after importing `App` — from the `App`
1190
+ component body (`if (!isServer) onHydrationEnd(() => { document.documentElement.dataset.hydrated = ""; });`;
1191
+ the server version also runs the callback, where `document` does not exist). Registered
1192
+ at `App.tsx` module scope it runs before hydration starts, in the next microtask, ahead
1193
+ of the streamed boundaries it was meant to wait for. It fires when
1146
1194
  the hydration phase ends — including when a module preload failed and the markup was left
1147
1195
  non-interactive — so it is a test-tooling marker, never a branch for what the app renders
1148
1196
  (`isHydrating()` / `onSettled` own that). Outside hydration it runs in the next microtask.
@@ -1177,11 +1225,18 @@ Always-on dev checks include misplaced reads
1177
1225
  for a first read made after an `await` in an async compute — V8 engines only), misplaced writes
1178
1226
  (`[REACTIVE_WRITE_IN_OWNED_SCOPE]` — since rc.9 also for store setters called in a
1179
1227
  component or root body <!-- upstream:store-write-owned-scope -->, `[ASYNC_STORE_SETTER]` for a store setter callback that returns
1180
- a promise <!-- upstream:async-store-setter-throws -->, `[FLUSH_IN_ACTION]`, `[SERVER_WRITE]`), and
1228
+ a promise <!-- upstream:async-store-setter-throws -->, `[FLUSH_IN_ACTION]`, `[SERVER_WRITE]`,
1229
+ and since rc.17 `[STORE_WRITE_OUTSIDE_SETTER]` for a write to the store proxy itself
1230
+ <!-- upstream:store-write-outside-setter -->), misplaced bindings
1231
+ (`[UNCOMMITTED_INPUT_OVERWRITTEN]` — a `value` / `checked` binding re-applied its
1232
+ committed value over uncommitted user input <!-- upstream:uncommitted-input-overwritten -->),
1181
1233
  `[ASYNC_OUTSIDE_LOADING_BOUNDARY]` when a tracked read has no boundary above it, and
1182
1234
  `[LOADING_ON_OUTSIDE_HOLD]` when a `<Loading on>` boundary's data is also read outside it.
1183
- Each entry carries a stable `[CODE]`, what the runtime observed, and an owner chain
1184
- (`in <App> › <Cart> › <LineItem>`); never silence a code before understanding it.
1235
+ Each entry carries a stable `[CODE]`, what the runtime observed, an owner chain
1236
+ (`in <App> › <Cart> › <LineItem>`), and — since rc.17, under `vite dev` — an
1237
+ `at src/Cart.tsx:12:5` line naming where the scope was created (the compiler's
1238
+ `sourceNames.locations`; the same `loc` sits on every artifact event)
1239
+ <!-- upstream:source-names-locations -->; never silence a code before understanding it.
1185
1240
  Since rc.9 the server render reports on the same channel with the same owner chains:
1186
1241
  `[SSR_RENDER_ERROR_CONTAINED]` (a boundary routed a render error; `ownerPath` is where
1187
1242
  it was thrown, `boundaryPath` where it was met) <!-- upstream:ssr-render-error-contained -->, `[SERVER_ERROR_SANITIZED]` (the original
@@ -1189,7 +1244,10 @@ behind a production-sanitized error, from a server function or an SSR render),
1189
1244
  `[SSR_CLIENT_CONTENT_MASKED]` (client-only content that surfaced only after a real
1190
1245
  server wait — the wait was wasted), `[SSR_BOUNDARY_WATERFALL]` (sequential waits inside
1191
1246
  one server `<Loading>` boundary), `[SSR_UNDECLARED_LIVE_SOURCE]` (an async iterable
1192
- still pumping into a document render — declare it `live()`), and
1247
+ still pumping into a document render — declare it `live()`), `[SSR_LIVE_HOLE_LATCHED]`
1248
+ (a live markup hole or attribute stopped updating: re-running it created an async scope
1249
+ — a memo over a fetch inside the hole's expression — or called a slot, so it keeps the
1250
+ markup it last sent; create async sources in the component body), and
1193
1251
  `[REVEAL_IN_RENDER_TO_STRING]`.
1194
1252
  For the repair behind any code, read the `reactivity-diagnostics` skill Solid ships
1195
1253
  in the repo (`node_modules/solid-js/skills/reactivity-diagnostics/SKILL.md`) — every
@@ -1233,7 +1291,12 @@ anonymous nodes print as `computed` / `effect` / `signal`; component labels
1233
1291
  (`<Checkout>`) are always present under `vite dev` and survive minification only with
1234
1292
  the compiler's `sourceNames` option (components, binding effects, primitives — the Vite
1235
1293
  plugin turns it on under `vite dev` and `observe: true`; `componentNames` was its rc.9 name
1236
- and is rejected now) <!-- upstream:source-names-option -->. On the
1294
+ and is rejected now) <!-- upstream:source-names-option -->. Its `locations` kind (rc.17)
1295
+ adds `file:line:col` to every name — `createComponent(Home, props, "Home", "src/App.tsx:12:5")`,
1296
+ `{ name, loc }` on primitives — which is what the `at …` line in a diagnostic and
1297
+ `event.loc` in an artifact print; the plugin turns it on under `vite dev` only, and an
1298
+ observe build opts in with `sourceNames: { locations: true }` (paths relative to the
1299
+ compiler's `root`) <!-- upstream:source-names-locations -->. On the
1237
1300
  **prod build** the import resolves to an inert same-surface twin whose `enable()` is a
1238
1301
  no-op, so the import can stay in the code. On the **observe build** it is the real
1239
1302
  engine and an unguarded `enable()` records in production — keep the `isDev` guard
@@ -1334,7 +1397,8 @@ afterEach(async () => {
1334
1397
  What the shape has to get right:
1335
1398
 
1336
1399
  - **A DOM environment** (`happy-dom` / `jsdom`) for every reactive test, `.test.ts`
1337
- included. Under vitest's `node` environment the export conditions select Solid's
1400
+ included — set `test.environment` yourself; the Vite plugin does not set it, so a
1401
+ config without one runs under Vitest's default `node`. There the export conditions select Solid's
1338
1402
  **server build**: writes are inert (`[SERVER_WRITE]`), effects never run, and the
1339
1403
  attribution tables stay empty — a green test that proves nothing. Components that only
1340
1404
  exist in the server build (a `<html>` document shell) get their own `node` project
@@ -1465,7 +1529,10 @@ export default { plugins: [solid({ ssr: true, observe: true })] };
1465
1529
  `observe: true` adds the `observe` condition to every environment (client and server,
1466
1530
  `vite build` and preview) and turns on the compiler's `sourceNames` option (components,
1467
1531
  binding effects, and primitives) so `ownerPath` / `boundaryPath` name components instead
1468
- of `computed` <!-- upstream:source-names-option -->. Without the plugin, set `resolve.conditions` (or `node --conditions=observe`
1532
+ of `computed` <!-- upstream:source-names-option -->; source *locations* stay off in that
1533
+ build (names only) unless `sourceNames: { locations: true }` is passed — they cost bundle
1534
+ bytes and expose file paths, so opt in when the monitoring tool consumes `loc`
1535
+ <!-- upstream:source-names-locations -->. Without the plugin, set `resolve.conditions` (or `node --conditions=observe`
1469
1536
  for an unbundled server) and `sourceNames` yourself. Cost in Solid's size suite: ~16.8 KB against ~15.3 KB brotli
1470
1537
  for a small client app; importing the attribution engine on top raises the cap to
1471
1538
  ~27 KB (a separate entry — an observe build that never imports it never ships it). The
@@ -1478,7 +1545,14 @@ production too (observe build; a no-op on the prod build).
1478
1545
  must run before the modules it patches load. `import "./instrument"` at the top of an
1479
1546
  entry does not achieve that in ESM (static imports are hoisted in dependency order);
1480
1547
  the plugin's `start: { instrument: "./src/instrument.ts" }` awaits that server-only
1481
- module before anything else in the server graph.
1548
+ module before anything else in the server graph — since vite-plugin 3.0.0-next.51 the
1549
+ `node` entry too: `dist/server/node.js` awaits `./server.js` (which awaits the
1550
+ instrument module) and then loads `node-runtime.js`, so deploy both files. Failures the
1551
+ handler contains itself reach the hook as `{ kind: "request", handling: "failed" }`.
1552
+ The client-side counterpart is `start: { clientSetup: "./src/client-setup.ts" }`: a
1553
+ browser module run (and awaited, top-level `await` allowed) before `hydrate` / `render`,
1554
+ for a client SDK's `init()` or `enableRichArguments()`; it conflicts with an authored
1555
+ `entry-client.tsx`, which already owns that order.
1482
1556
  - **Tracing.** The server continues an incoming W3C `traceparent` or originates one;
1483
1557
  `getTraceContext()` from `@solidjs/web` reads it during the request (in-process
1484
1558
  server-function calls included) — forward it downstream with
@@ -1662,8 +1736,11 @@ and touch. Without `preloadLinks` nothing preloads ahead of the click. The other
1662
1736
  strategies, all from `@solidjs/router` and combinable as an array: `tapPreload()`
1663
1737
  (pointerdown), `viewportPreload()` and `eagerPreload()` (code only by default, for links
1664
1738
  marked `preload="viewport"` / `preload="eager"`, or every link with `{ all: true }`).
1665
- `preload="false"` on an anchor opts that link out; `usePreloadRoute()` warms a route by
1666
- hand. A boolean `preloadLinks` (the old on/off switch) only warns in dev and preloads
1739
+ `preload="false"` on an anchor opts that link out of every strategy — the **string**:
1740
+ the types also accept `preload={false}`, but a `false` attribute is removed, so that link
1741
+ is still preloaded. `usePreloadRoute()` warms a route by hand (code only;
1742
+ `{ preloadData: true }` also runs its `preload`). A boolean `preloadLinks` (the old
1743
+ on/off switch) is a type error, and at runtime it only warns in dev and preloads
1667
1744
  nothing. <!-- upstream:preload-links-opt-in --> Intent values are `"initial"` /
1668
1745
  `"navigate"` / `"native"` / `"preload"`. One router per app.
1669
1746
 
@@ -1702,16 +1779,19 @@ the raw strings rather than throwing). Query values are strings, so coerce in th
1702
1779
  `page: v.optional(v.pipe(v.unknown(), v.transform(Number)), 1)`; a plain `v.number()`
1703
1780
  rejects every value and hands back the string, still typed `number`.
1704
1781
 
1705
- **Two different `action`s.** Core `action` from `solid-js` is a generator transaction
1706
- (optimistic writes that span an async gap). Router `action` from `@solidjs/router` is
1707
- a URL-addressable POST mutation (`useSubmissions`, `<form action={save} method="post">`).
1708
- Do not import one and use it as the other.
1782
+ **`formAction` and core `action` are different things.** Core `action` from `solid-js` is
1783
+ a generator transaction (optimistic writes that span an async gap). Router `formAction`
1784
+ from `@solidjs/router` is a URL-addressable POST mutation (`useSubmissions`,
1785
+ `<form action={save} method="post">`). The router's `action` / `Action` exports are
1786
+ deprecated aliases of `formAction` / `FormAction` since 2.0.0-next.41 — dev warns on the
1787
+ first call, and `solid2-kit check` flags the import — so a router `action` import is
1788
+ always the stale spelling, never the core primitive <!-- upstream:router-action-deprecated -->.
1709
1789
 
1710
1790
  ```tsx
1711
- import { action } from '@solidjs/router';
1791
+ import { formAction } from '@solidjs/router';
1712
1792
  import { redirect } from '@solidjs/web';
1713
1793
 
1714
- const save = action(async (form: FormData) => {
1794
+ const save = formAction(async (form: FormData) => {
1715
1795
  await saveProfile(form);
1716
1796
  return redirect('/account');
1717
1797
  }, 'update-profile');
@@ -1724,7 +1804,20 @@ const save = action(async (form: FormData) => {
1724
1804
 
1725
1805
  Only POST forms are accepted. Bind extra args with `.with(id)` (they go in the
1726
1806
  action URL). `onSubmit={(e) => { e.preventDefault(); fetch(...) }}` *runs* and
1727
- drops the no-JS form fallback. `useAction` is JS-only for the same reason.
1807
+ drops the no-JS form fallback. `useAction` is deprecated (next.41, dev warns) and has
1808
+ no replacement because it needs none: **single flight is the router's.** A mounted
1809
+ router intercepts every server-function call — from a handler, a core `action`, a memo
1810
+ — and applies the response's `revalidate` keys, `redirect()` / `reload()` and flight
1811
+ data, so outside a form you call the server function (or a core `action` around it)
1812
+ directly. The router treats **every POST server-function call as a mutation**: with no
1813
+ `revalidate` keys on the response the server re-collects the current page's reads into
1814
+ the flight and the client revalidates every query; a POST that changed nothing returns
1815
+ `respond(value, { revalidate: "" })`; reads stay exempt through `query()` (which
1816
+ switches the function to GET) or `GET(fn)`; `createRouter({ routes, singleFlight: false })` opts the app
1817
+ out <!-- upstream:router-post-mutation -->. Route params arrive **decoded**
1818
+ (`/users/%E7%8C%AB` gives `params.id === "猫"`, and `Router.paths` encodes on the way
1819
+ out), and `useInRouter()` answers whether a router is mounted — for a component shared
1820
+ between routed and unrouted trees.
1728
1821
  Optimistic rows for a router form keep the `<form>` and add no core `action`: read the
1729
1822
  list `query` with `createOptimisticStore(() => getList(), [])` and register
1730
1823
  `save.onSubmit((form) => setList((d) => { d.push({ …, pending: true }); }))` in the
@@ -1769,9 +1862,9 @@ flow — the status and value survive production sanitization, unlike a plain th
1769
1862
  `Error`. Pass 3 renders the messages inline from the recorded submission:
1770
1863
 
1771
1864
  ```tsx
1772
- import { action, useSubmissions } from '@solidjs/router';
1865
+ import { formAction, useSubmissions } from '@solidjs/router';
1773
1866
 
1774
- const submitAddress = action(saveAddress);
1867
+ const submitAddress = formAction(saveAddress);
1775
1868
  const submissions = useSubmissions(submitAddress);
1776
1869
  const issues = () => (submissions.at(-1)?.error as { issues?: AddressIssues } | undefined)?.issues ?? {};
1777
1870
 
@@ -1852,19 +1945,18 @@ revalidate: `redirect` / `reload` from `@solidjs/web` (see the server-function
1852
1945
  guides).
1853
1946
 
1854
1947
  **`redirect()` / `reload()` are signals for an integration, not navigation.** Solid
1855
- Router's `action` / `query` apply them (navigate, revalidate), and a no-JS form post
1856
- follows the 302. Any other caller — a core `action`, an event handler, a plain
1857
- `createMemo(() => listOrders())` — receives the raw `Response` as the resolved value:
1858
- no navigation happens, and the type still claims your data. (When a mounted router's
1859
- single-flight hook intercepts a direct POST call, it does navigate, but the call
1860
- resolves to `null`; a direct `GET()` read always gets the `Response`.) Either way the
1861
- caller never gets its data, so the redirect guard belongs to functions called through
1862
- the router:
1948
+ Router's `formAction` / `query` apply them (navigate, revalidate), a no-JS form post
1949
+ follows the 302, and since router 2.0.0-next.41 the mounted router's single-flight hook
1950
+ applies them on *any* server-function call made while it is mounted (the call then
1951
+ resolves to `null`). Without a mounted router a caller — a core `action`, an event
1952
+ handler, a plain `createMemo(() => listOrders())` — receives the raw `Response` as the
1953
+ resolved value: no navigation happens, and the type still claims your data. Either way
1954
+ the caller never gets its data, so the redirect guard belongs to routed apps:
1863
1955
 
1864
1956
  ```ts
1865
1957
  import { getRequestEvent, redirect, respond } from '@solidjs/web';
1866
1958
 
1867
- // called through a router action/query: the router navigates to /sign-in
1959
+ // a routed app (router formAction/query, or any call under a mounted router): it navigates to /sign-in
1868
1960
  export async function listOrders() {
1869
1961
  'use server';
1870
1962
  const userId = getRequestEvent()?.locals.userId;
@@ -1931,27 +2023,29 @@ export const fetchStory = GET(async (id: number) => {
1931
2023
  });
1932
2024
  ```
1933
2025
 
1934
- A value that changes over time is `live(GET(fn))` — **`live()` outermost**: `GET()`
1935
- chooses the read transport and `live()` wraps the call behavior around it. `live()`
1936
- does not imply `GET()`; without it the source streams over POST, which is fine when a
1937
- cacheable URL is of no use. A server component rendered into a document that reads an
1938
- *undeclared* async iterable still producing after 5s is `[SSR_UNDECLARED_LIVE_SOURCE]` in
1939
- dev — declare it `live()` (the document takes the first value and closes it; the client
1940
- connects after hydration) or bound the source.
2026
+ A value that changes over time is `live(fn)`. Since 2.0.0-rc.17 `live()` **implies
2027
+ `GET`**: a function not already declared `GET` is wrapped at runtime, so the stream has a
2028
+ cacheable URL and is never mistaken for a mutation (the router's single flight treats
2029
+ every POST call as one). `live(GET(fn))` is the same declaration spelled out — keep
2030
+ `live()` outermost when you write both <!-- upstream:live-implies-get -->. A server
2031
+ component rendered into a document that reads an *undeclared* async iterable still
2032
+ producing after 5s is `[SSR_UNDECLARED_LIVE_SOURCE]` in dev — declare it `live()` (the
2033
+ document takes the first value and closes it; the client connects after hydration) or
2034
+ bound the source. `broadcast(fn)` (experimental) is `live()` for one answer shared across
2035
+ every viewer — the server runs the generator once per argument set instead of once per
2036
+ connection.
1941
2037
 
1942
2038
  ```ts
1943
2039
  // src/data/auction.ts
1944
- import { GET, live } from '@solidjs/web/server-functions';
1945
-
1946
- export const liveAuction = live(
1947
- GET(async function* (id: string) {
1948
- 'use server';
1949
- yield await db.auctions.find(id); // current state first, from storage
1950
- for await (const _change of db.auctions.subscribe(id)) {
1951
- yield await db.auctions.find(id);
1952
- }
1953
- }),
1954
- );
2040
+ import { live } from '@solidjs/web/server-functions';
2041
+
2042
+ export const liveAuction = live(async function* (id: string) {
2043
+ 'use server';
2044
+ yield await db.auctions.find(id); // current state first, from storage
2045
+ for await (const _change of db.auctions.subscribe(id)) {
2046
+ yield await db.auctions.find(id);
2047
+ }
2048
+ });
1955
2049
 
1956
2050
  export async function saveBid(id: string, amount: number) {
1957
2051
  'use server';
@@ -2008,13 +2102,18 @@ existing Express / Fastify app, mount the `listener` that file exports — do no
2008
2102
  hand-write a Node↔web bridge (`node dist/server/server.js` starts nothing: it is the
2009
2103
  handler module). Client-only `start: true` is still static `dist/client`
2010
2104
  — there is no handler to wrap. Request middleware is the Vite option
2011
- `start: { middleware: "./src/middleware.ts" }` (web `Request` + `next`), not
2012
- Express `app.use`. Server functions that return components
2105
+ `start: { middleware: "./src/middleware.ts" }`, not Express `app.use`: its default
2106
+ export is `(event, next) => Response` (type `StartMiddleware` from
2107
+ `@solidjs/vite-plugin`), or an array of them. The request is `event.request`; `next()`
2108
+ takes no arguments — assign `event.request` first to substitute it, and set per-request
2109
+ render inputs (`event.nonce`, `event.renderMode`) before calling it. A `(request, next)`
2110
+ middleware written from older examples receives the event as `request`.
2111
+ <!-- upstream:start-middleware-event --> Server functions that return components
2013
2112
  (`serverFunctions: { components: true }`) are experimental preview — do not use
2014
2113
  them unless the project already has that flag on.
2015
2114
 
2016
- Unscripted (no-JS) POST forms go through a router action —
2017
- `const save = action(createTodo, "create-todo")` from `@solidjs/router`, then
2115
+ Unscripted (no-JS) POST forms go through a router form action —
2116
+ `const save = formAction(createTodo, "create-todo")` from `@solidjs/router`, then
2018
2117
  `<form action={save} method="post">` — not a client `preventDefault` + `fetch`, not a
2019
2118
  hand-built `/_server/` URL, and not `action={createTodo.url}`: a bare `"use server"`
2020
2119
  function keeps its declared function type, so `.url` is a type error (TS2339) even
@@ -2062,9 +2161,9 @@ how to reuse. Prefer the form on the right.
2062
2161
  | `const rest = { ...props }` | `const rest = omit(props, "label")` |
2063
2162
  | `const user = createMemo(async () => { await fetch(...); return id(); })` | read `id()` **before** `await` |
2064
2163
  | `action(async function* () { await save(); setX(v); })` | `yield save()` or `await save(); yield; setX(v)` |
2065
- | `action(async (item) => { ... })` with core `action` from `solid-js` | `action(function* (item) { ...; yield ...; ... })` — only a generator re-enters the transaction (`solid2-kit check` flags this). Router `action` from `@solidjs/router` is the one that takes `async (form) => ...` |
2164
+ | `action(async (item) => { ... })` with core `action` from `solid-js` | `action(function* (item) { ...; yield ...; ... })` — only a generator re-enters the transaction (`solid2-kit check` flags this). Router `formAction` from `@solidjs/router` is the one that takes `async (form) => ...` |
2066
2165
  | `todos()` on a store | `todos.length` / `todo.title` (property reads) |
2067
- | `todos.push(x)` / `state.count++` / `todo.done = !todo.done` on the store itself | `setTodos((draft) => { draft.push(x); })` — writes to the store proxy outside a setter are silently ignored (no error, no warning, tsc passes) |
2166
+ | `todos.push(x)` / `state.count++` / `todo.done = !todo.done` on the store itself | `setTodos((draft) => { draft.push(x); })` — writes to the store proxy outside a setter are ignored (no error, tsc passes; dev warns `[STORE_WRITE_OUTSIDE_SETTER]` since rc.17 <!-- upstream:store-write-outside-setter -->) |
2068
2167
  | `createStore({ selected: new Set<string>() })` + `draft.selected.add(id)` | `selected: {} as Record<string, true>` + `draft.selected[id] = true` / `delete draft.selected[id]`, or `draft.selected = new Set([...draft.selected, id])` — `Map` / `Set` / `Date` are stored as-is, not tracked |
2069
2168
  | `draft.todos = fresh` (wholesale fresh-tree assignment) | `reconcile(fresh, "id")`, or function-form `createStore` / `createProjection` (auto-keyed by `"id"`). Removal via `setTodos((t) => t.filter(...))` is fine — survivors keep identity |
2070
2169
  | `createEffect(() => user, (u) => { log(u.name); })` | `createEffect(() => user.name, (name) => { log(name); })` |
@@ -2079,8 +2178,12 @@ how to reuse. Prefer the form on the right.
2079
2178
  | `dangerouslySetInnerHTML={{ __html }}` | `innerHTML={html()}` (sanitized); never with JSX children |
2080
2179
  | `onClick={setCount}` | `onClick={() => setCount((c) => c + 1)}` |
2081
2180
  | `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 --> |
2181
+ | `on:click={fn}` / `use:tooltip` / `attr:title` / `classList={{ on: x() }}` on a native element | `onClick`, a ref callback from a directive factory, `title`, `class={["btn", { on: x() }]}` — the 1.x forms are compile errors since rc.17 (`prop:` remains) <!-- upstream:removed-attribute-compile-error --> |
2182
+ | `value={props.name} onChange={(e) => props.onCommit(e.currentTarget.value)}` (bound to the committed value, no per-edit write) | a draft signal `createSignal(() => props.name)` written from `onInput`, or `defaultValue={props.name}` — the binding re-applies the committed value over uncommitted typing (`[UNCOMMITTED_INPUT_OVERWRITTEN]`); `equals: false` does not help <!-- upstream:uncommitted-input-overwritten --> |
2183
+ | `useDeferredValue(query())` / `startTransition(() => setQuery(q))` for an expensive derivation | `const rows = createDeferred(() => filter(all(), query()))` — an async memo that lags but never leads its inputs; `isPending(rows)` while it catches up. Unrelated to the 1.x `createDeferred(source, { timeoutMs })` <!-- upstream:create-deferred-export --> |
2082
2184
  | `event.target.value` | `event.currentTarget.value` |
2083
2185
  | `createContext(emptyStore)` for reactive state | `createContext<Todos>()` — dummy defaults silently no-op |
2186
+ | `createContext<Session | undefined>(undefined)` + `value={session()}` for an optional payload | `createContext<Session | null>(null)` + `value={session() ?? null}` — `undefined` means *not provided*: a provider whose value is `undefined` counts as none and the read throws `ContextNotFoundError` <!-- upstream:context-undefined-unprovided --> |
2084
2187
  | `<For keyed={(t) => t.id}>{(todo) => todo.title}` | `todo().title` — key-function items are accessors |
2085
2188
  | `{list().length ? <For each={list()}> : <Empty />}` | `<For each={list()} fallback={<Empty />}>` |
2086
2189
  | `<For each={rows.slice(from, from + n)}>` | `<Repeat from={from()} count={n}>` |
@@ -2089,14 +2192,16 @@ how to reuse. Prefer the form on the right.
2089
2192
  | `const r = await api.x(); if (!r.ok) ...` result-object checks at every read | throw unusable responses; contain each region with `<Errored>` |
2090
2193
  | `GET(async (id) => { "use server"; await db.delete(id) })` | mutations stay on POST; `GET()` is for idempotent reads |
2091
2194
  | `live()` yields as an event log to append | each yield **replaces** the current answer; yield current state first, read from the state mutations write to — a reconnect calls the function again, so generator locals or per-connection memory are not the source |
2092
- | `GET(live(async function* () { ... }))` | `live(GET(async function* () { ... }))` — `live()` outermost; `live()` alone streams over POST |
2195
+ | `GET(live(async function* () { ... }))` | `live(async function* () { ... })` — `live()` implies `GET` since rc.17 (`live(GET(fn))` is the same; `live()` outermost) <!-- upstream:live-implies-get --> |
2093
2196
  | a plain `async function*` `"use server"` stream for a value that must survive a dropped connection | `live()` — a plain stream is an event sequence on one connection (`for await`, accumulate); nothing reopens it when it drops |
2094
2197
  | `yield saveBid(...)` then `refresh(auction)` on a store derived from `live()` | `yield saveBid(...); yield until(() => auction.highBid >= amount, { timeout })` — `live()` is outside revalidation/single-flight; `refresh` re-runs the derivation (closes the stream, calls the source again): a reconnect per mutation, and the old value still flashes back whenever the new first value predates the write |
2095
2198
  | `<article innerHTML={html()}>…children…</article>` | `innerHTML` **or** children, not both |
2096
2199
  | `fallback={(error) => <p>{error.message}</p>}` | `error` is an accessor: `error().message` (or `String(error())`) |
2097
- | `import { action } from "solid-js"` on a `<form>` | `import { action } from "@solidjs/router"` + `<form action={save} method="post">` (core `action` is a generator transaction, not a form URL) |
2200
+ | `import { action } from "solid-js"` on a `<form>` | `import { formAction } from "@solidjs/router"` + `<form action={save} method="post">` (core `action` is a generator transaction, not a form URL) |
2201
+ | `import { action, useAction } from "@solidjs/router"` | `formAction` (`action` / `Action` are deprecated aliases since next.41, dev warns; `solid2-kit check` flags them); `useAction` is deprecated with no replacement — call the server function directly, the mounted router applies its revalidation and redirect <!-- upstream:router-action-deprecated --> |
2202
+ | `async function markRead(id) { "use server"; … }` called from a handler, no `revalidate` anywhere | fine — the mounted router treats every POST call as a mutation and revalidates the page's queries; a POST that changed nothing returns `respond(value, { revalidate: "" })`, reads go through `query()` / `GET()`, and `createRouter({ routes, singleFlight: false })` opts out <!-- upstream:router-post-mutation --> |
2098
2203
  | `createMemo(async () => fetchUser(id()))` when `@solidjs/router` is present | `query(fetchUser, "users")` + `createMemo(() => getUser(id()))`; after mutations `revalidate(getUser.key)`, not core `refresh()` |
2099
- | `onSubmit={(e) => { e.preventDefault(); fetch(...) }}` for a router/server action | `<form action={save} method="post">` (POST only; `.with(id)` binds args into the URL). `useAction` is JS-only |
2204
+ | `onSubmit={(e) => { e.preventDefault(); fetch(...) }}` for a router/server action | `<form action={save} method="post">` (POST only; `.with(id)` binds args into the URL); outside a form, call the server function directly |
2100
2205
  | `query(saveUser, "users")` wrapping a mutation | don't wrap mutations in `query` — an undeclared server function becomes GET |
2101
2206
  | `render(<App />, root)` / Testing Library `render(<Counter />)` | `render(() => <App />, root)` — pass a function so Solid creates the root first |
2102
2207
  | `render` onto SSR HTML / `hydrate` into an empty node | `hydrate(() => <App />, root)` when HTML already exists; `render` for an empty mount |
@@ -2126,12 +2231,13 @@ how to reuse. Prefer the form on the right.
2126
2231
  | `createOptimistic` for a local edit draft | writable derivation / plain store. Optimistic is for an in-flight mutation |
2127
2232
  | `createMemo(async fn, { loadingValue })` / `{ seedLoadingValue: true }` as the default first-flight UI | `<Loading>` for first flight; those options are escape hatches (store projections use `seedLoadingValue`) |
2128
2233
  | `setSubmitted(true)` + an effect that watches it | do the work in the handler or an `action` |
2129
- | `findUser(userId)` with the id from the client as identity | `const userId = getRequestEvent()?.locals.userId; if (!userId) throw respond(null, { status: 401 });` (`throw redirect("/sign-in")` under a router `action` / `query`) |
2130
- | `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) |
2234
+ | `findUser(userId)` with the id from the client as identity | `const userId = getRequestEvent()?.locals.userId; if (!userId) throw respond(null, { status: 401 });` (`throw redirect("/sign-in")` in a routed app) |
2235
+ | `action={"/_server/" + id}` / `action={createTodo.url}` on a POST form | a router form action: `const save = formAction(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) |
2131
2236
  | `<form method="get" action={update.url}>` for a mutation | GET forms only for idempotent search; mutations are POST |
2132
2237
  | `window.location.href = ...` / `history.pushState` | `useNavigate()` or `<a href={Router.paths...}>` |
2133
2238
  | `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 --> |
2134
- | `createRouter({ routes, preloadLinks: true })` (or `false`) | `preloadLinks: intentPreload()` for hover/focus warming; omit the option for none — it takes strategies and a boolean preloads nothing <!-- upstream:preload-links-opt-in --> |
2239
+ | `createRouter({ routes, preloadLinks: true })` (or `false`) | `preloadLinks: intentPreload()` for hover/focus/touch warming; omit the option for none — it takes strategies and a boolean preloads nothing <!-- upstream:preload-links-opt-in --> |
2240
+ | `<a href={...} preload={false}>` to skip warming one link | `preload="false"` (the string) — a `false` attribute is removed, so the link is still preloaded <!-- upstream:preload-links-opt-in --> |
2135
2241
  | `new URLSearchParams(location.search)` hand-rolled query parsing | `useSearchParams()` (setter merges, no scroll); typed via a route `search` schema + `useSearchParams(Router.paths.search)` |
2136
2242
  | `Router.paths.about()` / string-concatenated URLs | static nodes are properties (`Router.paths.about`); calls bind params, then search object and hash |
2137
2243
  | `httpStatus(404)` in a click handler | call it bare in the component / error-fallback body (a scope declaration, not a mutation) |
@@ -2157,7 +2263,7 @@ how to reuse. Prefer the form on the right.
2157
2263
  | a held write with no feedback anywhere | pair it with `isPending()` / `latest()` / an optimistic value / `affects()` — otherwise dev + attribution reports `[SILENT_HOLD]` |
2158
2264
  | `refresh(getUser.key)` / `revalidate(user)` / `return refresh()` from `"use server"` | three APIs: core `refresh(source)` reruns a reactive source; router `revalidate(getUser.key)` invalidates the query cache; `return reload({ revalidate: "todos" })` asks the integration to refresh cached data |
2159
2265
  | `return redirect(...)` as the only redirect shape | guards `throw redirect("/sign-in")`; `redirect()` / `respond()` take `revalidate` too when the mutation also moves or returns a value |
2160
- | `throw redirect("/sign-in")` / `return reload(...)` in a function called directly (core `action`, handler, plain memo) | only Solid Router `action` / `query` (and no-JS form posts) apply them — a direct caller resolves to the raw `Response` (or `null` when the router's single-flight hook intercepts a POST call), typed as your data. Without the router: `throw respond(null, { status: 401 })` or return a value and navigate in the caller |
2266
+ | `throw redirect("/sign-in")` / `return reload(...)` in a function called directly (core `action`, handler, plain memo) in an app with no router | Solid Router `formAction` / `query`, no-JS form posts, and a mounted router's single-flight hook (any call, since next.41; the call resolves to `null`) apply them — with no router a direct caller resolves to the raw `Response`, typed as your data. There: `throw respond(null, { status: 401 })` or return a value and navigate in the caller |
2161
2267
  | `submission.clear()` from an effect | on dismiss/resubmit only — from an effect, errors flash and vanish |
2162
2268
  | `revalidate(liveSource)` | `live()` updates through the open stream; do not revalidate it |
2163
2269
  | `refresh(user)` without `affects` when the reload should look pending | pair `affects(user)` with `refresh(user)` — a bare `refresh` re-asks quietly |
@@ -2165,13 +2271,13 @@ how to reuse. Prefer the form on the right.
2165
2271
  | `setStore(async (draft) => { draft.x = await load(); })` | `const x = await load(); setStore((draft) => { draft.x = x; })` — the setter is a synchronous transaction; dev throws `[ASYNC_STORE_SETTER]` <!-- upstream:async-store-setter-throws --> |
2166
2272
  | `setStore((d) => { d.items = seed; })` in a component body | seed through `createStore(seed)` / `createStore(() => props.items, fallback)` — a body-level store write is `[REACTIVE_WRITE_IN_OWNED_SCOPE]` <!-- upstream:store-write-owned-scope --> |
2167
2273
  | `const saved = await api.save(); yield until(() => ...)` | `await api.save(); yield; yield until(() => ...)` — a bare `yield` re-enters the transaction before any reader is created, not only before writes |
2168
- | `fallback={(err) => { captureException(err()); return <Fallback />; }}` | `configureClientErrors({ onError })` from `solid-js` (or per root `render(fn, root, undefined, { onError })`), `configureServerErrors({ onError })` from `@solidjs/web` — once per error, with `ownerPath` / `boundaryPath` |
2274
+ | `fallback={(err) => { captureException(err()); return <Fallback />; }}` | `configureClientErrors({ onError })` from `solid-js` (or per root `render(fn, root, undefined, { onError })`), `configureServerErrors({ onError })` from `@solidjs/web` — once per error (server: per error, kind and request), with `ownerPath` / `boundaryPath` |
2169
2275
  | `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 --> |
2170
2276
  | `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 --> |
2171
2277
  | `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 |
2172
2278
  | `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 -->) |
2173
2279
  | `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 |
2174
- | `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 |
2280
+ | `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 (next.51 wraps the `node` entry too: deploy `node.js` with `server.js`). Browser-side init before `hydrate` / `render` is `start: { clientSetup: "./src/client-setup.ts" }` |
2175
2281
  | 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` |
2176
2282
  | `OBSERVE.records.subscribe("call", (e) => setStats(...))` | listeners run synchronously inside the runtime and must not write signals; collect into a plain buffer and drain from outside |
2177
2283
  | `import { storePath } from "solid-js"` | gone from `solid-js` (rc.9); write draft setters `setState((d) => { d.user.name = "Grace"; })` <!-- upstream:store-path-removed --> |
@@ -2266,9 +2372,9 @@ const save = action(async function* (form: FormData) {
2266
2372
  <button type="submit">Save</button>
2267
2373
  </form>
2268
2374
 
2269
- // CORRECT — router action is POST-addressable (only if `@solidjs/router` is in the project)
2270
- import { action } from '@solidjs/router';
2271
- const save = action(async (form: FormData) => persist(form), 'save');
2375
+ // CORRECT — router formAction is POST-addressable (only if `@solidjs/router` is in the project)
2376
+ import { formAction } from '@solidjs/router';
2377
+ const save = formAction(async (form: FormData) => persist(form), 'save');
2272
2378
  <form action={save} method="post">
2273
2379
  <button type="submit">Save</button>
2274
2380
  </form>
@@ -2313,20 +2419,8 @@ agent use the mirror, which serves every page as plain markdown:
2313
2419
  - Any page: append `.md`, e.g. `https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/reactivity/create-signal.md`
2314
2420
 
2315
2421
  A curated URL list is in [references/official-docs.md](references/official-docs.md).
2316
-
2317
- Known stale pages — this skill is verified against the installed release and wins:
2318
-
2319
- - `routing/solid-router/navigation` lists `data-pending` among the attributes the router
2320
- sets on its own; it needs `createRouter({ links: pendingLinks })`, and `aria-current`
2321
- also compares the query (the link-state paragraph under Solid Router 2 above).
2322
- <!-- upstream:docs-pending-links-stale -->
2323
- - `reference/solid-js/reactivity/create-signal` says a write to a writable derivation
2324
- wins over a same-tick recompute; a source change in the same update re-runs the
2325
- function, which receives the write as `prev`. <!-- upstream:docs-derived-write-stale -->
2326
- - `guides/performance` ("tracks 30 or more sources") and the attribution reference
2327
- (`wideDeps`, "default 30") give the old `[WIDE_SCOPE_DEPS]` threshold; the default is
2328
- 200, and a list's insert pass is exempt from the check (see
2329
- [Lists](#lists-for-child-signatures-per-keying-mode)). <!-- upstream:docs-wide-deps-stale -->
2422
+ Every diagnostic code has its own page under `reference/solid-js/diagnostics/<code-kebab>.md`
2423
+ (the console footer links it on a code's first report).
2330
2424
 
2331
2425
  **Never** consult Solid 1.x sources (docs.solidjs.com, pre-2.0 tutorials, old Stack Overflow).
2332
2426
  Solid 2.0 is a breaking rewrite; 1.x answers are wrong here. The banned-API table is in the
@@ -2372,7 +2466,7 @@ always-applied rules installed alongside this skill.
2372
2466
  loading/error branches, disable optimistic rows until ack, or refetch in
2373
2467
  `hydrate` / `onSettled`. HTTP does not enforce TypeScript — validate
2374
2468
  inside `"use server"`; do not invent tRPC. `live()` owns the client connection,
2375
- is declared `live(GET(fn))`, and a mutation on a live-read value holds with
2469
+ is declared `live(fn)` (it implies `GET`), and a mutation on a live-read value holds with
2376
2470
  `yield until(...)` for the echo instead of `refresh()`.
2377
2471
  A server bundle uses `handleRequest` from `dist/server/server.js`; client-only start
2378
2472
  is static `dist/client`. Do not delay `renderToStream` for visual order
@@ -2412,8 +2506,10 @@ always-applied rules installed alongside this skill.
2412
2506
  themselves with a per-row `<Show>` (no diagnostic catches it).
2413
2507
  - [ ] If the project has a router: `createRouter({ routes })`, not JSX `<Route>` / `<A>`.
2414
2508
  One instance, no nested `<Router>`. Navigate with `useNavigate` / `<a href>`, not
2415
- `window.location`. Router `action`/`query` come from `@solidjs/router` (POST forms + cache), not
2416
- core `action`/`refresh`. Forms: `<form action={save} method="post">`. Core `action`
2509
+ `window.location`. Router `formAction`/`query` come from `@solidjs/router` (POST forms + cache), not
2510
+ core `action`/`refresh`; router `action` is a deprecated alias of `formAction` and
2511
+ `useAction` is deprecated <!-- upstream:router-action-deprecated --> (single flight is
2512
+ the router's — call server functions directly). Forms: `<form action={save} method="post">`. Core `action`
2417
2513
  bodies are generators (`function*` / `async function*`) suspending on `yield`,
2418
2514
  never plain `async` functions. Link state is automatic attributes + CSS
2419
2515
  (`aria-current` / `data-active`, `data-pending` via `links: pendingLinks`;
@@ -122,6 +122,7 @@ verify API names against the reference pages above.
122
122
 
123
123
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/reactivity/create-signal.md
124
124
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/reactivity/create-memo.md
125
+ - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/reactivity/create-deferred.md
125
126
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/reactivity/create-effect.md
126
127
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/reactivity/create-optimistic.md
127
128
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/reactivity/flush.md
@@ -151,7 +152,11 @@ verify API names against the reference pages above.
151
152
  ## Reference: diagnostics, attribution, and observability
152
153
 
153
154
  `DEV` is dev builds only; `OBSERVE` and attribution exist on observe and dev builds;
154
- the error hooks fire on every build.
155
+ the error hooks fire on every build. Every diagnostic code has a page of its own —
156
+ `reference/solid-js/diagnostics/<code-in-kebab-case>.md` (for example
157
+ `.../diagnostics/silent-hold.md`), the page the console footer links on a code's first report.
158
+
159
+ - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/diagnostics.md
155
160
 
156
161
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/advanced/diagnostics-dev-hooks/dev.md
157
162
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/advanced/diagnostics-dev-hooks/observe.md
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "solid2-agent-kit",
3
- "version": "0.22.0",
3
+ "version": "0.23.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",