solid2-agent-kit 0.15.0 → 0.16.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,11 +16,11 @@ 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 `[files...]`): 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, `vite-plugin-solid`, old-router `<Route>`/`<A>`/`<HashRouter>`/`<FileRoutes>`, `MetaProvider`, `Context.Provider`, camelCase style keys, `key` props, `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 — `Element` from `solid-js`, DOM `JSX` types from `@solidjs/web`), 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), 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` |
23
- | Project-wiring gate | `solid2-kit doctor` | same | Config drift the source gate cannot see: React / `vite-plugin-solid` / `eslint-plugin-solid` / SolidStart deps in `package.json`, a Solid 1.x line of `solid-js` / `@solidjs/router` / `@solidjs/meta` (declared, or installed from the `latest` tag — Solid 2 releases are on `next`), `jsx` ≠ `preserve` or `jsxImportSource` ≠ `@solidjs/web` in tsconfig, Solid 1.x wiring in root config files, and stale installed guidance (kit updated but `sync` not re-run) |
23
+ | Project-wiring gate | `solid2-kit doctor` | same | Config drift the source gate cannot see: React / `vite-plugin-solid` / `eslint-plugin-solid` / SolidStart deps in `package.json`, a Solid 1.x line of `solid-js` / `@solidjs/router` / `@solidjs/meta` / `@tanstack/solid-router` (declared, or installed from the `latest` tag — Solid 2 releases are on `next`; TanStack Router's on `rc`), `jsx` ≠ `preserve` or `jsxImportSource` ≠ `@solidjs/web` in tsconfig, Solid 1.x wiring in root config files, and stale installed guidance (kit updated but `sync` not re-run) |
24
24
  | Review command | `.cursor/commands/solid2-review.md` | `.claude/commands/solid2-review.md` | `/solid2-review` — on-demand deep review: runs both gates, then walks the diff against the skill checklist targeting the semantic failure classes regexes cannot see (post-`await` reads, effect misuse, unkeyed server rows, boundary placement, context snapshots) |
25
25
 
26
26
  Managed blocks are delimited with `<!-- solid2-agent-kit:*:start/end -->` markers; everything
@@ -40,10 +40,15 @@ npx github:lightsound/solid2-agent-kit init
40
40
  npx github:lightsound/solid2-agent-kit init --cursor
41
41
  npx github:lightsound/solid2-agent-kit init --claude
42
42
 
43
- # run the mechanical pattern gate (default source dir: src; or name files)
43
+ # run the mechanical pattern gate (default source dir: src; or name files and directories)
44
44
  npx github:lightsound/solid2-agent-kit check
45
45
  npx github:lightsound/solid2-agent-kit check --dir app
46
- npx github:lightsound/solid2-agent-kit check src/App.tsx
46
+ npx github:lightsound/solid2-agent-kit check src/App.tsx src/routes
47
+
48
+ # only the files a change touched: a missing path always fails (exit 2), so
49
+ # --diff-filter=d keeps deleted files out of the list; -z / -0 keep paths with
50
+ # spaces whole, -r skips the run on an empty (or delete-only) diff
51
+ git diff --name-only -z --diff-filter=d main | xargs -0 -r npx github:lightsound/solid2-agent-kit check
47
52
 
48
53
  # check the project wiring (deps, tsconfig, root configs)
49
54
  npx github:lightsound/solid2-agent-kit doctor
@@ -405,10 +405,78 @@ function actionAsyncFindings(content) {
405
405
  }));
406
406
  }
407
407
 
