solid2-agent-kit 0.9.0 → 0.11.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 +3 -3
- package/bin/solid2-kit.mjs +21 -1
- package/files/commands/solid2-review.md +19 -1
- package/files/shared/agents-section.md +1 -1
- package/files/shared/rules-body.md +52 -15
- package/files/skills/solid-2/SKILL.md +291 -15
- package/files/skills/solid-2/references/official-docs.md +12 -0
- package/package.json +1 -1
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 `[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`),
|
|
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), and core `action(async ...)` with a plain async function instead of a generator (only `yield` re-enters the transaction) |
|
|
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` |
|
|
@@ -127,8 +127,8 @@ Run both gates on every PR so nothing an agent (or human) merges can reintroduce
|
|
|
127
127
|
Solid 1.x patterns:
|
|
128
128
|
|
|
129
129
|
```yaml
|
|
130
|
-
- uses: actions/setup-node@
|
|
131
|
-
with: { node-version:
|
|
130
|
+
- uses: actions/setup-node@v7
|
|
131
|
+
with: { node-version: 26 }
|
|
132
132
|
- run: npm ci
|
|
133
133
|
- run: npx solid2-agent-kit check && npx solid2-agent-kit doctor
|
|
134
134
|
```
|
package/bin/solid2-kit.mjs
CHANGED
|
@@ -354,6 +354,18 @@ function effectSyncFindings(content) {
|
|
|
354
354
|
return matches;
|
|
355
355
|
}
|
|
356
356
|
|
|
357
|
+
// Core `action` (solid-js) takes a generator (`function*` / `async function*`)
|
|
358
|
+
// so `yield` can re-enter the transaction; router `action` (@solidjs/router)
|
|
359
|
+
// legitimately takes `async (form) => ...`. Only flag `action(async` when this
|
|
360
|
+
// file's `action` comes from "solid-js" — a router import (or none) stays silent.
|
|
361
|
+
function actionAsyncFindings(content) {
|
|
362
|
+
if (!/import\s*\{[^}]*\baction\b[^}]*\}\s*from\s*['"]solid-js['"]/.test(content)) return [];
|
|
363
|
+
if (/import\s*\{[^}]*\baction\b[^}]*\}\s*from\s*['"]@solidjs\/router['"]/.test(content)) return [];
|
|
364
|
+
return [...content.matchAll(/\baction\s*\(\s*async(?!\s+function\s*\*)/g)].map((match) => ({
|
|
365
|
+
index: match.index,
|
|
366
|
+
}));
|
|
367
|
+
}
|
|
368
|
+
|
|
357
369
|
const CHECKS = [
|
|
358
370
|
{
|
|
359
371
|
id: 'props-destructure-param',
|
|
@@ -511,7 +523,7 @@ const CHECKS = [
|
|
|
511
523
|
id: 'solid1-router',
|
|
512
524
|
pattern: /<(?:HashRouter|MemoryRouter|Route|Navigate|A|FileRoutes|StartClient|StartServer)\b/g,
|
|
513
525
|
message:
|
|
514
|
-
'Solid Router 0.x/1.x or SolidStart JSX. Define routes with createRouter({ routes }) / fileRoutes(pageRoutes) and plain <a href={Router.paths...}
|
|
526
|
+
'Solid Router 0.x/1.x or SolidStart JSX. Define routes with createRouter({ routes }) / fileRoutes(pageRoutes) and plain <a href={Router.paths...}> (link state: automatic aria-current/data-active/data-pending + CSS, or useLinkState).',
|
|
515
527
|
},
|
|
516
528
|
{
|
|
517
529
|
id: 'meta-provider',
|
|
@@ -593,6 +605,14 @@ const CHECKS = [
|
|
|
593
605
|
message:
|
|
594
606
|
'`<Dynamic>` is a JSX convenience wrapper. Application code should use dynamic(() => ...) from "@solidjs/web" so the component identity stays stable.',
|
|
595
607
|
},
|
|
608
|
+
{
|
|
609
|
+
// `action(async (item) => ...)` with core `action` never re-enters the
|
|
610
|
+
// transaction — `action(async function* ...)` does not match.
|
|
611
|
+
id: 'action-plain-async',
|
|
612
|
+
find: actionAsyncFindings,
|
|
613
|
+
message:
|
|
614
|
+
'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) => ...`.)',
|
|
615
|
+
},
|
|
596
616
|
];
|
|
597
617
|
|
|
598
618
|
// .ts / .tsx / .jsx sources, excluding .d.ts declaration files.
|
|
@@ -17,6 +17,9 @@ Steps:
|
|
|
17
17
|
- async computations reading reactive inputs **after** the first `await`;
|
|
18
18
|
- effects that copy state instead of deriving it, or reading stores in the apply phase;
|
|
19
19
|
- `<For>` over server/refetched rows without a stable-id key function;
|
|
20
|
+
- core `action` bodies that are not generators (plain `async` functions) or that write
|
|
21
|
+
after a bare `await` without re-entering on `yield` (router `action` from
|
|
22
|
+
`@solidjs/router` is the one that takes `async (form) => ...`);
|
|
20
23
|
- `<Loading>`/`<Errored>` wrapping page chrome instead of the data slot, or
|
|
21
24
|
`const u = user()` extracted then passed (a real parent-side read). Passing
|
|
22
25
|
`user={user()}` is the colorless form — do not "fix" it into accessors or
|
|
@@ -31,7 +34,22 @@ Steps:
|
|
|
31
34
|
- tRPC / type-gen around `"use server"`, a client `fetch` after a mutation,
|
|
32
35
|
refetch in `hydrate`/`onSettled`, a custom Worker/Express adapter, or
|
|
33
36
|
delaying `renderToStream` for visual order (`<Reveal>` owns display);
|
|
34
|
-
- store updates that rebuild objects/arrays instead of mutating the draft or reconciling
|
|
37
|
+
- store updates that rebuild objects/arrays instead of mutating the draft or reconciling
|
|
38
|
+
(`filter` for removal is fine — survivors keep identity; only fresh-tree wholesale
|
|
39
|
+
replacement needs `reconcile` / the derived form);
|
|
40
|
+
- hand-rolled link-active comparisons (`location.pathname === ...`) instead of the
|
|
41
|
+
automatic `aria-current` / `data-active` / `data-pending` attributes (+ CSS) or
|
|
42
|
+
`useLinkState` / `useIsRouting`; hand-rolled query parsing instead of
|
|
43
|
+
`useSearchParams`;
|
|
44
|
+
- search inputs bound to a held value without `latest`, effect-based debounce, fully
|
|
45
|
+
controlled forms without `name`s, `{ success: false }` result objects instead of
|
|
46
|
+
thrown failures, or router forms without server-side validation (`throw respond(...)`)
|
|
47
|
+
and `useSubmissions` inline errors;
|
|
48
|
+
- browser-only *values* handled with a `clientOnly` component split or an `isServer`
|
|
49
|
+
branch instead of `ssrSource: "client"` / `"hybrid"` on the memo/signal/derived store;
|
|
50
|
+
- held writes with no feedback (`isPending` / `latest` / optimistic value / `affects()`)
|
|
51
|
+
— the `[SILENT_HOLD]` shape — and dev-console diagnostics (`[STRICT_READ_UNTRACKED]`,
|
|
52
|
+
attribution-only store/list/effect costs) left unaddressed;
|
|
35
53
|
- context values passed as snapshots instead of accessors/setters/stores;
|
|
36
54
|
- components with conditional/early returns on reactive values.
|
|
37
55
|
5. Verify any API you are not certain about against the official docs mirror
|
|
@@ -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`. 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`.
|
|
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`. 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`. Fire-and-forget confirmations wait with `yield until(predicate, {timeout})`. 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, and a held write needs `isPending`/`latest`/optimistic/`affects` feedback or dev + attribution reports `[SILENT_HOLD]`.
|
|
@@ -21,7 +21,8 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
21
21
|
functions (`() => props.x`, `omit(props, ...)`, `children(() => props.children)`) are fine.
|
|
22
22
|
Mechanically enforced by `solid2-kit check` — run it after editing TSX.
|
|
23
23
|
2. **Never read reactive values at component-body top level.** The body runs once, untracked;
|
|
24
|
-
`const v = count()` there is a frozen snapshot (Solid
|
|
24
|
+
`const v = count()` there is a frozen snapshot (Solid reports `[STRICT_READ_UNTRACKED]`
|
|
25
|
+
in dev, naming the component). Carry the accessor
|
|
25
26
|
(the function itself) into JSX and call it there.
|
|
26
27
|
3. Signals are read by **calling** them: `{count()}` in JSX, not `{count}`.
|
|
27
28
|
4. **Derived state is a function, not state + effect.** `const full = () => a() + b()`.
|
|
@@ -30,6 +31,8 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
30
31
|
`createSignal(() => { source(); return initial; })` (re-runs when `source()` changes;
|
|
31
32
|
the setter still works) — never a `createEffect` whose apply calls the setter. The
|
|
32
33
|
derivation also moves the `source()` read to the read site, under its boundaries.
|
|
34
|
+
(React needs `useState(props.x)` plus a `previousX` comparison reset for the same
|
|
35
|
+
derive-override-reset behavior.)
|
|
33
36
|
Mechanically enforced by `solid2-kit check` (effects whose apply only writes a local
|
|
34
37
|
signal). Use `createMemo` only for expensive
|
|
35
38
|
computations, multiple consumers, or an equality boundary. When several writes can land
|
|
@@ -52,13 +55,15 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
52
55
|
`event.currentTarget` (the element that owns the handler), not `event.target`
|
|
53
56
|
(a nested child). Never pass a setter as the handler (`onClick={setCount}`) —
|
|
54
57
|
that writes the event object; wrap it: `onClick={() => setCount((c) => c + 1)}`.
|
|
58
|
+
Debounce at the handler (`onInput={debounce(...)}`), never as an effect copying
|
|
59
|
+
one signal into another.
|
|
55
60
|
7. **Lists use `<For>`**, never `{list().map(...)}` in reactive JSX and never `key` props.
|
|
56
61
|
`{todos().map((t) => <Row todo={t} />)}` *renders*, then recreates every row on each
|
|
57
62
|
update. Row identity: default = item reference; `keyed={(item) => item.id}` = key function
|
|
58
63
|
(child receives item as an accessor); `keyed={false}` = positional. **Rows from
|
|
59
64
|
server/refetched data (fresh object references on every update — fetch results,
|
|
60
65
|
subscription payloads) must use a key function on a stable id**, or every update
|
|
61
|
-
recreates every row. Reference keying is for local arrays whose item identities are
|
|
66
|
+
recreates every row (dev + attribution flags the rebuilds as `[UNSTABLE_LIST_IDENTITY]`). Reference keying is for local arrays whose item identities are
|
|
62
67
|
stable (e.g. store rows). Match the child signature to the mode: default item is the
|
|
63
68
|
**raw** value (`todo.title`); `keyed={(t) => t.id}` / `keyed={false}` pass an
|
|
64
69
|
**accessor** (`todo().title`). Mixing those still “runs” (you render a function, or
|
|
@@ -69,11 +74,14 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
69
74
|
`compute`; its return value feeds `apply`, which does imperative work and may return a
|
|
70
75
|
cleanup. Single-argument `createEffect(fn)` is an error in Solid 2. Do not substitute
|
|
71
76
|
`createTrackedEffect` for that — it is an advanced one-callback form that cannot nest
|
|
72
|
-
primitives. Most React `useEffect` code should not become an effect at all — see the skill.
|
|
77
|
+
primitives. Skip the initial run with `{ defer: true }`. Most React `useEffect` code should not become an effect at all — see the skill.
|
|
73
78
|
9. **Stores update by mutating a draft**: `setStore(draft => { draft.user.name = "Ada" })`.
|
|
74
|
-
Never rebuild with spreads — that destroys property-level subscriptions.
|
|
75
|
-
|
|
76
|
-
|
|
79
|
+
Never rebuild with spreads — that destroys property-level subscriptions. The return
|
|
80
|
+
form is for shapes where mutation is awkward — most commonly removal
|
|
81
|
+
(`setTodos((t) => t.filter(...))` is fine; survivors keep identity). Replacing an
|
|
82
|
+
array with a fresh tree replaces rows by index — that is the identity loss; use
|
|
83
|
+
`reconcile` or the derived form (dev + attribution flags the store-level waste as
|
|
84
|
+
`[IMMUTABLE_UPDATE_IN_STORE]`). Stores are
|
|
77
85
|
**property reads** (`todos.length`, `todo.title`), not accessors — never `todos()`.
|
|
78
86
|
10. **Async data is an async computation**: `createMemo(async () => ...)` (or
|
|
79
87
|
`createMemo(() => fetchUser(id()))`) read under `<Loading>` / `<Errored>`.
|
|
@@ -99,13 +107,18 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
99
107
|
Default navigation holds: keep `selectedId()` so highlight and content stay
|
|
100
108
|
consistent; `latest(selectedId)` only when the highlight should move first.
|
|
101
109
|
Pair `class={{ pending: isPending(selectedId) }}` with a short CSS
|
|
102
|
-
`transition-delay` so fast swaps do not flash.
|
|
110
|
+
`transition-delay` so fast swaps do not flash. Inputs feeding a request bind
|
|
111
|
+
`value={latest(query)}` so keystrokes show while the write is held.
|
|
103
112
|
`isPending` is a per-expression question, not a global spinner: ask it of the
|
|
104
113
|
async source, a derived prop (`isPending(() => props.story)`), or the write
|
|
105
114
|
(`isPending(selectedId)`), even when the fetch lives in a child. It is
|
|
106
115
|
the *refetch* indicator after a settled answer exists — do not use it as the
|
|
107
116
|
first-load spinner (`<Loading>` owns that). Pass the accessor: `isPending(user)`,
|
|
108
117
|
not `isPending(user())` (that evaluates the read before `isPending` runs).
|
|
118
|
+
Granularity is the expression (`isPending(() => item.quantity)` with
|
|
119
|
+
`affects(item, "quantity")`). A hold with no feedback at all is `[SILENT_HOLD]`
|
|
120
|
+
under attribution — always pair a held write with one of these questions, an
|
|
121
|
+
optimistic value, or `affects()`.
|
|
109
122
|
"Prop with local edits" = writable derivation:
|
|
110
123
|
`createSignal(() => props.value)` or `createStore(() => props.value, fallback)`.
|
|
111
124
|
`createOptimistic` is for an in-flight mutation, not a local editing session.
|
|
@@ -166,12 +179,18 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
166
179
|
Making a client store "real" is additive: same setters, wrap mutations in
|
|
167
180
|
`action`, swap `createOptimisticStore` and a file of server functions — do
|
|
168
181
|
not rewrite `App.tsx` with loading/error branches.
|
|
182
|
+
No request counters or `AbortController`s for dedup/cancellation — superseded
|
|
183
|
+
answers drop automatically. Throw unusable responses; never `{ success: false }`
|
|
184
|
+
result objects checked at every read.
|
|
169
185
|
Mechanically enforced by `solid2-kit check` (loading-signal naming, zero-arg-call undefined checks).
|
|
170
186
|
19. **Refetch is `refresh(source)`** — rarely needed, but when it is, never fake it with a
|
|
171
187
|
counter/version signal read inside the computation. Input-driven refetch is automatic
|
|
172
188
|
(tracked inputs re-run the computation) and subscriptions push. Legit uses: after a
|
|
173
189
|
mutation inside an `action`, and explicit reload buttons. Pair with `affects(source)`
|
|
174
190
|
when the reload should present as pending — a bare `refresh()` re-asks quietly.
|
|
191
|
+
On a derived store, `refresh(store)` re-runs the derivation (`fn`); refreshing a
|
|
192
|
+
plain signal accessor is a no-op. `refresh` returns a promise for the target's next
|
|
193
|
+
settled state (`await refresh(user)`, or `yield refresh(x)` inside an action).
|
|
175
194
|
These are three different APIs: core `refresh(source)` reruns a reactive source;
|
|
176
195
|
router `revalidate(getUser.key)` invalidates the query cache; server-function
|
|
177
196
|
`return reload({ revalidate: "todos" })` asks the integration to refresh cached
|
|
@@ -191,11 +210,14 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
191
210
|
`postMessage` / logs via `snapshot(store)` (proxies fail or leak reactivity); render
|
|
192
211
|
modals/tooltips/overlays through `<Portal>` from `@solidjs/web`; mutations whose writes
|
|
193
212
|
cross an async gap default to `action` + `createOptimistic`/`createOptimisticStore` —
|
|
194
|
-
the sync mutation *is* the prediction (an overlay, discarded on settle);
|
|
213
|
+
the sync mutation *is* the prediction (an overlay, discarded on settle); write the
|
|
214
|
+
body as a generator (`function*` / `async function*`) suspending on `yield`, never a
|
|
215
|
+
plain `async` function (`solid2-kit check` flags that); put
|
|
195
216
|
`pending` on the record rather than a second copy of state. Do not snapshot
|
|
196
217
|
and restore, disable optimistic rows until ack, mutex rapid clicks, or freeze
|
|
197
218
|
unrelated writes because one action is in flight; failed-action replay is
|
|
198
|
-
optional.
|
|
219
|
+
optional. Fire-and-forget confirmations wait with `yield until(predicate,
|
|
220
|
+
{ timeout })` — the predicate reads authoritative state. Except reactive clients (e.g. Convex) whose subscriptions already push authoritative
|
|
199
221
|
state after mutations. Compiler: `"jsxImportSource": "@solidjs/web"` (not `"solid-js"`);
|
|
200
222
|
Vite plugin is `@solidjs/vite-plugin` (not `vite-plugin-solid`) — run `solid2-kit doctor`
|
|
201
223
|
after touching `package.json` / tsconfig / root configs; it fails on React and Solid 1.x
|
|
@@ -205,6 +227,11 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
205
227
|
`clientOnly` starts loading at declaration; `{ lazy: true }` defers until first
|
|
206
228
|
render. `NoHydration` / `Hydration` split hydration *ownership*; they do not choose
|
|
207
229
|
visible content — `clientOnly` is for components that must never run on the server.
|
|
230
|
+
A single browser-only *value* takes `ssrSource` on the memo/signal/derived store
|
|
231
|
+
(`"client"` skips the server value and computes after hydration, e.g. `localStorage`;
|
|
232
|
+
`"hybrid"` re-runs after adopting the serialized value, e.g. window size mixed with
|
|
233
|
+
server data) — do not split a component via `clientOnly` for one value. An
|
|
234
|
+
`onSettled` fill-in remains a valid alternative.
|
|
208
235
|
Pass a **function** to `render` / `hydrate` / `renderToString` / `renderToStream`
|
|
209
236
|
(`render(() => <App />, root)`), never `render(<App />, root)`. `hydrate` when the
|
|
210
237
|
container already holds SSR HTML; `render` for an empty mount. Client mode (the
|
|
@@ -240,14 +267,20 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
240
267
|
at module scope — not JSX `<Route>` / `<A>` / `<HashRouter>`. That package's `action` /
|
|
241
268
|
`query` are URL-addressable POST forms and a read cache — not core `action` /
|
|
242
269
|
`refresh` from `solid-js`. Router mutations: `<form action={save} method="post">`
|
|
243
|
-
(POST only; `.with(id)` binds args into the URL).
|
|
270
|
+
(POST only; `.with(id)` binds args into the URL). Validate on the server
|
|
271
|
+
(`throw respond({ issues }, { status: 400 })`); inline errors from `useSubmissions`,
|
|
272
|
+
pending via `form[aria-busy]` CSS. Keep server functions in `src/data/`, not under
|
|
273
|
+
`src/routes` (a `.ts` file there is an API route). `useAction` is JS-only. Cache
|
|
244
274
|
reads: `query(fn, "name")` then `createMemo(() => getUser(id()))`; after a mutation
|
|
245
275
|
`revalidate(getUser.key)`. Do not wrap mutations in `query` (an undeclared server
|
|
246
276
|
function becomes GET). `Router.paths` is not the current location (`useLocation` /
|
|
247
|
-
`useParams`);
|
|
277
|
+
`useParams`); search params via `useSearchParams` (merging setter; typed with a route
|
|
278
|
+
`search` schema + path node). A route `preload` result is `props.data`. One router instance —
|
|
248
279
|
Solid Router does not support nested `<Router>` / nested `createRouter`.
|
|
249
280
|
In-app navigation is `useNavigate` or `<a href={Router.paths...}>`, not
|
|
250
|
-
`window.location` / `history.pushState`.
|
|
281
|
+
`window.location` / `history.pushState`. Link state is automatic
|
|
282
|
+
(`aria-current` / `data-active` / `data-pending` + CSS; `useLinkState` /
|
|
283
|
+
`useIsRouting` in JSX) — never hand-rolled `location.pathname` comparisons. Trusted identity is
|
|
251
284
|
`getRequestEvent()`, never a caller-supplied user id. Unscripted forms use
|
|
252
285
|
the function `.url` (or a router `action`), not a hand-built `/_server/`
|
|
253
286
|
path. Client history adapters do not select the SSR URL — pass
|
|
@@ -258,8 +291,12 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
258
291
|
(`<meta name="description">`) that Solid Meta should manage.
|
|
259
292
|
Return `respond(value, { status, headers })` from a `"use server"` function, not
|
|
260
293
|
`Response.json` — a raw `Response` is HTTP-handler control flow; scripted
|
|
261
|
-
callers should receive the value.
|
|
262
|
-
|
|
294
|
+
callers should receive the value. Reads are function calls too (`await getUser(id)`
|
|
295
|
+
on a `GET()`-declared read; the method mirrors the operation). Return or throw a
|
|
296
|
+
redirect (`if (!session?.userId) throw redirect("/sign-in")`); `redirect()` /
|
|
297
|
+
`respond()` take `revalidate` like `reload()`. Cache GET reads with
|
|
298
|
+
`Cache-Control` on that response (the default is `no-store`), not a hand-rolled
|
|
299
|
+
Solid cache. HTTP does not enforce TypeScript —
|
|
263
300
|
validate inside the function; do not invent tRPC / RPC type-gen.
|
|
264
301
|
Locals only the `"use server"` body reads (db, secrets) stay off the client.
|
|
265
302
|
Do not add an API-route file just to wrap a database call — `"use server"`
|
|
@@ -334,7 +371,7 @@ directory (default `src/`).
|
|
|
334
371
|
| `renderToStringAsync(...)` | `await renderToStream(() => <App />)` (one consumer: `pipe` / `pipeTo` / `readable`) |
|
|
335
372
|
| `clearDelegatedEvents` | delete — delegated listeners are scoped to each render root |
|
|
336
373
|
| `vite-plugin-solid` / `jsxImportSource: "solid-js"` | `@solidjs/vite-plugin` / `"jsxImportSource": "@solidjs/web"` |
|
|
337
|
-
| JSX `<Route>` / `<A>` / `<HashRouter>` / `<Navigate>` / `<FileRoutes>` | `createRouter({ routes })`, `fileRoutes(pageRoutes)`, plain `<a href={Router.paths...}>`, `hashHistory()` |
|
|
374
|
+
| 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) |
|
|
338
375
|
| `createAsync` / `createAsyncStore` / `useSubmission` / router `json()` / `cache()` | `createMemo(() => getUser(id()))`, `useSubmissions`, `respond()` from `@solidjs/web`, `query` |
|
|
339
376
|
| `import ... from "@solidjs/start"` / `vinxi` / `h3` / `"use client"` | `GET` from `@solidjs/web/server-functions`; Solid has no `"use client"` |
|
|
340
377
|
| `render(<App />, root)` | `render(() => <App />, root)` (same for `hydrate` / `renderToString` / `renderToStream`) |
|
|
@@ -73,6 +73,7 @@ Most React `useEffect` code should NOT become `createEffect`:
|
|
|
73
73
|
| 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 |
|
|
74
74
|
| 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)`) |
|
|
75
75
|
| Browser-only widget (charts, maps, `window`) | `clientOnly(() => import("./Chart"))` from `@solidjs/web` (`{ lazy: true }` defers the import until first render) |
|
|
76
|
+
| 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) |
|
|
76
77
|
| Server vs browser branch | `isServer` / `isDev` from `@solidjs/web` (build-time constants), not `typeof window` |
|
|
77
78
|
| SSR-stable `id` / `for` / `aria-*` pairing | `createUniqueId()` |
|
|
78
79
|
|
|
@@ -223,7 +224,9 @@ createEffect(
|
|
|
223
224
|
Reads inside `apply` do not track. Extract every needed reactive value in `compute`
|
|
224
225
|
(e.g. `() => ({ name: user.name, role: user.role })`). Passing a store proxy into `apply`
|
|
225
226
|
and reading `user.name` there *runs once* and never retriggers. Compute-phase errors can be
|
|
226
|
-
intercepted with the bundle form `createEffect(compute, { effect, error })`.
|
|
227
|
+
intercepted with the bundle form `createEffect(compute, { effect, error })`. Skip the
|
|
228
|
+
initial run with `{ defer: true }` — the `on(deps, fn, { defer: true })` replacement;
|
|
229
|
+
the effect first runs on the next change.
|
|
227
230
|
|
|
228
231
|
A two-phase effect whose apply only calls a local signal setter is "state + effect" in
|
|
229
232
|
disguise — formally legal Solid 2, still rule 4 (`solid2-kit check` flags it). The reset
|
|
@@ -247,6 +250,10 @@ const [signInError, setSignInError] = createSignal<string | null>(() => {
|
|
|
247
250
|
});
|
|
248
251
|
```
|
|
249
252
|
|
|
253
|
+
The writable form spells exactly this: derive from the source, allow a local
|
|
254
|
+
override, reset when the source changes. (React needs `useState(props.x)` plus a
|
|
255
|
+
`previousX` comparison reset during render for the same behavior.)
|
|
256
|
+
|
|
250
257
|
### Async data — fetch high, block low
|
|
251
258
|
|
|
252
259
|
Creating and reading an async value are the same as sync. **Consuming** (the
|
|
@@ -330,11 +337,18 @@ return <StoryDetail story={story} />;
|
|
|
330
337
|
(`isPending(() => props.story)`), on a derived memo (`isPending(byline)`), or
|
|
331
338
|
*above* the fetch but below the write (`isPending(selectedId)`), even when the
|
|
332
339
|
fetch lives in a child. You do not need to pass the memo accessor just to ask.
|
|
340
|
+
Granularity is the expression: `isPending(() => item.quantity)` (paired with
|
|
341
|
+
`affects(item, "quantity")`) or `isPending(() => todos.length)` dims only what
|
|
342
|
+
that read depends on. A hold with no feedback at all is `[SILENT_HOLD]` under
|
|
343
|
+
attribution — always pair a held write with one of these questions, an
|
|
344
|
+
optimistic value, or `affects()`.
|
|
333
345
|
- Default navigation **holds**. `selectedId()` and the old story stay put until
|
|
334
346
|
the new answer lands, so the highlight never points at the wrong content.
|
|
335
347
|
`latest(selectedId)` is the opt-in when the design wants the highlight to move
|
|
336
348
|
on click. Pair `class={{ pending: isPending(selectedId) }}` with a short CSS
|
|
337
|
-
`transition-delay` so fast swaps do not flash.
|
|
349
|
+
`transition-delay` so fast swaps do not flash. Inputs that feed a request follow
|
|
350
|
+
the same rule: bind `value={latest(query)}` so a controlled search box reflects
|
|
351
|
+
keystrokes while its write is held (an uncontrolled input needs nothing).
|
|
338
352
|
- Read every reactive input **before the first `await`** in an async computation. A read
|
|
339
353
|
after `await` does not subscribe; production can sit pending with no retry.
|
|
340
354
|
- `<Errored>` function fallbacks receive an error **accessor** and a `reset` callback:
|
|
@@ -354,7 +368,12 @@ reflexes that bypass the async model and `solid2-kit check` flags them. If a val
|
|
|
354
368
|
make it an async computation and read it under boundaries. Refetching is `refresh(source)`
|
|
355
369
|
(typically as the last step of a mutation `action`, or a reload button) — never a
|
|
356
370
|
counter/version signal read inside the computation to force re-runs; input-driven refetch is
|
|
357
|
-
already automatic.
|
|
371
|
+
already automatic. No request counters or `AbortController`s for dedup/cancellation
|
|
372
|
+
either — superseded answers are dropped automatically and settled content holds;
|
|
373
|
+
debounce at the event handler (`onInput={debounce(...)}`), never as an effect copying
|
|
374
|
+
one signal into another. And throw unusable responses instead of returning
|
|
375
|
+
`{ success: false }` objects checked at every read — failures travel to `<Errored>`
|
|
376
|
+
like values.
|
|
358
377
|
|
|
359
378
|
**Normalize thrown values once.** Thrown values are `unknown` in JavaScript, so define one
|
|
360
379
|
shared `ErrorFallback` component per app — extract the message via `instanceof Error`, wire
|
|
@@ -409,6 +428,35 @@ state — it is an overlay the graph discards when the action settles (pass or f
|
|
|
409
428
|
There is no rollback to write. Put in-flight affordances on the record
|
|
410
429
|
(`pending?: boolean`); they vanish with the overlay.
|
|
411
430
|
|
|
431
|
+
Minimal shape — the durable value derives from `fn`, optimistic writes overlay it,
|
|
432
|
+
and `refresh` re-runs the derivation so the store reconciles against the server
|
|
433
|
+
(no separate `getMessages()` call):
|
|
434
|
+
|
|
435
|
+
```tsx
|
|
436
|
+
import { action, createOptimisticStore, refresh } from 'solid-js';
|
|
437
|
+
|
|
438
|
+
function createMessages() { // call from a component — not module scope (SSR shares modules)
|
|
439
|
+
const [messages, setMessages] = createOptimisticStore(() => getMessages(), []);
|
|
440
|
+
|
|
441
|
+
const addMessage = action(function* (message: Message) {
|
|
442
|
+
setMessages((m) => { m.push(message); });
|
|
443
|
+
yield saveMessage(message);
|
|
444
|
+
refresh(messages);
|
|
445
|
+
});
|
|
446
|
+
return [messages, { addMessage }] as const;
|
|
447
|
+
}
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
Mutate the draft (`m.push(message)`), never spread-rebuild (`[...m, message]`) —
|
|
451
|
+
stores track writes finely, so only the changed part updates. When the action
|
|
452
|
+
settles, the overlay is discarded and the refreshed server data is authoritative;
|
|
453
|
+
if it agrees with the prediction, reconciliation finds no differences and nothing
|
|
454
|
+
re-renders. Derived reconciliation keys by `"id"` by default — pass `{ key: ... }`
|
|
455
|
+
only when the identity field differs.
|
|
456
|
+
|
|
457
|
+
Layered form, when per-row UI must survive overlay discard (a recoverable error
|
|
458
|
+
with Retry on that row only):
|
|
459
|
+
|
|
412
460
|
```tsx
|
|
413
461
|
import { action, createOptimisticStore, refresh } from 'solid-js';
|
|
414
462
|
|
|
@@ -480,6 +528,28 @@ snapshot/restore.
|
|
|
480
528
|
**`yield`, not a bare `await`, is the transaction boundary.** `await api.save()` then
|
|
481
529
|
`setTodos(...)` *runs*, but the write commits immediately — it is not held as an
|
|
482
530
|
overlay. Either `yield saveTodo(todo)` or, when you need a typed result, `const saved = await api.save(); yield; setTodos(...)`.
|
|
531
|
+
Write the body as a generator — `function*` or `async function*`, never a plain
|
|
532
|
+
`async (...) =>` — because only `yield` re-enters the transaction: the runtime has no
|
|
533
|
+
hook into a plain `await` continuation, so even inside `async function*` an `await`
|
|
534
|
+
leaves the transaction until the next `yield` (`solid2-kit check` flags the
|
|
535
|
+
non-generator form).
|
|
536
|
+
|
|
537
|
+
When confirmation arrives outside the response — a fire-and-forget transport echoed
|
|
538
|
+
back on a live source — hold the action open with `yield until(predicate, { timeout })`:
|
|
539
|
+
|
|
540
|
+
```tsx
|
|
541
|
+
const send = action(function* (text: string) {
|
|
542
|
+
const clientId = crypto.randomUUID();
|
|
543
|
+
setMessages((m) => { m.push({ clientId, text, pending: true }); });
|
|
544
|
+
yield socket.send({ clientId, text });
|
|
545
|
+
yield until(() => messages.some((m) => m.clientId === clientId), { timeout: 10_000 });
|
|
546
|
+
});
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
The predicate reads authoritative state, so the optimistic row cannot satisfy it;
|
|
550
|
+
only the echo can. Timeout rejects with `TimeoutError` (strongly recommended on
|
|
551
|
+
droppable transports; abort via `signal`). Outside actions, `await until(() => user())`
|
|
552
|
+
waits for a value before continuing.
|
|
483
553
|
|
|
484
554
|
Navigation-shaped updates do **not** need core `action`. A plain setter is enough:
|
|
485
555
|
reads pull the async, and downstream async computeds hold previous values until the
|
|
@@ -632,6 +702,46 @@ do not `return () => cleanup`. Put owned setup in a **directive factory** (like
|
|
|
632
702
|
`ResizeObserver`, third-party widgets, and native listener options (`capture` /
|
|
633
703
|
`passive`) — Solid event props do not take those options.
|
|
634
704
|
|
|
705
|
+
### Per-value SSR policy: `ssrSource`
|
|
706
|
+
|
|
707
|
+
`clientOnly` splits a *component*; `ssrSource` keeps one *value* off the server,
|
|
708
|
+
decided at the data source. `createMemo`, function-form `createSignal`, and derived
|
|
709
|
+
stores (`createStore(fn, seed)`, `createProjection`, `createOptimisticStore(fn, seed)`)
|
|
710
|
+
all accept `ssrSource: "server" | "hybrid" | "client"`:
|
|
711
|
+
|
|
712
|
+
```tsx
|
|
713
|
+
import { Loading, createMemo } from 'solid-js';
|
|
714
|
+
|
|
715
|
+
function DraftPreview(props: { documentId: string }) {
|
|
716
|
+
const draft = createMemo(
|
|
717
|
+
() => localStorage.getItem(`draft:${props.documentId}`),
|
|
718
|
+
{ ssrSource: 'client' },
|
|
719
|
+
);
|
|
720
|
+
|
|
721
|
+
return (
|
|
722
|
+
<section>
|
|
723
|
+
<h2>Saved draft</h2>
|
|
724
|
+
<Loading fallback={<p>Checking for a saved draft...</p>}>
|
|
725
|
+
<p>{draft() ?? 'No saved draft.'}</p>
|
|
726
|
+
</Loading>
|
|
727
|
+
</section>
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
| Policy | Meaning | Use when |
|
|
733
|
+
|---|---|---|
|
|
734
|
+
| `"server"` (default) | Client adopts the serialized server value; compute does not re-run | Compute is deterministic from server-available inputs |
|
|
735
|
+
| `"hybrid"` | Client adopts the serialized value, then re-runs the compute to take over | Server data mixes with client signals (window size, user locale) |
|
|
736
|
+
| `"client"` | Server value skipped; compute runs after hydration as if first-mounted | Serialization is meaningless (`localStorage`, viewport) |
|
|
737
|
+
|
|
738
|
+
With `"client"`, the bare form suspends on the server and the nearest `<Loading>`
|
|
739
|
+
fallback renders into the HTML; a declared first value (`loadingValue` on
|
|
740
|
+
signal-family sources, `seedLoadingValue: true` on store-family sources) renders
|
|
741
|
+
provisional data with no boundary involvement instead. Filling the value in from
|
|
742
|
+
an `onSettled` callback remains a valid alternative; `NoHydration` / `Hydration`
|
|
743
|
+
split hydration *ownership* instead and do not express this policy.
|
|
744
|
+
|
|
635
745
|
## Scheduling and tests
|
|
636
746
|
|
|
637
747
|
Signal/store writes outside a synchronous flush scope are **staged**; the reactive queue
|
|
@@ -648,6 +758,8 @@ Wait for an async expression to settle in tests: `await resolve(() => value())`.
|
|
|
648
758
|
on a pending resource. `onSettled` is also a **single** fire (untracked): it is not
|
|
649
759
|
`onMount` that re-runs, and it is not an effect. Run tests in dev mode first — Solid 2 emits diagnostics for top-level reactive reads,
|
|
650
760
|
writes from owned scopes, and async reads outside `<Loading>`. Fix them; don't suppress.
|
|
761
|
+
A read at component-body top level reports `[STRICT_READ_UNTRACKED]` naming the
|
|
762
|
+
component — move the read into JSX, a memo, or an effect compute.
|
|
651
763
|
|
|
652
764
|
Component tests use `@solidjs/testing-library`. Pass a **function** to `render` so
|
|
653
765
|
the tree has an owner, and `flush()` after interactions that stage writes:
|
|
@@ -675,6 +787,60 @@ To turn those dev diagnostics into a hard gate, patch `console.warn` in the test
|
|
|
675
787
|
file to rethrow unexpected warnings — then any top-level reactive read or unowned write
|
|
676
788
|
an agent sneaks in fails the suite instead of scrolling by.
|
|
677
789
|
|
|
790
|
+
### Dev diagnostics and attribution
|
|
791
|
+
|
|
792
|
+
Diagnostics come in two tiers, both dev-build only (production strips them and `DEV`
|
|
793
|
+
is `undefined` there). Always-on findings include misplaced reads
|
|
794
|
+
(`[STRICT_READ_UNTRACKED]`, `[PENDING_ASYNC_UNTRACKED_READ]`), misplaced writes
|
|
795
|
+
(`[REACTIVE_WRITE_IN_OWNED_SCOPE]`, `[FLUSH_IN_ACTION]`, `[SERVER_WRITE]`), and
|
|
796
|
+
`[ASYNC_OUTSIDE_LOADING_BOUNDARY]` when a tracked read has no boundary above it.
|
|
797
|
+
Each entry carries a stable `[CODE]`, what the runtime observed, and an owner chain
|
|
798
|
+
(`in <App> › <Cart> › <LineItem>`); never silence a code before understanding it.
|
|
799
|
+
|
|
800
|
+
Cost and responsiveness findings need the opt-in attribution engine, which records
|
|
801
|
+
every scope that re-ran, what changed to cause it, and how long it took:
|
|
802
|
+
|
|
803
|
+
```ts
|
|
804
|
+
import { DEV } from 'solid-js';
|
|
805
|
+
|
|
806
|
+
DEV?.attribution.enable();
|
|
807
|
+
// reproduce the interaction, then ask why a scope ran:
|
|
808
|
+
for (const event of DEV!.attribution.why(total)) {
|
|
809
|
+
console.log(DEV!.attribution.format(event));
|
|
810
|
+
}
|
|
811
|
+
```
|
|
812
|
+
|
|
813
|
+
While attribution is enabled, Solid also warns on its own about over-subscription,
|
|
814
|
+
chained async waterfalls (`[ASYNC_WATERFALL]`), memos whose fresh-but-equal output
|
|
815
|
+
never settles (`[UNSTABLE_MEMO_OUTPUT]`), effects that relay state
|
|
816
|
+
(`[EFFECT_RELAY_TEAR]`) or feed their own inputs (`[EFFECT_WRITES_OWN_SOURCE]`),
|
|
817
|
+
immutable updates inside stores (`[IMMUTABLE_UPDATE_IN_STORE]` — mutate the draft),
|
|
818
|
+
row-rebuilding lists (`[UNSTABLE_LIST_IDENTITY]` — key by id or reconcile into a
|
|
819
|
+
store), and silent holds (`[SILENT_HOLD]` — a write held on async work with no
|
|
820
|
+
`isPending()` / `latest()` reader, optimistic value, `affects()` mark, or effect that
|
|
821
|
+
ran while held; reported from 300ms as info, from 500ms as a warning). `DEV.attribution.costs()`,
|
|
822
|
+
`holds()`, and `feedback()` rank the session worst-first. Name the scopes you intend
|
|
823
|
+
to interrogate (`createMemo(..., { name: "total" })`); `DEV.attribution.subscriptions(scope)`
|
|
824
|
+
lists a scope's current dependencies to compare against what it uses.
|
|
825
|
+
|
|
826
|
+
For regression tests, `@solidjs/diagnostics` turns the same channels into assertions:
|
|
827
|
+
|
|
828
|
+
```ts
|
|
829
|
+
import { captureArtifact } from '@solidjs/diagnostics';
|
|
830
|
+
|
|
831
|
+
const { artifact } = await captureArtifact(() => {
|
|
832
|
+
fireEvent.click(getByRole('button', { name: 'Add' }));
|
|
833
|
+
flush();
|
|
834
|
+
}, { scenario: 'add-item' });
|
|
835
|
+
|
|
836
|
+
expect(artifact).toHaveNoDiagnostics();
|
|
837
|
+
expect(artifact).toHaveNoSilentHolds();
|
|
838
|
+
```
|
|
839
|
+
|
|
840
|
+
`toHaveNoDiagnostics` fails on any coded warning (`info` findings excluded);
|
|
841
|
+
`toStayWithinRerunBudget` / `toHaveNoWaste` catch recompute regressions. See the
|
|
842
|
+
debugging-reactivity guide in [references/official-docs.md](references/official-docs.md).
|
|
843
|
+
|
|
678
844
|
## Client mode: no server HTML, no hydration reflexes
|
|
679
845
|
|
|
680
846
|
The `@solidjs/vite-plugin` default (client mode, the `bare` tier) serves an empty-body
|
|
@@ -687,7 +853,7 @@ deferred "safe" initial value is worse than unnecessary: if anything persists th
|
|
|
687
853
|
(an effect writing `localStorage`), the placeholder **overwrites the user's stored
|
|
688
854
|
preference** before the real value is adopted. Mismatch only becomes a concept in
|
|
689
855
|
start/SSR mode, where `hydrate()` mounts onto server HTML — and the Solid answer there is
|
|
690
|
-
`isServer` / `clientOnly`, still not React-style deferred adoption.
|
|
856
|
+
`isServer` / `clientOnly` / per-value `ssrSource`, still not React-style deferred adoption.
|
|
691
857
|
|
|
692
858
|
## App stack (only if the project has these packages)
|
|
693
859
|
|
|
@@ -736,7 +902,7 @@ export default function App() {
|
|
|
736
902
|
<Router>
|
|
737
903
|
{(props) => (
|
|
738
904
|
<>
|
|
739
|
-
<a href={Router.paths
|
|
905
|
+
<a href={Router.paths}>Home</a>
|
|
740
906
|
<main>{props.children}</main>
|
|
741
907
|
</>
|
|
742
908
|
)}
|
|
@@ -759,6 +925,27 @@ Link warming: `preload="false"` skips that link's *data* preload; `preloadLinks:
|
|
|
759
925
|
disables automatic link preloading. Intent values are `"initial"` / `"navigate"` /
|
|
760
926
|
`"native"` / `"preload"`. One router per app.
|
|
761
927
|
|
|
928
|
+
Link state needs no component code: the router sets `aria-current="page"` (exact
|
|
929
|
+
match), `data-active` (exact or descendant — the `/` link is active only on exact
|
|
930
|
+
match), and `data-pending` (in-flight navigation target) on the anchors it handles;
|
|
931
|
+
style them in CSS. For non-anchors, or state needed in JSX, `useLinkState(() => href,
|
|
932
|
+
{ end })` returns the same three as accessors (`active` / `current` / `pending`);
|
|
933
|
+
`useIsRouting()` is the page-level pending flag for progress bars. Hand-rolled
|
|
934
|
+
`location.pathname === ...` comparisons are the wrong tool.
|
|
935
|
+
|
|
936
|
+
Build URLs from `paths`: static segments are properties (`Router.paths.about`, never
|
|
937
|
+
`Router.paths.about()`), parameterized segments are calls (`paths.products(id)`),
|
|
938
|
+
followed by an optional search object and hash (`paths.search({ q, page }, "results")`).
|
|
939
|
+
Nodes stringify in `href` / `navigate()` / `redirect()`, so call with no arguments
|
|
940
|
+
only when an API insists on a plain string.
|
|
941
|
+
|
|
942
|
+
Search params are the home for shareable state (filter, sort, page): `const [search,
|
|
943
|
+
setSearch] = useSearchParams()` — the setter merges and navigates without scrolling;
|
|
944
|
+
`""` / `undefined` / `null` removes a key. For typed values, put a synchronous
|
|
945
|
+
Standard Schema on the route's `search` field and pass the path node:
|
|
946
|
+
`useSearchParams(Router.paths.search)` (async schemas throw; a failing schema leaves
|
|
947
|
+
the raw strings rather than throwing).
|
|
948
|
+
|
|
762
949
|
**Two different `action`s.** Core `action` from `solid-js` is a generator transaction
|
|
763
950
|
(optimistic writes that span an async gap). Router `action` from `@solidjs/router` is
|
|
764
951
|
a URL-addressable POST mutation (`useSubmissions`, `<form action={save} method="post">`).
|
|
@@ -783,6 +970,59 @@ Only POST forms are accepted. Bind extra args with `.with(id)` (they go in the
|
|
|
783
970
|
action URL). `onSubmit={(e) => { e.preventDefault(); fetch(...) }}` *runs* and
|
|
784
971
|
drops the no-JS form fallback. `useAction` is JS-only for the same reason.
|
|
785
972
|
|
|
973
|
+
Validated forms come in three passes sharing one HTML shape. Pass 1 (above) posts
|
|
974
|
+
uncontrolled inputs with browser `required` / `pattern` — it works with no JavaScript.
|
|
975
|
+
Pass 2 validates on the server, where the user cannot bypass it:
|
|
976
|
+
|
|
977
|
+
```ts
|
|
978
|
+
import { redirect, respond } from '@solidjs/web';
|
|
979
|
+
import * as v from 'valibot';
|
|
980
|
+
|
|
981
|
+
const Address = v.object({
|
|
982
|
+
postalCode: v.pipe(v.string(), v.regex(/^[0-9]{5}$/, 'Enter a five-digit postal code')),
|
|
983
|
+
});
|
|
984
|
+
export type AddressIssues = Partial<Record<'postalCode', string>>;
|
|
985
|
+
|
|
986
|
+
export async function saveAddress(form: FormData) {
|
|
987
|
+
'use server';
|
|
988
|
+
const parsed = v.safeParse(Address, Object.fromEntries(form));
|
|
989
|
+
if (!parsed.success) {
|
|
990
|
+
const issues: AddressIssues = {};
|
|
991
|
+
for (const issue of parsed.issues) {
|
|
992
|
+
const field = issue.path?.[0]?.key as keyof AddressIssues | undefined;
|
|
993
|
+
if (field && !issues[field]) issues[field] = issue.message;
|
|
994
|
+
}
|
|
995
|
+
throw respond({ issues }, { status: 400 });
|
|
996
|
+
}
|
|
997
|
+
await database.addresses.save(parsed.output);
|
|
998
|
+
return redirect('/checkout/shipping');
|
|
999
|
+
}
|
|
1000
|
+
```
|
|
1001
|
+
|
|
1002
|
+
Keep server functions in `src/data/`, not under `src/routes` (with method discovery
|
|
1003
|
+
on, a `.ts` file there is an API route). `throw respond(...)` is intentional control
|
|
1004
|
+
flow — the status and value survive production sanitization, unlike a plain thrown
|
|
1005
|
+
`Error`. Pass 3 renders the messages inline from the recorded submission:
|
|
1006
|
+
|
|
1007
|
+
```tsx
|
|
1008
|
+
import { action, useSubmissions } from '@solidjs/router';
|
|
1009
|
+
|
|
1010
|
+
const submitAddress = action(saveAddress);
|
|
1011
|
+
const submissions = useSubmissions(submitAddress);
|
|
1012
|
+
const issues = () => (submissions.at(-1)?.error as { issues?: AddressIssues } | undefined)?.issues ?? {};
|
|
1013
|
+
|
|
1014
|
+
<Show when={issues().postalCode}>{(message) => <p role="alert">{message()}</p>}</Show>
|
|
1015
|
+
```
|
|
1016
|
+
|
|
1017
|
+
`issues()` is a plain derived function; `aria-invalid` on the input, `role="alert"`
|
|
1018
|
+
on the message. Redirect outcomes do not stay in the list; the no-JS outcome arrives
|
|
1019
|
+
via flash cookie as the same submission shape. Pending state is CSS on the automatic
|
|
1020
|
+
attribute — `form[aria-busy] button[type="submit"] { opacity: .6 }` — no component
|
|
1021
|
+
code. `submission.clear()` on dismiss/resubmit only, never from an effect (errors
|
|
1022
|
+
would flash and vanish). Live per-keystroke validation is a controlled input plus a
|
|
1023
|
+
memo message (`aria-live="polite"`), keeping `name` so `FormData` still works; leave
|
|
1024
|
+
the rest uncontrolled.
|
|
1025
|
+
|
|
786
1026
|
**`query` is the read cache** (optional, only with the router). Wrap a read,
|
|
787
1027
|
give it a stable name, and consume it through a Solid async primitive:
|
|
788
1028
|
|
|
@@ -811,7 +1051,8 @@ router mutation, `revalidate(...)` — not core `refresh()`, and not
|
|
|
811
1051
|
refresh cached data: `return reload({ revalidate: "todos" })`). `live()` sources
|
|
812
1052
|
update through the open stream; do not `revalidate` them. Replace SolidStart /
|
|
813
1053
|
Router 0.x leftovers: no `createAsync`, `useSubmission` (singular), `cache()`,
|
|
814
|
-
router `json()`, or `<FileRoutes />` (`fileRoutes(pageRoutes)`
|
|
1054
|
+
router `json()`, or `<FileRoutes />` (`fileRoutes(pageRoutes)` from `@solidjs/router/fs`
|
|
1055
|
+
over `pageRoutes` from `virtual:file-routes` instead).
|
|
815
1056
|
|
|
816
1057
|
### Server functions — `"use server"`
|
|
817
1058
|
|
|
@@ -836,7 +1077,9 @@ reference runs in-process (no HTTP); in the browser it is HTTP. Anything
|
|
|
836
1077
|
referenced only inside the `"use server"` body (db client, secrets) never
|
|
837
1078
|
reaches the client — the directive is the privacy boundary. Mutations that should
|
|
838
1079
|
revalidate: `redirect` / `reload` from `@solidjs/web` (see the server-function
|
|
839
|
-
guides).
|
|
1080
|
+
guides). Return or throw a redirect — guards read `if (!session?.userId) throw
|
|
1081
|
+
redirect("/sign-in")`. `redirect()` and `respond()` accept the same `revalidate`
|
|
1082
|
+
option as `reload()` when the mutation also changes location or returns a value. After a router/server mutation do **not** follow with a client `fetch`
|
|
840
1083
|
to "refresh the page" — `reload` / `revalidate` (and the router's single-flight
|
|
841
1084
|
response) close that loop. For 404 during SSR: `httpStatus(404)` from `@solidjs/web`.
|
|
842
1085
|
`throw new Error("…")` is stripped to `"Internal Server Error"` in production —
|
|
@@ -847,7 +1090,13 @@ caller unwraps. JSON-encodable arguments only unless `enableRichArguments()`
|
|
|
847
1090
|
was called once in the client entry (`Date` / `Map` / `Set` throw without it).
|
|
848
1091
|
`GET()` is only for idempotent reads (URLs leak into logs/history);
|
|
849
1092
|
import it from `@solidjs/web/server-functions`, never `@solidjs/start`.
|
|
850
|
-
|
|
1093
|
+
Reads are function calls too: `GET(async (id) => { "use server"; ... })` is invoked
|
|
1094
|
+
as `await getUser(id)` — Solid owns the endpoint, serialization, and decoding, the
|
|
1095
|
+
database access stays on the server, and argument/return types flow to the caller.
|
|
1096
|
+
The HTTP method mirrors the operation (reads GET, mutations POST); when encoded
|
|
1097
|
+
arguments would exceed the URL limit the client falls back to a read-only POST.
|
|
1098
|
+
Cache GET reads with HTTP, not a hand-rolled Solid cache: responses default to
|
|
1099
|
+
`Cache-Control: no-store`, so supply the policy via `respond()` — the caller still
|
|
851
1100
|
receives the value; headers ride the transport.
|
|
852
1101
|
|
|
853
1102
|
```ts
|
|
@@ -943,8 +1192,9 @@ how to reuse. Prefer the form on the right.
|
|
|
943
1192
|
| `const rest = { ...props }` | `const rest = omit(props, "label")` |
|
|
944
1193
|
| `const user = createMemo(async () => { await fetch(...); return id(); })` | read `id()` **before** `await` |
|
|
945
1194
|
| `action(async function* () { await save(); setX(v); })` | `yield save()` or `await save(); yield; setX(v)` |
|
|
1195
|
+
| `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) => ...` |
|
|
946
1196
|
| `todos()` on a store | `todos.length` / `todo.title` (property reads) |
|
|
947
|
-
| `
|
|
1197
|
+
| `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 |
|
|
948
1198
|
| `createEffect(() => user, (u) => log(u.name))` | `createEffect(() => user.name, (name) => log(name))` |
|
|
949
1199
|
| `<Loading fallback={<PageSkeleton />}>{/* header + data */}</Loading>` | wrap only the data slot; chrome stays outside |
|
|
950
1200
|
| `<Loading on={id} fallback={...}>` (the accessor) | `on={id()}` — a value, so identity changes can show fallback |
|
|
@@ -962,6 +1212,7 @@ how to reuse. Prefer the form on the right.
|
|
|
962
1212
|
| `<For each={rows.slice(from, from + n)}>` | `<Repeat from={from()} count={n}>` |
|
|
963
1213
|
| `{count() && <Badge />}` | `<Show when={count()}>` — `0` must not render as text |
|
|
964
1214
|
| `throw new Error("expired")` from a server function (prod) | `throw markSafeError(...)` or `throw respond(body, { status })` |
|
|
1215
|
+
| `const r = await api.x(); if (!r.ok) ...` result-object checks at every read | throw unusable responses; contain each region with `<Errored>` |
|
|
965
1216
|
| `GET(async (id) => { "use server"; await db.delete(id) })` | mutations stay on POST; `GET()` is for idempotent reads |
|
|
966
1217
|
| `live()` yields as an event log to append | each yield **replaces** the current answer; yield current state first |
|
|
967
1218
|
| `<article innerHTML={html()}>…children…</article>` | `innerHTML` **or** children, not both |
|
|
@@ -975,6 +1226,7 @@ how to reuse. Prefer the form on the right.
|
|
|
975
1226
|
| `renderToString(() => <App />)` for an async or lazy-route tree | `await renderToStream(() => <App />)` (one consumer: `pipe` / `pipeTo` / `readable`). String render emits `<Loading>` fallbacks |
|
|
976
1227
|
| `clientOnly(() => import("./Map"))` for a rarely shown widget | same + `{ lazy: true }` so the import waits until first render (default starts at declaration) |
|
|
977
1228
|
| `<NoHydration><Chart /></NoHydration>` to skip SSR of a widget | `clientOnly(() => import("./Chart"))` — `NoHydration`/`Hydration` split ownership, they do not choose visible content |
|
|
1229
|
+
| `clientOnly` split (or `isServer` branch) for one browser-only *value* | `createMemo(() => localStorage.getItem(k), { ssrSource: "client" })` — `"hybrid"` when server data mixes with client signals |
|
|
978
1230
|
| `await flush()` / `flush()` to wait on a pending memo | `await resolve(() => value())` (or Testing Library async queries); `flush()` only drains staged *sync* writes |
|
|
979
1231
|
| `action(function* () { setX(v); })` around a navigation-shaped setter | plain setter; async computeds hold previous values. Core `action` only when writes happen *after* async work |
|
|
980
1232
|
| `createContext` + a root provider for a true app singleton | module-level signal/store (SSR: shared across requests). Context is subtree scoping |
|
|
@@ -990,6 +1242,8 @@ how to reuse. Prefer the form on the right.
|
|
|
990
1242
|
| `const View = tab() ? A : B; return <View />` | `const View = dynamic(() => (tab() ? A : B))` — the ternary at setup freezes the choice |
|
|
991
1243
|
| `<For each={resolved()}>` after `children()` | `{resolved.toArray()}` — resolved children are not a reactive data list |
|
|
992
1244
|
| `<input onChange={...}>` for keystrokes | `onInput` — native `onChange` is blur/commit |
|
|
1245
|
+
| debounce as an effect copying one signal into another | debounce at the handler: `onInput={debounce((e) => setQuery(e.currentTarget.value), 150)}` |
|
|
1246
|
+
| `value={query()}` on a search box whose writes feed a request | `value={latest(query)}` so keystrokes show while the write is held (uncontrolled needs nothing) |
|
|
993
1247
|
| `createOptimistic` for a local edit draft | writable derivation / plain store. Optimistic is for an in-flight mutation |
|
|
994
1248
|
| `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`) |
|
|
995
1249
|
| `setSubmitted(true)` + an effect that watches it | do the work in the handler or an `action` |
|
|
@@ -997,6 +1251,9 @@ how to reuse. Prefer the form on the right.
|
|
|
997
1251
|
| `action={"/_server/" + id}` in app JSX | `action={createTodo.url}` or a router `action` |
|
|
998
1252
|
| `<form method="get" action={update.url}>` for a mutation | GET forms only for idempotent search; mutations are POST |
|
|
999
1253
|
| `window.location.href = ...` / `history.pushState` | `useNavigate()` or `<a href={Router.paths...}>` |
|
|
1254
|
+
| `class={{ active: location.pathname === "/about" }}` hand-rolled link state | CSS on automatic `aria-current` / `data-active` / `data-pending`; `useLinkState` in JSX, `useIsRouting()` for progress bars |
|
|
1255
|
+
| `new URLSearchParams(location.search)` hand-rolled query parsing | `useSearchParams()` (setter merges, no scroll); typed via a route `search` schema + `useSearchParams(Router.paths.search)` |
|
|
1256
|
+
| `Router.paths.about()` / string-concatenated URLs | static nodes are properties (`Router.paths.about`); calls bind params, then search object and hash |
|
|
1000
1257
|
| `httpStatus(404)` in a click handler | call it bare in the component / error-fallback body (a scope declaration, not a mutation) |
|
|
1001
1258
|
| `try { user() } catch (e) { /* NotReadyError */ }` | `<Loading>` — app code should not catch `NotReadyError` |
|
|
1002
1259
|
| `{latest(() => results())}` as the visible list | settled read + `isPending()` for the indicator; `latest` is for previews |
|
|
@@ -1015,7 +1272,10 @@ how to reuse. Prefer the form on the right.
|
|
|
1015
1272
|
| `Object.assign({}, props)` / expecting `merge` to skip `undefined` like 1.x `mergeProps` | `omit` / `merge` — explicit `undefined` **overrides** (Object.assign) |
|
|
1016
1273
|
| `isPending(user())` / `latest(user())` | `isPending(user)` / `latest(user)` — pass the accessor (or `() => user()`). Calling it first evaluates the read before the helper runs |
|
|
1017
1274
|
| `isPending(...)` as the first-load spinner | `<Loading>` owns first flight; `isPending` is the *refetch* indicator after a settled answer exists |
|
|
1275
|
+
| a held write with no feedback anywhere | pair it with `isPending()` / `latest()` / an optimistic value / `affects()` — otherwise dev + attribution reports `[SILENT_HOLD]` |
|
|
1018
1276
|
| `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 |
|
|
1277
|
+
| `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 |
|
|
1278
|
+
| `submission.clear()` from an effect | on dismiss/resubmit only — from an effect, errors flash and vanish |
|
|
1019
1279
|
| `revalidate(liveSource)` | `live()` updates through the open stream; do not revalidate it |
|
|
1020
1280
|
| `refresh(user)` without `affects` when the reload should look pending | pair `affects(user)` with `refresh(user)` — a bare `refresh` re-asks quietly |
|
|
1021
1281
|
| `<Dynamic component={tab() ? A : B} />` | `const View = dynamic(() => (tab() ? A : B))` — `<Dynamic>` is a JSX convenience; app code should use `dynamic()` for stable identity |
|
|
@@ -1170,14 +1430,17 @@ always-applied rules installed alongside this skill.
|
|
|
1170
1430
|
is a writable derivation (`createSignal(() => { source(); return initial; })`), not
|
|
1171
1431
|
an effect calling the setter.
|
|
1172
1432
|
- [ ] Event handlers wrap setters (`onClick={() => setX(...)}`), use `currentTarget`,
|
|
1173
|
-
and `onInput` for keystrokes.
|
|
1433
|
+
and `onInput` for keystrokes. Debounce at the handler, never as an effect copying
|
|
1434
|
+
signals. `innerHTML` is sanitized and not mixed with children.
|
|
1174
1435
|
- [ ] Async memos are passed as values (`user={user()}`); JSX props are lazy.
|
|
1175
1436
|
Types stay `User` — not `Promise<User>` / `Accessor<User>`. `<Loading>` wraps
|
|
1176
1437
|
the *read*, not the memo, not chrome. Extracting `const u = user()` at the
|
|
1177
1438
|
parent is the read that throws. Nested child fetches run in parallel
|
|
1178
1439
|
(components do not suspend); a real waterfall is `fetchB(a().id)`.
|
|
1179
1440
|
`isPending` is per-expression (`isPending(() => props.story)` works);
|
|
1180
|
-
`latest(selectedId)` is opt-in, not the default highlight.
|
|
1441
|
+
`latest(selectedId)` is opt-in, not the default highlight. Inputs feeding a request
|
|
1442
|
+
bind `latest(query)`; failures throw to `<Errored>`, never `{ success: false }`
|
|
1443
|
+
result objects checked at every read.
|
|
1181
1444
|
- [ ] Lists via `<For>` (server/refetched rows keyed by stable id), conditionals via
|
|
1182
1445
|
ternary/`<Show>`; no `{list().map(...)}` in JSX; no `key` props; no `value()!`.
|
|
1183
1446
|
- [ ] Effects are two-phase and only at imperative boundaries; apply does not read stores.
|
|
@@ -1198,8 +1461,8 @@ always-applied rules installed alongside this skill.
|
|
|
1198
1461
|
is static `dist/client`. Do not delay `renderToStream` for visual order
|
|
1199
1462
|
(`<Reveal>` does).
|
|
1200
1463
|
- [ ] External collections reconcile into stores (function-form `createStore` /
|
|
1201
|
-
`reconcile`), never wholesale draft assignment
|
|
1202
|
-
|
|
1464
|
+
`reconcile`), never wholesale draft assignment of a fresh tree
|
|
1465
|
+
(`setTodos((t) => t.filter(...))` for removal is fine — survivors keep identity).
|
|
1203
1466
|
- [ ] Rest props via `omit`, not `{ ...props }`. Conditional `class` via objects, not string concat.
|
|
1204
1467
|
- [ ] Input filters write back to the DOM on reject (inputs do not rewind).
|
|
1205
1468
|
- [ ] No Solid 1.x imports or APIs (`solid-js/store`, `createResource`, `onMount`, `Suspense`, ...).
|
|
@@ -1207,12 +1470,25 @@ always-applied rules installed alongside this skill.
|
|
|
1207
1470
|
`solid-js`; DOM `JSX` types come from `@solidjs/web`.
|
|
1208
1471
|
- [ ] Inspect children with `children()`; code-split with `lazy` + `<Loading>`; reactive
|
|
1209
1472
|
component choice with `dynamic()`. No `React.lazy`, no effects inside ref callbacks.
|
|
1210
|
-
- [ ] Browser-only code uses `isServer` / `clientOnly
|
|
1473
|
+
- [ ] Browser-only code uses `isServer` / `clientOnly` (components) / `ssrSource`
|
|
1474
|
+
(values), not `typeof window`.
|
|
1211
1475
|
Component teardown is `onSettled` (return cleanup), not `onCleanup`.
|
|
1476
|
+
- [ ] Dev console shows no diagnostics: reads in tracking scopes (no
|
|
1477
|
+
`[STRICT_READ_UNTRACKED]`), held writes paired with `isPending` / `latest` /
|
|
1478
|
+
optimistic values / `affects()` (no `[SILENT_HOLD]`). Attribution-only costs
|
|
1479
|
+
(`[IMMUTABLE_UPDATE_IN_STORE]`, `[UNSTABLE_LIST_IDENTITY]`, `[EFFECT_*]`)
|
|
1480
|
+
checked via `DEV.attribution` when touching stores, lists, or effects. Name
|
|
1481
|
+
interrogated scopes (`{ name: "total" }`).
|
|
1212
1482
|
- [ ] If the project has a router: `createRouter({ routes })`, not JSX `<Route>` / `<A>`.
|
|
1213
1483
|
One instance, no nested `<Router>`. Navigate with `useNavigate` / `<a href>`, not
|
|
1214
1484
|
`window.location`. Router `action`/`query` come from `@solidjs/router` (POST forms + cache), not
|
|
1215
|
-
core `action`/`refresh`. Forms: `<form action={save} method="post">`.
|
|
1485
|
+
core `action`/`refresh`. Forms: `<form action={save} method="post">`. Core `action`
|
|
1486
|
+
bodies are generators (`function*` / `async function*`) suspending on `yield`,
|
|
1487
|
+
never plain `async` functions. Link state is automatic attributes + CSS
|
|
1488
|
+
(`aria-current` / `data-active` / `data-pending`; `useLinkState` / `useIsRouting`
|
|
1489
|
+
in JSX). Search params via `useSearchParams`. Forms validate on the server
|
|
1490
|
+
(`throw respond({ issues }, { status: 400 })`) with inline errors from
|
|
1491
|
+
`useSubmissions`.
|
|
1216
1492
|
- [ ] `render(() => <App />, root)` — a function, not `render(<App />)`. `hydrate` when
|
|
1217
1493
|
HTML already exists. Stream async/lazy trees (`await renderToStream(...)`) with
|
|
1218
1494
|
exactly one consumer (`pipe` / `pipeTo` / `readable`). Snapshot stores with
|
|
@@ -26,6 +26,7 @@ in `llms.txt` rather than guessing from Solid 1.x or React.
|
|
|
26
26
|
- https://v2-rebuild--solid-docs-v2.netlify.app/concepts/components-and-jsx.md
|
|
27
27
|
- https://v2-rebuild--solid-docs-v2.netlify.app/concepts/async-reactivity.md
|
|
28
28
|
- https://v2-rebuild--solid-docs-v2.netlify.app/concepts/boundaries.md
|
|
29
|
+
- https://v2-rebuild--solid-docs-v2.netlify.app/concepts/mutations.md
|
|
29
30
|
- https://v2-rebuild--solid-docs-v2.netlify.app/concepts/rendering-and-ssr.md
|
|
30
31
|
|
|
31
32
|
## Building apps (start mode / fullstack)
|
|
@@ -65,6 +66,10 @@ read these before writing route JSX.
|
|
|
65
66
|
## Guides and migration
|
|
66
67
|
|
|
67
68
|
- https://v2-rebuild--solid-docs-v2.netlify.app/guides/avoid-unnecessary-effects.md
|
|
69
|
+
- https://v2-rebuild--solid-docs-v2.netlify.app/guides/debugging-reactivity.md
|
|
70
|
+
- https://v2-rebuild--solid-docs-v2.netlify.app/guides/lists.md
|
|
71
|
+
- https://v2-rebuild--solid-docs-v2.netlify.app/guides/data-fetching-patterns.md
|
|
72
|
+
- https://v2-rebuild--solid-docs-v2.netlify.app/guides/forms.md
|
|
68
73
|
- https://v2-rebuild--solid-docs-v2.netlify.app/guides/testing.md
|
|
69
74
|
- https://v2-rebuild--solid-docs-v2.netlify.app/migration/from-react.md
|
|
70
75
|
- https://v2-rebuild--solid-docs-v2.netlify.app/migration/from-solid-1.md
|
|
@@ -108,9 +113,14 @@ verify API names against the reference pages above.
|
|
|
108
113
|
- https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/lifecycle-actions/affects.md
|
|
109
114
|
- https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/lifecycle-actions/on-settled.md
|
|
110
115
|
- https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/lifecycle-actions/refresh.md
|
|
116
|
+
- https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/lifecycle-actions/until.md
|
|
111
117
|
- https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/advanced/specialized-reactivity/on-cleanup.md
|
|
112
118
|
- https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/advanced/store-advanced/snapshot.md
|
|
113
119
|
|
|
120
|
+
## Reference: diagnostics (dev builds only)
|
|
121
|
+
|
|
122
|
+
- https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/advanced/diagnostics-dev-hooks/dev.md
|
|
123
|
+
|
|
114
124
|
## Reference: components and JSX (solid-js)
|
|
115
125
|
|
|
116
126
|
- https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/components-jsx/show.md
|
|
@@ -160,8 +170,10 @@ verify API names against the reference pages above.
|
|
|
160
170
|
|
|
161
171
|
- https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-router.md
|
|
162
172
|
- https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-router/router-factory.md
|
|
173
|
+
- https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-router/routes-and-paths.md
|
|
163
174
|
- https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-router/navigation.md
|
|
164
175
|
- https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-router/data.md
|
|
176
|
+
- https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-router/types.md
|
|
165
177
|
- https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-router/history.md
|
|
166
178
|
- https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-router/filesystem.md
|
|
167
179
|
- https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-meta/title.md
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "solid2-agent-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.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",
|