408
+ // Solid 1.x signatures: `createEffect(fn)` (Solid 2 needs compute + apply),
409
+ // `createEffect(fn, initialValue)`, and `createMemo(fn, initialValue[, options])`
410
+ // (Solid 2's second memo argument is the options object). A second argument
411
+ // that is a primitive or array literal is an initial value; functions,
412
+ // identifiers, and object literals are left alone. A name this file imports
413
+ // from another library (effector's one-argument `createEffect`) or declares
414
+ // itself is not Solid's.
415
+ const INITIAL_VALUE_LITERAL = /^(?:-?\d|['"`[]|true\b|false\b|null\b|undefined\b)/;
416
+ const NOT_SOLID_CORE_MODULE = /^(?!(?:solid-js|@solidjs\/signals)$)/;
417
+
418
+ // Blank type-argument lists (`Record<string, number>`, `Map<K, () => void>`)
419
+ // so their commas do not split arguments. `<` counts only right after an
420
+ // identifier: formatted comparisons have a space before it.
421
+ function blankTypeArguments(text) {
422
+ let previous;
423
+ do {
424
+ previous = text;
425
+ text = text.replace(/(?<=[\w$])<(?:=>|[^<>=])*>/g, (typeArgs) => ' '.repeat(typeArgs.length));
426
+ } while (text !== previous);
427
+ return text;
428
+ }
429
+
430
+ function solid1SignatureFindings(content) {
431
+ const foreign = importedNames(content, NOT_SOLID_CORE_MODULE);
432
+ const matches = [];
433
+ for (const call of content.matchAll(
434
+ /(?<![.\w$])create(Effect|Memo)\s*(?:<(?:=>|[^<>]|<(?:=>|[^<>])*>)*>)?\s*\(/g,
435
+ )) {
436
+ const name = `create${call[1]}`;
437
+ if (foreign.has(name)) continue;
438
+ if (new RegExp(`\\b(?:function\\s*\\*?|const|let|var|class)\\s+${name}\\b`).test(content)) continue;
439
+ const open = call.index + call[0].length - 1;
440
+ const close = scanBalanced(content, open);
441
+ if (close === -1) continue;
442
+ const args = splitTopLevelArgs(blankTypeArguments(content.slice(open + 1, close)))
443
+ .map((arg) => arg.trim())
444
+ .filter(Boolean);
445
+ const initialValue = args.length >= 2 && INITIAL_VALUE_LITERAL.test(args[1]);
446
+ const legacy =
447
+ call[1] === 'Effect' ? args.length === 1 || initialValue : args.length >= 3 || initialValue;
448
+ if (legacy) matches.push({ index: call.index });
449
+ }
450
+ return matches;
451
+ }
452
+
453
+ // `key` is meaningless in Solid JSX except on the Solid Meta tags, where it
454
+ // names the tag's identity (`<Meta key="og-image" …>`). The enclosing tag is
455
+ // the nearest `<Name` (or `<Namespace.Name`) before the attribute.
456
+ const META_MODULE = /^@solidjs\/meta$/;
457
+
458
+ function reactKeyPropFindings(content) {
459
+ const metaTags = importedNames(content, META_MODULE);
460
+ const metaNamespaces = new Set(
461
+ [...content.matchAll(/\bimport\s+\*\s+as\s+([\w$]+)\s+from\s*['"]@solidjs\/meta['"]/g)].map((m) => m[1]),
462
+ );
463
+ const matches = [];
464
+ for (const attr of content.matchAll(/\skey=\{/g)) {
465
+ const opener = [...content.slice(0, attr.index).matchAll(/<([A-Za-z][\w$.]*)/g)].pop();
466
+ const tag = opener?.[1] ?? '';
467
+ if (metaTags.has(tag) || metaNamespaces.has(tag.split('.')[0])) continue;
468
+ matches.push({ index: attr.index });
469
+ }
470
+ return matches;
471
+ }
472
+
408
473
  // TanStack Router for Solid exports names the Next.js / Solid Router 1.x rules
409
474
  // target (`useRouter()`, `notFound()`, `<Navigate>`). A match is exempt only
410
475
  // when this file binds that exact local name from one of these modules, so a
411
- // stray Next or Router 1.x call in the same file is still reported.
476
+ // stray Next or Router 1.x call in the same file is still reported. TanStack
477
+ // Router 1.x (Solid 1) and 2.x (Solid 2) export the same names from the same
478
+ // module, so the major is invisible here; doctor's tanstack-router-version
479
+ // reads it from package.json / node_modules.
412
480
  const TANSTACK_ROUTER_MODULE = /^@tanstack\/(?:solid-router|router-core)$/;
413
481
 
414
482
  // Local names bound by `import [Default,] { a, b as c } from "<module>"`.
@@ -454,7 +522,19 @@ const CHECKS = [
454
522
  id: 'jsx-namespace-import',
455
523
  pattern: /import\s+(?:type\s+)?\{[^}]*\bJSX\b[^}]*\}\s*from\s+['"]solid-js['"]/g,
456
524
  message:
457
- 'Solid 2 does not export a `JSX` namespace from "solid-js" (TS2305). Children/return types are `Element` from "solid-js"; DOM-specific JSX types (JSX.IntrinsicElements, JSX.CSSProperties) come from "@solidjs/web".',
525
+ 'Solid 2 does not export a `JSX` namespace from "solid-js" (TS2305). Import `JSX` from "@solidjs/web" (JSX.Element, JSX.IntrinsicElements, JSX.CSSProperties), or use `Element` from "solid-js" (aliased, e.g. `type Element as SolidElement`, where the DOM Element is also used).',
526
+ },
527
+ {
528
+ id: 'solid1-effect-memo-signature',
529
+ find: solid1SignatureFindings,
530
+ message:
531
+ 'Solid 1.x signature. createEffect takes (compute, apply) — a single argument is an error in Solid 2. createMemo(fn, initialValue) is gone: the second argument is the options object; give the compute a default parameter instead (createMemo((prev = 0) => ...)).',
532
+ },
533
+ {
534
+ id: 'projection-destructure',
535
+ pattern: /\b(?:const|let|var)\s*\[[^\]]*\]\s*=\s*createProjection\b/g,
536
+ message:
537
+ 'createProjection returns the store itself, not a [store, setter] tuple — destructuring it takes the first row. Write `const rows = createProjection(...)`.',
458
538
  },
459
539
  {
460
540
  // A two-phase createEffect whose apply is nothing but one (optionally
@@ -508,8 +588,9 @@ const CHECKS = [
508
588
  },
509
589
  {
510
590
  id: 'react-key-prop',
511
- pattern: /\skey=\{/g,
512
- message: 'React `key` prop has no meaning in Solid. Row identity belongs on <For keyed={...}>.',
591
+ find: reactKeyPropFindings,
592
+ message:
593
+ 'React `key` prop has no meaning in Solid. Row identity belongs on <For keyed={...}>. (Only the @solidjs/meta tags take `key`, as the tag identity.)',
513
594
  },
514
595
  {
515
596
  // `{todos().map((t) => <Row />)}` renders then recreates every row.
@@ -574,8 +655,14 @@ const CHECKS = [
574
655
  message: 'Solid 1.x Vite plugin. Import `solid` from "@solidjs/vite-plugin".',
575
656
  },
576
657
  {
658
+ // Valueless attributes count too (`<input use:autofocus />`): followed by
659
+ // `/>`, `>`, or another attribute, unlike an unspaced object key
660
+ // (`{ on:true }`, `{ on:x = false }`, `{ attr:v as string }`); `as="font"`
661
+ // and SVG `in="…"` are still attributes. `prop:` is still a Solid 2
662
+ // namespace and is not listed.
577
663
  id: 'solid1-jsx-namespace',
578
- pattern: /\b(?:use|on|oncapture|attr|bool):[A-Za-z][\w-]*=/g,
664
+ pattern:
665
+ /(?<=\s)(?:use|on|oncapture|attr|bool):[A-Za-z][\w-]*(?==|\s*\/?>|\s+(?!(?:as|satisfies|in|instanceof)\b(?!\s*=))[A-Za-z_${])/g,
579
666
  message:
580
667
  'Solid 1.x JSX namespace. Use ref callbacks (and directive factories), camelCase event props, and standard attributes.',
581
668
  },
@@ -787,10 +874,15 @@ function check() {
787
874
  files = [];
788
875
  scanned = pathArgs;
789
876
  walked = false;
877
+ // A missing path is always an error: a deleted file, a misspelled file, and
878
+ // a renamed source directory look the same here, and only the list's
879
+ // producer knows which it was.
790
880
  for (const arg of pathArgs) {
791
881
  const path = resolve(target, arg);
792
882
  if (!existsSync(path)) {
793
- console.error(`solid2-kit check — path not found: ${path}`);
883
+ console.error(
884
+ `solid2-kit check — path not found: ${path} (drop deleted files from a changed-file list with \`git diff --name-only --diff-filter=d\`)`,
885
+ );
794
886
  process.exit(2);
795
887
  }
796
888
  if (statSync(path).isDirectory()) {
@@ -1129,8 +1221,9 @@ const BANNED_DEPS = {
1129
1221
  };
1130
1222
 
1131
1223
  // Packages whose npm `latest` dist-tag is still the Solid 1.x line (solid-js
1132
- // 1.9, @solidjs/router 1.0, @solidjs/meta 0.29): a bare `pnpm add <name>`
1133
- // installs it. The Solid 2 releases are published under `next`.
1224
+ // 1.9, @solidjs/router 1.0, @solidjs/meta 0.29, @tanstack/solid-router 1.x):
1225
+ // a bare `pnpm add <name>` installs it. The Solid 2 releases are published
1226
+ // under `next` (TanStack: `rc`, 2.0.0-rc.x).
1134
1227
  const SOLID2_LINE_PACKAGES = [
1135
1228
  { name: 'solid-js', id: 'solid-js-version', solid1: /^[\s^~=v]*[01](?:\.|$)/, fix: 'Install solid-js@next (^2).' },
1136
1229
  {
@@ -1145,6 +1238,12 @@ const SOLID2_LINE_PACKAGES = [
1145
1238
  solid1: /^[\s^~=v]*0(?:\.|$)/,
1146
1239
  fix: 'Meta 0.x is built for Solid 1 (<MetaProvider> required); install @solidjs/meta@next (1.x).',
1147
1240
  },
1241
+ {
1242
+ name: '@tanstack/solid-router',
1243
+ id: 'tanstack-router-version',
1244
+ solid1: /^[\s^~=v]*[01](?:\.|$)/,
1245
+ fix: 'TanStack Router 1.x has a solid-js ^1.9 peer; install @tanstack/solid-router@rc (2.x, solid-js 2 peer).',
1246
+ },
1148
1247
  ];
1149
1248
 
1150
1249
  function installedVersion(target, name) {
@@ -47,7 +47,8 @@ Steps:
47
47
  data), and POST forms built on a bare function's `.url` (TS2339) instead of a
48
48
  router `action`;
49
49
  error reporting done as a side effect inside `fallback` instead of
50
- `configureClientErrors` / `configureServerErrors` / `render(..., { onError })`;
50
+ `configureClientErrors` / `configureServerErrors` / `render(fn, root, undefined, { onError })`
51
+ (options are `render`'s 4th argument — a 3rd-argument object is `init`);
51
52
  - store setter callbacks that `await` (the draft closes when the callback returns —
52
53
  `[ASYNC_STORE_SETTER]`), store setters called at component-body top level, and
53
54
  `latest()` used as a null-safe read of an unsettled source;
@@ -5,4 +5,4 @@
5
5
  - After editing TSX, run the `solid2-kit check` mechanical gate — it fails on React/Solid 1.x tokens and props destructuring. Use the project's `lint:solid` script if one exists; otherwise `npx solid2-agent-kit check` (with the kit as a devDependency) or `npx github:lightsound/solid2-agent-kit check`. An edit-time hook may also run this gate automatically after each file edit — treat its findings as errors to fix immediately, not warnings. Do not install `eslint-plugin-solid` (built for Solid 1.x; misreads Solid 2 idioms).
6
6
  - After editing `package.json`, `tsconfig*.json`, or root config files (Vite, ESLint), run `solid2-kit doctor` — it fails on React/Solid 1.x wiring: `react`/`vite-plugin-solid`/`eslint-plugin-solid`/SolidStart dependencies, 1.x `solid-js` ranges, and `jsx`/`jsxImportSource` values other than `preserve`/`@solidjs/web`.
7
7
  - The kit's guidance and gate files (`.cursor/rules/solid-2.mdc`, the `solid-2` skill directories, hook entries, managed blocks in this file and `CLAUDE.md`) are guardrails, not obstacles. Never edit, weaken, bypass, or delete them to make a failure go away — fix the flagged code. If a guardrail seems wrong, stop and tell the user.
8
- - Solid 2.0 shipped recently and breaks with 1.x — never trust pre-2.0 Solid docs, tutorials, or training knowledge. Verify APIs against https://v2-rebuild--solid-docs-v2.netlify.app/llms.txt (markdown mirror of https://v2.solidjs.com/, which blocks non-browser fetchers). If the project has `@solidjs/router`, server functions, or `@solidjs/meta`, use those official pages (see the skill's official-docs index) — not React Router, Next.js, SolidStart 1.x, or Solid Router 0.x/1.x JSX `<Route>`/`<A>`. Core `action` from `solid-js` is a generator transaction; router `action`/`query` from `@solidjs/router` are POST forms and a read cache. Core `refresh(source)`, router `revalidate(key)`, and server-function `return reload(...)` are three different APIs. Pass the accessor to `isPending(user)`, not `isPending(user())`. Pass async values as values (`user={user()}`) — JSX props are lazy; types stay `User`, not `Promise<User>`. `<Loading>` wraps the read, not the memo. Nested fetches run in parallel (components do not suspend). `isPending` is a per-expression question (`isPending(() => props.story)` / `isPending(selectedId)`), not a global spinner; `latest(selectedId)` is opt-in so highlight and content stay consistent by default. `<Errored>` heals when the source succeeds. Pass a function to `render`/`hydrate`: `render(() => <App />, root)`. Component teardown is `onSettled`, not `onCleanup` — return the cleanup; `onCleanup`, memo/effect creation, and `flush()` throw inside the `onSettled` callback in dev. Effect `apply` functions use block bodies (`(t) => { document.title = t; }`): any return value other than a cleanup function halts reactivity. Stores are written only through the setter's draft — writes to the store itself are silently ignored, and `Map`/`Set`/`Date` inside a store are not tracked. Shared state, app-wide included, is created in a provider at the root of `App` (`createContext<T>()` + `useX()`), never a module-level signal/store (one instance serves every SSR request). Errors thrown out of an `action` never reach `<Errored>` — catch them in the action or `.catch` the call. One router instance; in-app navigation is `useNavigate` / `<a href>`, not `window.location`. Types: `solid-js` exports no `JSX` namespace — children/return types are `Element` from `solid-js`; DOM `JSX` types come from `@solidjs/web`. Reset-a-signal-on-change is a writable derivation (`createSignal(() => { source(); return initial; })`), never an effect calling the setter. Client mode mounts with `render()` into an empty body — no hydration mismatch exists, so never defer initial values to post-mount React-style (that can overwrite persisted state). Client → server is additive (same setters + `action` + server functions): do not rewrite App with loading/error branches, disable optimistic rows until ack, invent tRPC, or refetch in `hydrate`/`onSettled`. The graph continues across the wire (`live()` owns the client connection; `<Reveal>` not a delayed stream). A server bundle uses `handleRequest` / Fetchable `fetch`; client-only start is static `dist/client`. One browser-only value takes `ssrSource` on the memo/signal/derived store (`client`/`hybrid`), not a `clientOnly` component split. Router link state is automatic (`aria-current`/`data-active`/`data-pending` + CSS; `useLinkState`/`useIsRouting` in JSX); search params via `useSearchParams`; router forms validate on the server (`throw respond({issues},{400})`) with inline errors from `useSubmissions`. Confirmations on another channel — fire-and-forget sends, and mutations whose result is read through `live()` — wait with `yield until(predicate, {timeout})`, not `refresh()`. Declare `live(GET(fn))` (`live()` outermost). Debounce at the handler; inputs feeding a request bind `latest(query)`. Dev diagnostics are errors: `[STRICT_READ_UNTRACKED]` means a read outside a tracking scope, `[ASYNC_STORE_SETTER]` means a store setter callback awaited (the draft is a synchronous transaction — await first, then write), and a held write needs `isPending`/`latest`/optimistic/`affects` feedback or dev + attribution reports `[SILENT_HOLD]`. Error monitoring is `configureClientErrors` / `configureServerErrors` (or `render(..., { onError })`), 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()`. Observability is on by default in development: whenever a change touches stores, lists, async, actions, or effects, keep the dev console free of `[CODE]` findings and run an attribution session (`/__solid/diagnostics` `begin` → interact → `whyDidRun` / `costs` → `end` with `@solidjs/diagnostics` installed, otherwise `isDev`-guarded `attribution.enable({ log: false })` + `why()` / `costs()`) before finishing. `vite dev` is always the dev build; `solid({ observe: true })` is a production opt-in.
8
+ - Solid 2.0 shipped recently and breaks with 1.x — never trust pre-2.0 Solid docs, tutorials, or training knowledge. Verify APIs against https://v2-rebuild--solid-docs-v2.netlify.app/llms.txt (markdown mirror of https://v2.solidjs.com/, which blocks non-browser fetchers). If the project has `@solidjs/router`, server functions, or `@solidjs/meta`, use those official pages (see the skill's official-docs index) — not React Router, Next.js, SolidStart 1.x, or Solid Router 0.x/1.x JSX `<Route>`/`<A>`. Core `action` from `solid-js` is a generator transaction; router `action`/`query` from `@solidjs/router` are POST forms and a read cache. Core `refresh(source)`, router `revalidate(key)`, and server-function `return reload(...)` are three different APIs. Pass the accessor to `isPending(user)`, not `isPending(user())`. Pass async values as values (`user={user()}`) — JSX props are lazy; types stay `User`, not `Promise<User>`. `<Loading>` wraps the read, not the memo. Nested fetches run in parallel (components do not suspend). `isPending` is a per-expression question (`isPending(() => props.story)` / `isPending(selectedId)`), not a global spinner; `latest(selectedId)` is opt-in so highlight and content stay consistent by default. `<Errored>` heals when the source succeeds. Pass a function to `render`/`hydrate`: `render(() => <App />, root)`. Component teardown is `onSettled`, not `onCleanup` — return the cleanup; `onCleanup`, memo/effect creation, and `flush()` throw inside the `onSettled` callback in dev. 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). Reset-a-signal-on-change is a writable derivation (`createSignal(() => { source(); return initial; })`), never an effect calling the setter. Client mode mounts with `render()` into an empty body — no hydration mismatch exists, so never defer initial values to post-mount React-style (that can overwrite persisted state). Client → server is additive (same setters + `action` + server functions): do not rewrite App with loading/error branches, disable optimistic rows until ack, invent tRPC, or refetch in `hydrate`/`onSettled`. The graph continues across the wire (`live()` owns the client connection; `<Reveal>` not a delayed stream). A server bundle uses `handleRequest` / Fetchable `fetch`; client-only start is static `dist/client`. One browser-only value takes `ssrSource` on the memo/signal/derived store (`client`/`hybrid`), not a `clientOnly` component split. Router link state is automatic (`aria-current`/`data-active`/`data-pending` + CSS; `useLinkState`/`useIsRouting` in JSX); search params via `useSearchParams`; router forms validate on the server (`throw respond({issues},{400})`) with inline errors from `useSubmissions`. Confirmations on another channel — fire-and-forget sends, and mutations whose result is read through `live()` — wait with `yield until(predicate, {timeout})`, not `refresh()`. Declare `live(GET(fn))` (`live()` outermost). Debounce at the handler; inputs feeding a request bind `latest(query)`. Dev diagnostics are errors: `[STRICT_READ_UNTRACKED]` means a read outside a tracking scope, `[ASYNC_STORE_SETTER]` means a store setter callback awaited (the draft is a synchronous transaction — await first, then write), 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()`. 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.
@@ -42,7 +42,8 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
42
42
  before a flush, use the setter updater (`setCount((c) => c + 1)`); `setCount(count() + 1)`
43
43
  reads the last *committed* value and drops the other staged writes. To store a *function*
44
44
  in a signal, wrap it: `setHandler(() => myHandler)` — `setHandler(myHandler)` treats it
45
- as an updater.
45
+ as an updater. The initial value too: `createSignal(() => myHandler)` —
46
+ `createSignal(myHandler)` is the derived form (it calls `myHandler` and stores the result).
46
47
  5. `class`, not `className`; `for`, not `htmlFor`. `class` accepts strings, conditional
47
48
  objects, and nested arrays: `class={["btn", { active: selected() }]}`. Put *conditional*
48
49
  names in an object — `class={`btn ${on() ? "on" : ""}`}`, `clsx(...)`, and
@@ -60,13 +61,15 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
60
61
  that writes the event object; wrap it: `onClick={() => setCount((c) => c + 1)}`.
61
62
  Debounce at the handler (`onInput={debounce(...)}`), never as an effect copying
62
63
  one signal into another.
63
- 7. **Lists use `<For>`**, never `{list().map(...)}` in reactive JSX and never `key` props.
64
+ 7. **Lists use `<For>`**, never `{list().map(...)}` in reactive JSX and never `key` props (only `@solidjs/meta` tags take `key`, as the tag's identity).
64
65
  `{todos().map((t) => <Row todo={t} />)}` *renders*, then recreates every row on each
65
66
  update. Row identity: default = item reference; `keyed={(item) => item.id}` = key function
66
67
  (child receives item as an accessor); `keyed={false}` = positional. **Rows from
67
68
  server/refetched data (fresh object references on every update — fetch results,
68
69
  subscription payloads) must use a key function on a stable id**, or every update
69
- recreates every row (dev + attribution flags the rebuilds as `[UNSTABLE_LIST_IDENTITY]`). Reference keying is for local arrays whose item identities are
70
+ recreates every row (dev + attribution flags the rebuilds as `[UNSTABLE_LIST_IDENTITY]`).
71
+ Usually better still: load them into a function-form store (rule 20), whose proxies keep
72
+ identity per id, and keep default keying. Reference keying is for local arrays whose item identities are
70
73
  stable (e.g. store rows). Match the child signature to the mode: default item is the
71
74
  **raw** value (`todo.title`); `keyed={(t) => t.id}` / `keyed={false}` pass an
72
75
  **accessor** (`todo().title`). Mixing those still “runs” (you render a function, or
@@ -83,8 +86,8 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
83
86
  block body — make it a derivation. Register an effect's cleanup by returning it
84
87
  from `apply` — `onCleanup` there never runs (`[NO_OWNER_CLEANUP]`).
85
88
  Single-argument `createEffect(fn)` is an error in Solid 2. Do not substitute
86
- `createTrackedEffect` for that — it is an advanced one-callback form that cannot nest
87
- primitives. Skip the initial run with `{ defer: true }`. Most React `useEffect` code should not become an effect at all — see the skill.
89
+ `createTrackedEffect` for that — it is `@deprecated` since 2.0.0-rc.9 (kept only for
90
+ 1.x migration). Skip the initial run with `{ defer: true }`. Most React `useEffect` code should not become an effect at all — see the skill.
88
91
  9. **Stores update by mutating a draft**: `setStore(draft => { draft.user.name = "Ada" })`.
89
92
  Never rebuild with spreads — that destroys property-level subscriptions. The return
90
93
  form is for shapes where mutation is awkward — most commonly removal
@@ -93,6 +96,9 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
93
96
  `reconcile` or the derived form (dev + attribution flags the store-level waste as
94
97
  `[IMMUTABLE_UPDATE_IN_STORE]`). Stores are
95
98
  **property reads** (`todos.length`, `todo.title`), not accessors — never `todos()`.
99
+ The setter takes only a function: `setState("a", v)` and the 1.x shallow merge
100
+ `setState({ a: 1 })` throw `fn is not a function`, and an object *returned* from the
101
+ callback replaces the whole store (keys it omits are deleted).
96
102
  The store itself is read-only: `todos.push(x)`, `state.count++`, or
97
103
  `todo.done = !todo.done` outside a setter is **silently ignored** (no throw, no
98
104
  warning, and TypeScript accepts it) — every write goes through the setter's draft.
@@ -116,13 +122,17 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
116
122
  `<Child user={user()} />` then `{props.user.name}` under the child's
117
123
  `<Loading>` is the colorless form. `const u = user()` at the parent *is* a
118
124
  read and throws/snapshots there. Pass the memo itself only when the child
119
- must `refresh()` that source — `isPending(() => props.user)` works on the
125
+ must `refresh()` that source — type that prop `SourceAccessor<User>` (`Accessor<User>`
126
+ is TS2345 at `refresh`) — `isPending(() => props.user)` works on the
120
127
  value. Read every reactive input **before the first
121
128
  `await`** — post-`await` reads do not subscribe, and in production the computation can
122
129
  sit pending with no retry. No `useEffect` + `setState` fetching, no `createResource`.
123
130
  `<Loading>` wraps the data slot, not page chrome. After first paint it keeps content
124
131
  during refetch (`isPending` for the indicator). Use `on={id()}` (the *value*, not the
125
- accessor) only when that identity change should show the fallback again. Do not start `fetch` (or any request) at component-body top
132
+ accessor) only when that identity change should show the fallback again. `on` compares
133
+ with `!==` (several keys: one string such as `` `${a()}/${b()}` ``, never a fresh array)
134
+ and takes effect only when no reader outside the boundary waits on the same write
135
+ (that is how one slow panel stops holding the page). Do not start `fetch` (or any request) at component-body top
126
136
  level — that runs once at mount and is not a reactive source. Do not `try/catch` `NotReadyError` around a read, and do not
127
137
  use `loadingValue` / `seedLoadingValue` as the default first-flight UI — those skip
128
138
  `<Loading>`. `{latest(() => x())}` is a preview, not the visible answer, and not a
@@ -196,8 +206,9 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
196
206
  JSX (`<Show>`, ternary, `<Switch>`/`<Match>`). Early returns on genuinely non-reactive
197
207
  values (build-time config, missing env) are fine. `<Switch>` is first-wins: later
198
208
  truthy `<Match>`es are skipped, not “all matching branches”.
199
- 16. Prefer `textContent` for text-only content. Use `innerHTML` only for trusted or
200
- sanitized markup — never interpolate user input into it, never React's
209
+ 16. Plain text is ordinary JSX children (`<p>{message()}</p>`); `textContent` is for an
210
+ element whose entire content is one text value, never together with children. Use
211
+ `innerHTML` only for trusted or sanitized markup — never interpolate user input into it, never React's
201
212
  `dangerouslySetInnerHTML`, and never combine `innerHTML` with JSX children (they fight
202
213
  over the same contents).
203
214
  17. **Never narrow a reactive read with a non-null assertion** (`error()!.message`,
@@ -224,7 +235,8 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
224
235
  overlay; otherwise a toast), and `.catch` the call in the handler for the rest.
225
236
  Error monitoring is a hook, not a side effect in the fallback: the boundary-caught
226
237
  failures no global handler sees reach `configureClientErrors({ onError })` from
227
- `solid-js` (or `render(..., { onError })` per root) and `configureServerErrors({ onError })`
238
+ `solid-js` (or per root: `render(() => <App />, root, undefined, { onError })` — options
239
+ are the 4th argument, the 3rd is `init` — and `hydrate(fn, root, { onError })`) and `configureServerErrors({ onError })`
228
240
  from `@solidjs/web` — do not call `captureException` inside `<Errored fallback>`.
229
241
  The server hook's return value replaces the sanitized error on the wire: return
230
242
  nothing or a reference id, never the error itself (message/stack/secrets would
@@ -245,6 +257,9 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
245
257
  On a derived store, `refresh(store)` re-runs the derivation (`fn`); refreshing a
246
258
  plain signal accessor is a no-op. `refresh` returns a promise for the target's next
247
259
  settled state (`await refresh(user)`, or `yield refresh(x)` inside an action).
260
+ Polling is `onSettled(() => { const t = setInterval(() => refresh(stats), 30_000); return () => clearInterval(t); })`.
261
+ Two components that each create the same memo make two requests (no automatic
262
+ dedup) — create it once and pass it down or through context (router: `query`).
248
263
  These are three different APIs: core `refresh(source)` reruns a reactive source;
249
264
  router `revalidate(getUser.key)` invalidates the query cache; server-function
250
265
  `return reload({ revalidate: "todos" })` asks the integration to refresh cached
@@ -255,13 +270,19 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
255
270
  apply `reconcile(fresh, "id")(draft.slot)` inside a draft setter. Plain wholesale
256
271
  assignment (`draft.todos = fresh`) renders correctly but notifies every subscriber
257
272
  under the path and destroys row identity — same class of problem as unkeyed `<For>`.
273
+ `createProjection` returns the store itself, not a tuple (`const [rows] = createProjection(…)`
274
+ type-checks and yields row 0). `reconcile` onto a single-object store whose `id`
275
+ changes throws in dev (`Cannot reconcile states with different identity`) — switch records
276
+ with the function form (`createStore(() => getUser(id()), seed)`) or a returned object.
258
277
  21. **Solid inputs do not rewind the DOM.** `value={v()}` writes only when `v` changes; a
259
278
  React-style controlled input that ignores invalid keystrokes silently breaks. To reject
260
279
  input, write back explicitly:
261
280
  `onInput={(e) => { if (valid(e.currentTarget.value)) setV(e.currentTarget.value); else e.currentTarget.value = v(); }}`.
262
281
  22. Miscellaneous defaults: SSR-stable element ids come from `createUniqueId()` (never
263
282
  `Math.random()` or hardcoded duplicates); pass store data to `structuredClone` /
264
- `postMessage` / logs via `snapshot(store)` (proxies fail or leak reactivity); render
283
+ `postMessage` / logs via `snapshot(store)` (proxies fail or leak reactivity) — a
284
+ view, not a copy: unchanged subtrees are the store's own objects, so mutating it (a
285
+ library's `arr.sort()`) silently changes the store; copy with `structuredClone(snapshot(store))`; render
265
286
  modals/tooltips/overlays through `<Portal>` from `@solidjs/web`; mutations whose writes
266
287
  cross an async gap default to `action` + `createOptimistic`/`createOptimisticStore` —
267
288
  the sync mutation *is* the prediction (an overlay, discarded on settle). Durable data
@@ -292,7 +313,12 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
292
313
  after touching `package.json` / tsconfig / root configs; it fails on React and Solid 1.x
293
314
  wiring. Server vs browser:
294
315
  `isServer` / `isDev` from `@solidjs/web`, or `clientOnly(() => import("./Widget"))` for
295
- browser-only components — never `typeof window` as the SSR boundary. Default
316
+ browser-only components — never `typeof window` as the SSR boundary. `isServer` picks
317
+ code paths, never markup: a JSX branch on it (or on `Date.now()` / `Math.random()`) is
318
+ a hydration mismatch. On the server the component body and memo/effect *computes*
319
+ run; effect `apply`, `onSettled`, and handlers do not. A `clientOnly` placeholder is
320
+ its `fallback` prop (`<Chart fallback={<Skeleton />} />`, rendered by SSR), not a
321
+ `<Loading>` around it; named exports take `{ export: "Chart" }`. Default
296
322
  `clientOnly` starts loading at declaration; `{ lazy: true }` defers until first
297
323
  render. `NoHydration` / `Hydration` split hydration *ownership*; they do not choose
298
324
  visible content — `clientOnly` is for components that must never run on the server.
@@ -318,7 +344,8 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
318
344
  fallback body), not event-time mutations. `JSON.stringify(store)`
319
345
  / `structuredClone(store)` without `snapshot(store)` can throw or leak proxies. Async SSR
320
346
  without `<Loading>` *works* but blocks the HTML shell until every read settles — wrap
321
- reads, or stream with `renderToStream`. `pipe` / `pipeTo` / `readable` each consume
347
+ reads, or stream with `renderToStream`. A read that must be in the first HTML (SEO,
348
+ link previews) takes `{ deferStream: true }` on its memo; keep the boundary. `pipe` / `pipeTo` / `readable` each consume
322
349
  a stream render — use exactly one. `createRoot` is for tests, libraries, and
323
350
  non-render entry points; inside a component let `render` / the component owner
324
351
  own the scope. Async reads inside `<Portal>` start on the client — hoist the
@@ -369,9 +396,10 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
369
396
  `<Title>` / `<Meta>` from `@solidjs/meta` with **no** `MetaProvider`. A static
370
397
  `<title>` in the document shell is the fallback; do not hardcode other tags
371
398
  (`<meta name="description">`) that Solid Meta should manage.
372
- Return `respond(value, { status, headers })` from a `"use server"` function, not
373
- `Response.json` — a raw `Response` is HTTP-handler control flow; scripted
374
- callers should receive the value. Production sanitizes every thrown value that
399
+ Success metadata is `return respond(value, { status: 201, headers })` from a
400
+ `"use server"` function, not `Response.json` — a raw `Response` is HTTP-handler control
401
+ flow; scripted callers should receive the value. A *returned* `respond()` resolves the
402
+ call whatever its status, so a failure is `throw respond(body, { status: 400 })`. Production sanitizes every thrown value that
375
403
  crosses the wire — server-function throws **and** SSR render errors (an `<Errored>`
376
404
  fallback printing `err().message` shows `"Internal Server Error"` in a production
377
405
  server render) — so intentional client-facing failures are `markSafeError(...)` or
@@ -399,14 +427,18 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
399
427
  display). Do not write subscribe/unsubscribe in the component around `live()`.
400
428
  When the app has a server bundle (`ssr` or server functions), production is
401
429
  `handleRequest(request)` from the built `dist/server/server.js` (or its
402
- Fetchable `fetch`); platform Vite plugins adopt that handler (Node: the
403
- template `server.js`). Client-only start mode is static `dist/client` —
430
+ Fetchable `fetch`); platform Vite plugins adopt that handler. Node: set
431
+ `start: { node: true }` and run `node dist/server/node.js` (serves `dist/client`,
432
+ reads `PORT` / `HOST`; its exported `listener` mounts in an existing Express /
433
+ Fastify server) — `server.js` is only the handler module. Client-only start mode is static `dist/client` —
404
434
  there is no handler to wrap. Request middleware is
405
435
  `start: { middleware: "./src/middleware.ts" }`, not Express `app.use`. Do not return
406
436
  components from `"use server"` unless the project already enabled the
407
- experimental `serverFunctions.components` flag. JSON-encodable server-function arguments
408
- only, unless `enableRichArguments()` was called once in the client entry
409
- (`Date` / `Map` / `Set` throw without it). Declare live reads as `live(GET(fn))` —
437
+ experimental `serverFunctions.components` flag. Server-function arguments are JSON:
438
+ send `Date` / `Map` / `Set` as ISO strings / arrays (they throw otherwise; one `File` /
439
+ `Blob` / `FormData` argument travels natively). `enableRichArguments()` from
440
+ `@solidjs/web/server-functions/rich-args` (at `src/App.tsx` module scope) lifts that,
441
+ but in rc.9 importing it fails `vite build` (`"./client" is not exported`). Declare live reads as `live(GET(fn))` —
410
442
  `live()` outermost. A plain `async function*` server function is an event stream on
411
443
  one connection (no reconnect when it drops); `live()` is one value that changes over
412
444
  time and reconnects. `live()` connection state is
@@ -437,7 +469,7 @@ directory (default `src/`).
437
469
  | `solid/event-handlers` | still valid | rule 6 |
438
470
  | `solid/jsx-no-undef`, `jsx-no-duplicate-props` | covered by TypeScript | project typecheck |
439
471
  | `solid/prefer-classlist` | obsolete — `classList` removed | `solid2-kit check` bans `classList` |
440
- | `solid/no-unknown-namespaces` | obsolete — `on:`/`use:`/`attr:` removed in 2.0 | banned-API table + `solid2-kit check` |
472
+ | `solid/no-unknown-namespaces` | obsolete — `on:`/`use:`/`attr:` removed in 2.0 (`prop:` remains) | banned-API table + `solid2-kit check` |
441
473
  | `solid/no-proxy-apis` | obsolete — Solid 2 requires Proxy | not carried |
442
474
 
443
475
  ## Banned Solid 1.x APIs (Solid 2 replacements)
@@ -445,15 +477,15 @@ directory (default `src/`).
445
477
  | Never write (Solid 1.x) | Write instead (Solid 2) |
446
478
  |---|---|
447
479
  | `import ... from "solid-js/store"` or `"solid-js/web"` | stores/`merge`/`omit` from `"solid-js"`; `render`/`hydrate`/`Portal`/`Dynamic` from `"@solidjs/web"` |
448
- | `import type { JSX } from "solid-js"` / `JSX.Element` as the children type | `solid-js` exports no `JSX` namespace: children/returns are `Element` from `"solid-js"`; DOM-specific `JSX` types (`JSX.IntrinsicElements`, `JSX.CSSProperties`) from `"@solidjs/web"` |
480
+ | `import type { JSX } from "solid-js"` | `solid-js` exports no `JSX` namespace: markup types are `JSX.Element` from `"@solidjs/web"` (with `JSX.IntrinsicElements`, `JSX.CSSProperties`) or `Element` from `"solid-js"` — import the latter as `type Element as SolidElement` where the DOM `Element` is also used, or it shadows it |
449
481
  | `createResource` | async `createMemo` + `<Loading>`/`<Errored>`; `refresh()`, `latest()`, `isPending()` |
450
- | `createEffect(fn)` (one arg), `on(...)` | `createEffect(compute, apply)`; deps belong in `compute` |
482
+ | `createEffect(fn)` (one arg), `on(...)`, `createTrackedEffect` (deprecated in 2.0) | `createEffect(compute, apply)`; deps belong in `compute`; one-time DOM work is `onSettled` |
451
483
  | `onMount` | `onSettled(() => { ...; return cleanup })` — return the cleanup; `onCleanup`, memo/effect creation, and `flush()` throw inside the callback |
452
484
  | `batch(...)` | delete it — writes auto-batch; `flush()` only to observe synchronously |
453
485
  | `<Suspense>`, `<ErrorBoundary>`, `<SuspenseList>` | `<Loading>`, `<Errored>` (fallback gets an error *accessor*), `<Reveal>` |
454
486
  | `<Index>` | `<For keyed={false}>` |
455
487
  | `<Ctx.Provider value={...}>` | `<Ctx value={...}>` |
456
- | `setState("a", "b", value)` path setters, `produce(...)` | draft setter: `setState(draft => { ... })` |
488
+ | `setState("a", "b", value)` path setters, `setState({ a })` merges, `produce(...)` | draft setter: `setState(draft => { ... })` (the others throw `fn is not a function`) |
457
489
  | `mergeProps` / `splitProps` / `unwrap` | `merge` / `omit` / `snapshot` |
458
490
  | `classList={...}` | `class` object/array form |
459
491
  | `createMutable`, `modifyMutable` | `createStore` + draft setters (direct writes to the store are silently ignored) |
@@ -466,19 +498,19 @@ directory (default `src/`).
466
498
  | `clearDelegatedEvents` | delete — delegated listeners are scoped to each render root |
467
499
  | `vite-plugin-solid` / `jsxImportSource: "solid-js"` | `@solidjs/vite-plugin` / `"jsxImportSource": "@solidjs/web"` |
468
500
  | JSX `<Route>` / `<A>` / `<HashRouter>` / `<Navigate>` / `<FileRoutes>` | `createRouter({ routes })`, `fileRoutes(pageRoutes)` (`fileRoutes` from `@solidjs/router/fs`, `pageRoutes` from `virtual:file-routes`), plain `<a href={Router.paths...}>`, `hashHistory()`; link state via automatic `aria-current` / `data-active` / `data-pending` + CSS (`useLinkState` in JSX) |
501
+ | `useCurrentMatches`, `MemoryRouter` / `createMemoryHistory`, `redirect` / `reload` from `@solidjs/router`, `indexArray` | `useRouteMatches`, `createRouter({ routes, history: memoryHistory("/x") })`, `redirect` / `reload` from `@solidjs/web`, `mapArray(list, fn, { keyed: false })` |
469
502
  | `createAsync` / `createAsyncStore` / `useSubmission` / router `json()` / `cache()` | `createMemo(() => getUser(id()))`, `useSubmissions`, `respond()` from `@solidjs/web`, `query` |
470
503
  | `import ... from "@solidjs/start"` / `vinxi` / `h3` / `"use client"` | `GET` from `@solidjs/web/server-functions`; Solid has no `"use client"` |
471
504
  | `render(<App />, root)` | `render(() => <App />, root)` (same for `hydrate` / `renderToString` / `renderToStream`) |
472
505
  | `<MetaProvider>` | no provider — render `<Title>` / `<Meta>` / `<Link>` from `@solidjs/meta` anywhere |
473
- | `use:directive`, `on:`/`oncapture:`, `attr:`/`bool:`, `/*@once*/` | `ref` callbacks, camelCase event props, standard attributes, keep values reactive |
506
+ | `use:directive`, `on:`/`oncapture:`, `attr:`/`bool:`, `/*@once*/` | `ref` callbacks, camelCase event props, standard attributes, keep values reactive; `prop:` remains for DOM properties (`prop:indeterminate`, objects/arrays on custom elements — plain props are attributes and stringify) |
474
507
  | `resource.loading` / `resource.error` | `<Loading>` boundary / `<Errored>` boundary |
475
508
 
476
509
  When unsure about any API, verify against the official Solid 2.0 docs — fetchable URLs are
477
510
  listed in `references/official-docs.md` next to the `solid-2` skill. Do not guess from
478
511
  Solid 1.x or React memory.
479
512
 
480
- `createTrackedEffect` and `createRenderEffect` exist in Solid 2 but are not defaults:
481
- use two-phase `createEffect` unless a single tracked callback is required. `storePath`
513
+ `createRenderEffect` exists in Solid 2 but is not a default. `storePath`
482
514
  (the 1.x path-setter migration helper) is no longer exported from `solid-js` as of
483
515
  2.0.0-rc.9 — convert path setters to draft setters instead of importing it from
484
516
  `@solidjs/signals`.
@@ -73,9 +73,9 @@ Most React `useEffect` code should NOT become `createEffect`:
73
73
  | Overlay / modal | `<Portal>` — hoist async reads *above* the portal (reads inside start on the client) |
74
74
  | Async value used several layers down | Create the memo high; pass `value={memo()}` through intermediates (they do not wait); put `<Loading>` around the leaf read |
75
75
  | Nested child with its own fetch | Leave it nested — it runs in parallel. Sequential only when the second call needs the first response (`fetchAuthor(story().authorId)`) |
76
- | Browser-only widget (charts, maps, `window`) | `clientOnly(() => import("./Chart"))` from `@solidjs/web` (`{ lazy: true }` defers the import until first render) |
76
+ | Browser-only widget (charts, maps, `window`) | `clientOnly(() => import("./Chart"))` from `@solidjs/web` (`{ lazy: true }` defers the import until first render; `{ export: "Chart" }` for a named export). Placeholder: `<Chart fallback={<Skeleton />} />` — SSR renders the fallback; a `<Loading>` around it leaves the server HTML empty |
77
77
  | Browser-only *value* (`localStorage`, viewport size) | `createMemo(..., { ssrSource: "client" })` — per-value policy at the data source, no component split (`"hybrid"` when server data mixes with client signals) |
78
- | Server vs browser branch | `isServer` / `isDev` from `@solidjs/web` (build-time constants), not `typeof window` |
78
+ | Server vs browser code path | `isServer` / `isDev` from `@solidjs/web` (build-time constants), not `typeof window` — never to choose markup (`{isServer ? <A /> : <B />}` is a hydration mismatch; use `clientOnly` / `ssrSource`) |
79
79
  | SSR-stable `id` / `for` / `aria-*` pairing | `createUniqueId()` |
80
80
 
81
81
  ## Canonical patterns
@@ -195,7 +195,10 @@ subscribers under the path re-run and row identity is lost. In order of preferen
195
195
  `setState((draft) => { reconcile(fresh, 'id')(draft.todos); })`. Positional data
196
196
  (fixed-shape dashboards): `reconcile(next, null)`.
197
197
 
198
- Plain non-reactive copy for logging/serialization/`structuredClone`: `snapshot(store)`.
198
+ Plain non-reactive view for logging/serialization: `snapshot(store)`. It is not a copy —
199
+ subtrees with no pending changes are the store's own objects, so mutating the snapshot
200
+ (or letting a library `sort()` it) changes the store with no notification. Edit
201
+ `structuredClone(snapshot(store))` instead.
199
202
  Subscribe an effect compute to every nested change: `deep(store)`.
200
203
 
201
204
  ### Children: pass through, or resolve with `children()`
@@ -500,7 +503,10 @@ registers this hook must load before the server graph — put it in the plugin's
500
503
  `start: { instrument: "./src/instrument.ts" }`, not at the top of an entry (see
501
504
  [Production observability](#production-observability-the-observe-build)).
502
505
 
503
- `render` / `hydrate` accept a per-root `onError` that wins over the ambient hook.
506
+ `render` / `hydrate` accept a per-root `onError` that wins over the ambient hook —
507
+ `render(() => <App />, root, undefined, { onError })` (options are the 4th argument; a
508
+ 3rd-argument object is taken as `init` and the hook is silently not installed) and
509
+ `hydrate(() => <App />, root, { onError })`.
504
510
  Uncaught errors (`REACTIVITY_HALTED`) go to the platform's `reportError`, which every
505
511
  monitor already listens on — do not report them twice. `renderToStream` /
506
512
  `renderToString`'s `onError` option *is* the per-render server hook: it now hears every
@@ -1029,6 +1035,14 @@ test('increments on click', () => {
1029
1035
  });
1030
1036
  ```
1031
1037
 
1038
+ Primitive tests write **outside** the root callback — a write inside `createRoot((dispose)
1039
+ => { …; setA(2); })` is `[REACTIVE_WRITE_IN_OWNED_SCOPE]` in dev. Return the handles, then
1040
+ write and `flush()`: `const { setA, b, dispose } = createRoot((dispose) => { …; return
1041
+ { setA, b, dispose }; }); setA(2); flush();`. A hook that reads context runs under
1042
+ `renderHook(useCart, { wrapper: CartProvider })`. A routed component gets a router built in
1043
+ the test — `createRouter({ routes, history: memoryHistory("/orders/42") })` — because
1044
+ Testing Library's `render(…, { location })` is typed but not implemented (1.0.0-beta.3).
1045
+
1032
1046
  `jsxImportSource` for tests and app code is `"@solidjs/web"`, not `"solid-js"`.
1033
1047
  Vite plugin is `@solidjs/vite-plugin`, not `vite-plugin-solid`.
1034
1048
 
@@ -1287,22 +1301,28 @@ Solid Router 0.x/1.x stand-ins.** Patterns below; fetch the matching pages from
1287
1301
  ```
1288
1302
 
1289
1303
  `solid-js` does **not** export a `JSX` namespace — `import type { JSX } from "solid-js"`
1290
- is a type error (TS2305), and `JSX.Element` as the children type is a React reflex.
1291
- Children/return types are the renderer-neutral `Element` from `solid-js`
1304
+ is a type error (TS2305). Markup types are `JSX.Element` from `@solidjs/web` (the
1305
+ docs' choice; it also admits DOM nodes) or the renderer-neutral `Element` from `solid-js`
1292
1306
  (`children?: Element`), alongside `Component` / `ParentProps` / `Accessor` / `Setter`.
1293
- DOM-specific `JSX` types (`JSX.IntrinsicElements`, `JSX.CSSProperties`) and
1294
- `ComponentProps` come from `@solidjs/web`. Vite: `import solid from "@solidjs/vite-plugin"`
1307
+ Import the latter as `import type { Element as SolidElement } from "solid-js"` in any file
1308
+ that also uses DOM elements — unaliased, it shadows the DOM `Element`
1309
+ (`el.getBoundingClientRect()` becomes TS2339). DOM-specific `JSX` types
1310
+ (`JSX.IntrinsicElements`, `JSX.CSSProperties`) and `ComponentProps` come from
1311
+ `@solidjs/web`. Vite: `import solid from "@solidjs/vite-plugin"`
1295
1312
  with `start: true` / `ssr: true` / `serverFunctions: true` only when the
1296
1313
  project already uses start mode. Tiers are `bare` → `basic` (router) →
1297
1314
  `fullstack` (SSR + server functions); do not jump a tier without cause.
1298
- Under `start: true` the plugin owns entries, the document shell, and the build —
1299
- write `src/App.tsx`, not SolidStart-style `entry-client.tsx` / `entry-server.tsx`
1300
- or a hand-written `index.html`.
1315
+ Under `start: true` the plugin owns entries and the build — write `src/App.tsx`, not
1316
+ SolidStart-style `entry-client.tsx` / `entry-server.tsx` (authored ones replace the
1317
+ generated entries) or a hand-written `index.html`. The document shell (`<html lang>`,
1318
+ favicon, fallback `<title>`) is an optional `src/Document.tsx` default export that renders
1319
+ `<HydrationScript />` in `<head>` and `<body>{props.children}</body>`; `App` never renders
1320
+ `<html>`.
1301
1321
 
1302
1322
  ### Solid Router 2 — `createRouter`, not JSX `<Route>`
1303
1323
 
1304
1324
  ```tsx
1305
- import { lazy } from 'solid-js';
1325
+ import { Loading, lazy } from 'solid-js';
1306
1326
  import { createRouter } from '@solidjs/router';
1307
1327
 
1308
1328
  export const Router = createRouter({
@@ -1320,7 +1340,9 @@ export default function App() {
1320
1340
  {(props) => (
1321
1341
  <>
1322
1342
  <a href={Router.paths}>Home</a>
1323
- <main>{props.children}</main>
1343
+ <Loading fallback={<p>Loading…</p>}>
1344
+ <main>{props.children}</main>
1345
+ </Loading>
1324
1346
  </>
1325
1347
  )}
1326
1348
  </Router>
@@ -1328,7 +1350,9 @@ export default function App() {
1328
1350
  }
1329
1351
  ```
1330
1352
 
1331
- Create the instance at **module scope**. Nested layouts are `children` arrays
1353
+ The `<Loading>` covers the first load of the lazy pages (and lets SSR stream the shell);
1354
+ later navigations hold the current page while the next one loads (links get
1355
+ `data-pending`). Create the instance at **module scope**. Nested layouts are `children` arrays
1332
1356
  on the route objects, not nested `<Route>` / nested routers. Solid Router does
1333
1357
  **not** support nested `<Router>` instances — compose one route tree (or a lazy
1334
1358
  `children` thunk). Navigate with
@@ -1410,7 +1434,9 @@ setSearch] = useSearchParams()` — the setter merges and navigates without scro
1410
1434
  `""` / `undefined` / `null` removes a key. For typed values, put a synchronous
1411
1435
  Standard Schema on the route's `search` field and pass the path node:
1412
1436
  `useSearchParams(Router.paths.search)` (async schemas throw; a failing schema leaves
1413
- the raw strings rather than throwing).
1437
+ the raw strings rather than throwing). Query values are strings, so coerce in the schema —
1438
+ `page: v.optional(v.pipe(v.unknown(), v.transform(Number)), 1)`; a plain `v.number()`
1439
+ rejects every value and hands back the string, still typed `number`.
1414
1440
 
1415
1441
  **Two different `action`s.** Core `action` from `solid-js` is a generator transaction
1416
1442
  (optimistic writes that span an async gap). Router `action` from `@solidjs/router` is
@@ -1435,6 +1461,14 @@ const save = action(async (form: FormData) => {
1435
1461
  Only POST forms are accepted. Bind extra args with `.with(id)` (they go in the
1436
1462
  action URL). `onSubmit={(e) => { e.preventDefault(); fetch(...) }}` *runs* and
1437
1463
  drops the no-JS form fallback. `useAction` is JS-only for the same reason.
1464
+ Optimistic rows for a router form keep the `<form>` and add no core `action`: read the
1465
+ list `query` with `createOptimisticStore(() => getList(), [])` and register
1466
+ `save.onSubmit((form) => setList((d) => { d.push({ …, pending: true }); }))` in the
1467
+ component — the hook runs inside the action's transaction, so the row is an overlay,
1468
+ replaced on settle by the revalidated `query` (a plain server-function read is not
1469
+ revalidated, so the row just disappears).
1470
+ `useSubmissions` keeps only completions with a result or error; observe void and
1471
+ redirect completions with `save.onSettled(hook)`.
1438
1472
 
1439
1473
  Validated forms come in three passes sharing one HTML shape. Pass 1 (above) posts
1440
1474
  uncontrolled inputs with browser `required` / `pattern` — it works with no JavaScript.
@@ -1534,7 +1568,15 @@ A function-level server function cannot close over component locals — pass
1534
1568
  values as arguments. Treat every argument as untrusted — TypeScript does not
1535
1569
  cross the HTTP boundary, so do not invent tRPC / RPC type-gen;
1536
1570
  validate inside the function (a parse in the function body is that
1537
- validation, not an RPC schema across the wire). These functions *are* the RPC — do not add an
1571
+ validation, not an RPC schema across the wire). Checks belong in the body: HTTP dispatch
1572
+ calls a function-level `"use server"` body directly, so a HOF around it (`withAuth(fn)`,
1573
+ `validated(schema, fn)`) runs for in-process calls and is skipped for `curl` — an auth
1574
+ bypass. A wrapper inside a file-level `"use server"` module (`export const x =
1575
+ withAccount(async …)`) is what gets registered and does run. Arguments are capped at 1 MiB
1576
+ by default (413 beyond; the host's `bodySizeLimit`) — upload large files to a signed URL.
1577
+ Never hand a server function point-free to a library callback (`mutationFn: renameUser`,
1578
+ `onClick={deleteTodo}`): every argument the callback receives (a context object, the
1579
+ event) is sent over the wire — wrap it in an arrow that passes only the data. These functions *are* the RPC — do not add an
1538
1580
  API-route file (or a Next `route.ts`) just to wrap `db.todos.insert`. API
1539
1581
  routes are for real HTTP endpoints (webhooks, third-party POST), not for the
1540
1582
  app's own RPC. Read trusted identity
@@ -1589,11 +1631,17 @@ on the server-function wire **and**, since 2.0.0-rc.9, on every SSR road a failu
1589
1631
  takes (an `<Errored>` fallback rendered on the server, a rejected async source
1590
1632
  serialized into the stream, a `<Loading>` fragment's rejection), so `err().message`
1591
1633
  in a fallback reads the generic message in a production server render. Use
1592
- `markSafeError` or `respond(..., { status })` for intentional client-facing
1593
- failures. Do not `return Response.json(...)` from `"use server"` — that is
1594
- HTTP-handler control flow; `respond(value, { status })` is what a scripted
1595
- caller unwraps. JSON-encodable arguments only unless `enableRichArguments()`
1596
- was called once in the client entry (`Date` / `Map` / `Set` throw without it).
1634
+ `throw markSafeError(...)` or `throw respond(..., { status })` for intentional
1635
+ client-facing failures — a *returned* `respond()` resolves the call whatever its
1636
+ status, so `return respond({ error }, { status: 400 })` reads as success to the UI,
1637
+ `useSubmissions`, and the optimistic overlay. Do not `return Response.json(...)` from
1638
+ `"use server"` — that is HTTP-handler control flow; `return respond(value, { status: 201 })`
1639
+ is success metadata a scripted caller unwraps. Arguments are JSON: send `Date` / `Map` /
1640
+ `Set` as ISO strings / arrays (they throw otherwise; one `File` / `Blob` / `FormData`
1641
+ argument travels natively). `enableRichArguments()` from
1642
+ `@solidjs/web/server-functions/rich-args` (at `src/App.tsx` module scope) lifts that, but
1643
+ in rc.9 importing it fails `vite build` (`"./client" is not exported` — the dev server
1644
+ works, so the break shows only at build).
1597
1645
  `GET()` is only for idempotent reads (URLs leak into logs/history);
1598
1646
  import it from `@solidjs/web/server-functions`, never `@solidjs/start`.
1599
1647
  Reads are function calls too: `GET(async (id) => { "use server"; ... })` is invoked
@@ -1686,9 +1734,11 @@ When the app has a server bundle (`ssr` or server functions), production is
1686
1734
  `handleRequest(request)` from the built `dist/server/server.js` (or that
1687
1735
  module's Fetchable default `fetch`) — not an import from `solid-js`. Cloudflare /
1688
1736
  Netlify / Nitro Vite plugins adopt that handler — do not write a Solid adapter,
1689
- a custom Worker, or a Netlify Function. For Node, use the official template
1690
- `server.js` (web `Request` in, stream the response); do not invent an
1691
- Express/Solid bridge. Client-only `start: true` is still static `dist/client`
1737
+ a custom Worker, or a Netlify Function. For Node, set `start: { node: true }` and run
1738
+ `node dist/server/node.js` (serves `dist/client`, reads `PORT` / `HOST`); inside an
1739
+ existing Express / Fastify app, mount the `listener` that file exports — do not
1740
+ hand-write a Node↔web bridge (`node dist/server/server.js` starts nothing: it is the
1741
+ handler module). Client-only `start: true` is still static `dist/client`
1692
1742
  — there is no handler to wrap. Request middleware is the Vite option
1693
1743
  `start: { middleware: "./src/middleware.ts" }` (web `Request` + `next`), not
1694
1744
  Express `app.use`. Server functions that return components
@@ -1807,7 +1857,7 @@ how to reuse. Prefer the form on the right.
1807
1857
  | `createOptimistic` for a local edit draft | writable derivation / plain store. Optimistic is for an in-flight mutation |
1808
1858
  | `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`) |
1809
1859
  | `setSubmitted(true)` + an effect that watches it | do the work in the handler or an `action` |
1810
- | `findUser(userId)` with the id from the client as identity | `getRequestEvent()!.locals.userId` |
1860
+ | `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`) |
1811
1861
  | `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) |
1812
1862
  | `<form method="get" action={update.url}>` for a mutation | GET forms only for idempotent search; mutations are POST |
1813
1863
  | `window.location.href = ...` / `history.pushState` | `useNavigate()` or `<a href={Router.paths...}>` |
@@ -1843,7 +1893,7 @@ how to reuse. Prefer the form on the right.
1843
1893
  | `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]` |
1844
1894
  | `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]` |
1845
1895
  | `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 |
1846
- | `fallback={(err) => { captureException(err()); return <Fallback />; }}` | `configureClientErrors({ onError })` from `solid-js` (or `render(..., { onError })`), `configureServerErrors({ onError })` from `@solidjs/web` — once per error, with `ownerPath` / `boundaryPath` |
1896
+ | `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` |
1847
1897
  | `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())` |
1848
1898
  | `attribution.why(total)` / `attribution.format(e)` / `attribution.costs()` | named imports from `solid-js/attribution`: `why(total)`, `formatRerun(e)`, `costs()`, `feedback()`, `subscriptions(scope)` (rc.9) |
1849
1899
  | `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 |
@@ -1866,8 +1916,8 @@ how to reuse. Prefer the form on the right.
1866
1916
  | `fetch('/api')` at component-body top level | `createMemo(async () => { ... fetch ... })` under `<Loading>` — a top-level fetch runs once at mount and is not a reactive source |
1867
1917
  | `<input type="checkbox" value={on()} />` | `checked={on()}` for the toggle; `value="string"` only when grouping radios or listing submitted values |
1868
1918
  | `createMemo(() => { void save(); return x(); })` / an effect that calls an action | invoke actions from handlers, not from memos or effects |
1869
- | `createSignal(props.value)` / `createStore(props.items)` | `createSignal(() => props.value)` / `createStore(() => props.items, fallback)` — a bare `props.x` at setup is a snapshot |
1870
- | `createRenderEffect` / `createTrackedEffect` as the default effect | two-phase `createEffect(compute, apply)` |
1919
+ | `createSignal(props.value)` / `createStore(props.items)` | `createSignal(() => props.value)` / `createStore(() => props.items, fallback)` — a bare `props.x` at setup is a snapshot. That form resets on every prop change; an intentional one-time read (an `initialCount` prop) is `createSignal(untrack(() => props.initialCount))` |
1920
+ | `createRenderEffect` / `createTrackedEffect` as the default effect | two-phase `createEffect(compute, apply)`; `createTrackedEffect` is `@deprecated` (rc.9) |
1871
1921
  | `createEffect(() => source(), () => setError(null))` to reset a signal | `createSignal(() => { source(); return null; })` — the writable derivation resets on change; a sole-setter apply is rule 4 |
1872
1922
  | a "server-safe" initial value adopted after mount (React hydration-mismatch fix) | client mode mounts with `render()` into an empty body — no hydration, no mismatch; read `localStorage` / `matchMedia` directly at signal creation |
1873
1923
  | rewrite `App.tsx` with loading/error branches, or snapshot/restore, when the list moves to the server | same `setTodos`; wrap mutations in `action`; swap in function-form `createOptimisticStore(() => api.list(), [])` + a server-functions file. The overlay is discarded; the confirmed store is the truth |
@@ -1878,7 +1928,7 @@ how to reuse. Prefer the form on the right.
1878
1928
  | router/server mutation, then `fetch` / `onSettled` / `hydrate` to refresh the page | `reload` / `revalidate` / single-flight (the mutation response can seed destination preloads). In-flight Promises serialize as Promises; `live()` continues from HTML |
1879
1929
  | delay `renderToStream` so HTML arrives in visual order | stream as soon as ready; `<Reveal collapsed>` controls when content *appears* |
1880
1930
  | subscribe/unsubscribe in the component around `live()` | `createMemo(() => stockPrice(props.symbol))` — hoist that memo to share one connection. `onstatus` is on the iterable the `live()` call returns, not on the memo. Until the first yield, `<Loading>`; later yields are updates |
1881
- | a Solid adapter / custom Worker / Express bridge, or `entry-client.tsx` under `start: true` | With a server bundle: `handleRequest` from `dist/server/server.js` / Fetchable `fetch`; platform plugins adopt it. Node: template `server.js`. Client-only start: static `dist/client`. Middleware is `start: { middleware: "..." }`, not Express `app.use` |
1931
+ | a Solid adapter / custom Worker / Express bridge, or `entry-client.tsx` under `start: true` | With a server bundle: `handleRequest` from `dist/server/server.js` / Fetchable `fetch`; platform plugins adopt it. Node: `start: { node: true }` → `node dist/server/node.js` (its `listener` for an existing Express / Fastify app). Client-only start: static `dist/client`. Middleware is `start: { middleware: "..." }`, not Express `app.use` |
1882
1932
  | `createMemo(async () => (await props.story).author)` | `createMemo(() => props.story.author)` — a derivation over an async value becomes async; no await, no Promise type |
1883
1933
  | return a component from `"use server"` / flip `serverFunctions.components` | experimental preview — do not enable unless the project already has that flag |
1884
1934
 
@@ -2017,7 +2067,8 @@ always-applied rules installed alongside this skill.
2017
2067
  bind `latest(query)`; failures throw to `<Errored>`, never `{ success: false }`
2018
2068
  result objects checked at every read.
2019
2069
  - [ ] Lists via `<For>` (server/refetched rows keyed by stable id), conditionals via
2020
- ternary/`<Show>`; no `{list().map(...)}` in JSX; no `key` props; no `value()!`.
2070
+ ternary/`<Show>`; no `{list().map(...)}` in JSX; no `key` props (except
2071
+ `@solidjs/meta` tags); no `value()!`.
2021
2072
  - [ ] Effects are two-phase and only at imperative boundaries; apply does not read stores.
2022
2073
  - [ ] Async reads sit under `<Loading>` (the data slot, not chrome); errors under `<Errored>`.
2023
2074
  Reactive inputs of an async memo are read before the first `await`. No hand-rolled
@@ -2028,7 +2079,7 @@ always-applied rules installed alongside this skill.
2028
2079
  failures never reach `<Errored>` (an escaping error reverts the overlay and
2029
2080
  rejects the call): catch them in the action/projection, and `.catch` the call
2030
2081
  in the handler. Error monitoring goes through `configureClientErrors` /
2031
- `configureServerErrors` (or `render(..., { onError })`), never a side effect in
2082
+ `configureServerErrors` (or `render(fn, root, undefined, { onError })`), never a side effect in
2032
2083
  the fallback. Core `refresh(source)`, router `revalidate(key)`, and `return reload(...)` are
2033
2084
  different APIs — do not mix them. Client → server is additive (same
2034
2085
  setters + `action` + server functions): do not rewrite App with
@@ -2046,8 +2097,8 @@ always-applied rules installed alongside this skill.
2046
2097
  - [ ] Rest props via `omit`, not `{ ...props }`. Conditional `class` via objects, not string concat.
2047
2098
  - [ ] Input filters write back to the DOM on reject (inputs do not rewind).
2048
2099
  - [ ] No Solid 1.x imports or APIs (`solid-js/store`, `createResource`, `onMount`, `Suspense`, ...).
2049
- No `JSX` imported from `solid-js` — children/return types are `Element` from
2050
- `solid-js`; DOM `JSX` types come from `@solidjs/web`.
2100
+ No `JSX` imported from `solid-js` — markup types are `JSX.Element` from
2101
+ `@solidjs/web` or `Element` from `solid-js` (aliased next to DOM code).
2051
2102
  - [ ] Inspect children with `children()`; code-split with `lazy` + `<Loading>`; reactive
2052
2103
  component choice with `dynamic()`. No `React.lazy`, no effects inside ref callbacks.
2053
2104
  - [ ] Browser-only code uses `isServer` / `clientOnly` (components) / `ssrSource`
@@ -14,6 +14,32 @@ exist when a project has routing, server functions, or document head tags.
14
14
  It is not every Advanced reference page. When an API is missing here, look it up
15
15
  in `llms.txt` rather than guessing from Solid 1.x or React.
16
16
 
17
+ ## Read before using
18
+
19
+ The skill does not cover these; fetch the pages when the task needs them.
20
+
21
+ - Filesystem routing (`fileRoutes({ types: true })` from `filesystem-routing/vite`, `[id]` / `[[p]]` / `[...rest]` / `(group)/` names, `defineFileRoute`): https://v2-rebuild--solid-docs-v2.netlify.app/reference/filesystem-routing.md, https://v2-rebuild--solid-docs-v2.netlify.app/reference/filesystem-routing/vite.md, https://v2-rebuild--solid-docs-v2.netlify.app/reference/filesystem-routing/conventions.md, https://v2-rebuild--solid-docs-v2.netlify.app/reference/filesystem-routing/manifest.md, https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-router/filesystem.md
22
+ - A route array in its own file (`defineRoutes`, `RouteProps<typeof Router.paths.x>`, `matchFilters`): https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-router/router-factory.md, https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-router/routes-and-paths.md
23
+ - Layout routes, pathless routes, where router hooks may run: https://v2-rebuild--solid-docs-v2.netlify.app/routing/solid-router/nested-routes.md
24
+ - Auth guards and redirect-after-sign-in (`next` validation): https://v2-rebuild--solid-docs-v2.netlify.app/guides/protected-routes.md
25
+ - Unsaved-changes guard (`useBeforeLeave`): https://v2-rebuild--solid-docs-v2.netlify.app/routing/solid-router/navigation.md, https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-router/navigation.md
26
+ - Row selection without `createSelector` (id-keyed projection): https://v2-rebuild--solid-docs-v2.netlify.app/guides/lists.md, https://v2-rebuild--solid-docs-v2.netlify.app/guides/performance.md
27
+ - Infinite scroll, polling, sharing one request: https://v2-rebuild--solid-docs-v2.netlify.app/guides/data-fetching-patterns.md
28
+ - Writing a custom primitive (called under an owner, `MaybeAccessor`, `getOwner` / `runWithOwner` / `isDisposed` after async): https://v2-rebuild--solid-docs-v2.netlify.app/guides/custom-primitives.md, https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/advanced/owner-introspection/get-owner.md, https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/advanced/owner-introspection/run-with-owner.md, https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/advanced/owner-introspection/is-disposed.md
29
+ - Wrapping a non-Solid library, push feeds with a reactive argument, web components (`prop:`, event names): https://v2-rebuild--solid-docs-v2.netlify.app/guides/integrate-non-solid-code.md
30
+ - Typed env (`env.ts` schema, `VITE_` client keys), `import "server-only"`, what compiles `"use server"`: https://v2-rebuild--solid-docs-v2.netlify.app/building-apps/environment.md, https://v2-rebuild--solid-docs-v2.netlify.app/reference/vite-plugin-solid/modules.md, https://v2-rebuild--solid-docs-v2.netlify.app/reference/vite-plugin-solid/server-functions.md
31
+ - Sessions and cookies (`parseCookieHeader` / `serializeCookie`, `RequestEventLocals` typing, when a cookie write commits): https://v2-rebuild--solid-docs-v2.netlify.app/building-apps/sessions-and-auth.md, https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-web/request-response/cookies.md
32
+ - Middleware chains and API routes / webhooks (`createAPIHandler`, `httpMethods: true`): https://v2-rebuild--solid-docs-v2.netlify.app/building-apps/middleware-and-api-routes.md, https://v2-rebuild--solid-docs-v2.netlify.app/reference/filesystem-routing/api.md
33
+ - Auth headers for every call, per-call `signal` / `keepalive` (`configureServerFunctionsClient`, `invoke`): https://v2-rebuild--solid-docs-v2.netlify.app/building-apps/server-functions/metadata-and-transport.md, https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-web/server-functions/configure-client.md, https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-web/server-functions/invoke.md
34
+ - Server-function limits (`bodySizeLimit`, `maxArguments`): https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-web/server-functions/host-configuration.md
35
+ - Static prerendering (`prerender-crawler/vite`, `prerendered()`), choosing client / SSR / prerender: https://v2-rebuild--solid-docs-v2.netlify.app/guides/choose-a-rendering-mode.md
36
+ - SSR-safe code (what runs on the server, hydration mismatches): https://v2-rebuild--solid-docs-v2.netlify.app/guides/ssr-safe-code.md
37
+ - Type helpers (`VoidProps`, `FlowProps`, `ComponentProps<"button">`, `JSX.EventHandler`, generic `<T,>` components): https://v2-rebuild--solid-docs-v2.netlify.app/guides/typescript.md, https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/types/component-types.md, https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/types/reactive-types.md, https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/types/jsx-types.md
38
+ - Head tag identity (OG `property`, `key`, layout `<Head>` groups): https://v2-rebuild--solid-docs-v2.netlify.app/building-apps/head-and-metadata.md
39
+ - Server-side tests (`createRequestEvent`, `provideRequestEvent` from `@solidjs/web/storage`, a Node Vitest project): https://v2-rebuild--solid-docs-v2.netlify.app/guides/testing.md, https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-web/request-response/provide-request-event.md
40
+ - Migrating 1.x data fetching (`createResource`, `resource.latest`): https://v2-rebuild--solid-docs-v2.netlify.app/migration/data-fetching-from-solid-1.md
41
+ - Terms used across the docs: https://v2-rebuild--solid-docs-v2.netlify.app/glossary.md; the mental model: https://v2-rebuild--solid-docs-v2.netlify.app/guides/thinking-in-solid.md
42
+
17
43
  ## Getting started
18
44
 
19
45
  - https://v2-rebuild--solid-docs-v2.netlify.app/getting-started/quick-start.md
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "solid2-agent-kit",
3
- "version": "0.15.0",
3
+ "version": "0.16.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",