solid2-agent-kit 0.9.0 → 0.11.1
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 +330 -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,99 @@ 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 layers, 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
|
+
For the repair behind any code, read the `reactivity-diagnostics` skill Solid ships
|
|
800
|
+
in the repo (`node_modules/solid-js/skills/reactivity-diagnostics/SKILL.md`) — every
|
|
801
|
+
code maps to what the runtime observed and what to change.
|
|
802
|
+
|
|
803
|
+
Cost and responsiveness findings need the opt-in attribution engine, which records
|
|
804
|
+
every scope that re-ran, what changed to cause it, and how long it took. Since
|
|
805
|
+
`solid-js` 2.0.0-rc.8 the engine lives behind its own subpath — `DEV.attribution`
|
|
806
|
+
no longer exists (it is a type error; `DEV` holds only devtools hooks, graph
|
|
807
|
+
traversal, and console reporting), and the low-level hook slot on `OBSERVE` is for
|
|
808
|
+
engines and devtools, not app code:
|
|
809
|
+
|
|
810
|
+
```ts
|
|
811
|
+
import { attribution } from 'solid-js/attribution';
|
|
812
|
+
import { isDev } from '@solidjs/web';
|
|
813
|
+
|
|
814
|
+
if (isDev) attribution.enable({ log: false });
|
|
815
|
+
// reproduce the interaction, then ask why a scope ran:
|
|
816
|
+
for (const event of attribution.why(total)) {
|
|
817
|
+
console.log(attribution.format(event));
|
|
818
|
+
}
|
|
819
|
+
```
|
|
820
|
+
|
|
821
|
+
`enable()` pretty-prints every re-run's why-chain to the console by default
|
|
822
|
+
(`log: true`); pass `{ log: false }` when you only want the coded findings and plan
|
|
823
|
+
to query the tables yourself. Name the scopes you intend to interrogate
|
|
824
|
+
(`createMemo(..., { name: "total" })`) — chains refer to nodes by name, and
|
|
825
|
+
anonymous nodes print as `computed` / `effect` / `signal`. In production the import
|
|
826
|
+
resolves to an inert same-surface twin (~640 bytes) whose `enable()` is a no-op, so
|
|
827
|
+
with the `isDev` guard the cost is zero and the import can stay in the code.
|
|
828
|
+
|
|
829
|
+
While attribution is enabled, Solid also warns on its own about over-subscription,
|
|
830
|
+
chained async waterfalls (`[ASYNC_WATERFALL]`), memos whose fresh-but-equal output
|
|
831
|
+
never settles (`[UNSTABLE_MEMO_OUTPUT]`), effects that relay state
|
|
832
|
+
(`[EFFECT_RELAY_TEAR]`) or feed their own inputs (`[EFFECT_WRITES_OWN_SOURCE]`),
|
|
833
|
+
immutable updates inside stores (`[IMMUTABLE_UPDATE_IN_STORE]` — mutate the draft),
|
|
834
|
+
row-rebuilding lists (`[UNSTABLE_LIST_IDENTITY]` — key by id or reconcile into a
|
|
835
|
+
store), and silent holds (`[SILENT_HOLD]` — a write held on async work with no
|
|
836
|
+
`isPending()` / `latest()` reader, optimistic value, `affects()` mark, or effect that
|
|
837
|
+
ran while held; info from 100ms, warning from 200ms, tunable via
|
|
838
|
+
`enable({ holds: { infoMs, warnMs } })`). `attribution.costs()`, `holds()`, and
|
|
839
|
+
`feedback()` rank the session worst-first; `waterfalls()`, `navigations()`, and
|
|
840
|
+
`interactions()` hold the fact tables behind the verdicts.
|
|
841
|
+
`attribution.subscriptions(scope)` lists a scope's current dependencies to compare
|
|
842
|
+
against what it uses.
|
|
843
|
+
|
|
844
|
+
For regression tests, `@solidjs/diagnostics` turns the same channels into assertions:
|
|
845
|
+
|
|
846
|
+
```ts
|
|
847
|
+
import { captureArtifact } from '@solidjs/diagnostics';
|
|
848
|
+
|
|
849
|
+
const { artifact } = await captureArtifact(() => {
|
|
850
|
+
fireEvent.click(getByRole('button', { name: 'Add' }));
|
|
851
|
+
flush();
|
|
852
|
+
}, { scenario: 'add-item' });
|
|
853
|
+
|
|
854
|
+
expect(artifact).toHaveNoDiagnostics();
|
|
855
|
+
expect(artifact).toHaveNoSilentHolds();
|
|
856
|
+
```
|
|
857
|
+
|
|
858
|
+
`toHaveNoDiagnostics` fails on any coded warning (`info` findings excluded);
|
|
859
|
+
`toStayWithinRerunBudget` / `toHaveNoWaste` catch recompute regressions. The
|
|
860
|
+
`agent-loops` skill the package ships
|
|
861
|
+
(`node_modules/@solidjs/diagnostics/skills/agent-loops/SKILL.md`) documents the agent
|
|
862
|
+
verification loops: capture until the channel is quiet, scenario budgets as the
|
|
863
|
+
definition of done, and hold/latency gates. See the debugging-reactivity guide in
|
|
864
|
+
[references/official-docs.md](references/official-docs.md).
|
|
865
|
+
|
|
866
|
+
With `@solidjs/diagnostics` in `devDependencies`, the Vite plugin's `diagnostics`
|
|
867
|
+
option (auto-on; dev server only) injects the capture bridge and serves
|
|
868
|
+
`POST /__solid/diagnostics`, so a live page can be interrogated with curl alone —
|
|
869
|
+
no test harness:
|
|
870
|
+
|
|
871
|
+
```sh
|
|
872
|
+
curl -X POST localhost:3000/__solid/diagnostics -d '{"method":"begin"}'
|
|
873
|
+
# ... interact with the app in the browser ...
|
|
874
|
+
curl -X POST localhost:3000/__solid/diagnostics -d '{"method":"whyDidRun","params":{"name":"TodoRow"}}'
|
|
875
|
+
curl -X POST localhost:3000/__solid/diagnostics -d '{"method":"costs"}'
|
|
876
|
+
curl -X POST localhost:3000/__solid/diagnostics -d '{"method":"feedback"}'
|
|
877
|
+
curl -X POST localhost:3000/__solid/diagnostics -d '{"method":"end"}'
|
|
878
|
+
```
|
|
879
|
+
|
|
880
|
+
`GET` the endpoint for status. With several open tabs the first responder wins,
|
|
881
|
+
so keep one page under test.
|
|
882
|
+
|
|
678
883
|
## Client mode: no server HTML, no hydration reflexes
|
|
679
884
|
|
|
680
885
|
The `@solidjs/vite-plugin` default (client mode, the `bare` tier) serves an empty-body
|
|
@@ -687,7 +892,7 @@ deferred "safe" initial value is worse than unnecessary: if anything persists th
|
|
|
687
892
|
(an effect writing `localStorage`), the placeholder **overwrites the user's stored
|
|
688
893
|
preference** before the real value is adopted. Mismatch only becomes a concept in
|
|
689
894
|
start/SSR mode, where `hydrate()` mounts onto server HTML — and the Solid answer there is
|
|
690
|
-
`isServer` / `clientOnly`, still not React-style deferred adoption.
|
|
895
|
+
`isServer` / `clientOnly` / per-value `ssrSource`, still not React-style deferred adoption.
|
|
691
896
|
|
|
692
897
|
## App stack (only if the project has these packages)
|
|
693
898
|
|
|
@@ -736,7 +941,7 @@ export default function App() {
|
|
|
736
941
|
<Router>
|
|
737
942
|
{(props) => (
|
|
738
943
|
<>
|
|
739
|
-
<a href={Router.paths
|
|
944
|
+
<a href={Router.paths}>Home</a>
|
|
740
945
|
<main>{props.children}</main>
|
|
741
946
|
</>
|
|
742
947
|
)}
|
|
@@ -759,6 +964,27 @@ Link warming: `preload="false"` skips that link's *data* preload; `preloadLinks:
|
|
|
759
964
|
disables automatic link preloading. Intent values are `"initial"` / `"navigate"` /
|
|
760
965
|
`"native"` / `"preload"`. One router per app.
|
|
761
966
|
|
|
967
|
+
Link state needs no component code: the router sets `aria-current="page"` (exact
|
|
968
|
+
match), `data-active` (exact or descendant — the `/` link is active only on exact
|
|
969
|
+
match), and `data-pending` (in-flight navigation target) on the anchors it handles;
|
|
970
|
+
style them in CSS. For non-anchors, or state needed in JSX, `useLinkState(() => href,
|
|
971
|
+
{ end })` returns the same three as accessors (`active` / `current` / `pending`);
|
|
972
|
+
`useIsRouting()` is the page-level pending flag for progress bars. Hand-rolled
|
|
973
|
+
`location.pathname === ...` comparisons are the wrong tool.
|
|
974
|
+
|
|
975
|
+
Build URLs from `paths`: static segments are properties (`Router.paths.about`, never
|
|
976
|
+
`Router.paths.about()`), parameterized segments are calls (`paths.products(id)`),
|
|
977
|
+
followed by an optional search object and hash (`paths.search({ q, page }, "results")`).
|
|
978
|
+
Nodes stringify in `href` / `navigate()` / `redirect()`, so call with no arguments
|
|
979
|
+
only when an API insists on a plain string.
|
|
980
|
+
|
|
981
|
+
Search params are the home for shareable state (filter, sort, page): `const [search,
|
|
982
|
+
setSearch] = useSearchParams()` — the setter merges and navigates without scrolling;
|
|
983
|
+
`""` / `undefined` / `null` removes a key. For typed values, put a synchronous
|
|
984
|
+
Standard Schema on the route's `search` field and pass the path node:
|
|
985
|
+
`useSearchParams(Router.paths.search)` (async schemas throw; a failing schema leaves
|
|
986
|
+
the raw strings rather than throwing).
|
|
987
|
+
|
|
762
988
|
**Two different `action`s.** Core `action` from `solid-js` is a generator transaction
|
|
763
989
|
(optimistic writes that span an async gap). Router `action` from `@solidjs/router` is
|
|
764
990
|
a URL-addressable POST mutation (`useSubmissions`, `<form action={save} method="post">`).
|
|
@@ -783,6 +1009,59 @@ Only POST forms are accepted. Bind extra args with `.with(id)` (they go in the
|
|
|
783
1009
|
action URL). `onSubmit={(e) => { e.preventDefault(); fetch(...) }}` *runs* and
|
|
784
1010
|
drops the no-JS form fallback. `useAction` is JS-only for the same reason.
|
|
785
1011
|
|
|
1012
|
+
Validated forms come in three passes sharing one HTML shape. Pass 1 (above) posts
|
|
1013
|
+
uncontrolled inputs with browser `required` / `pattern` — it works with no JavaScript.
|
|
1014
|
+
Pass 2 validates on the server, where the user cannot bypass it:
|
|
1015
|
+
|
|
1016
|
+
```ts
|
|
1017
|
+
import { redirect, respond } from '@solidjs/web';
|
|
1018
|
+
import * as v from 'valibot';
|
|
1019
|
+
|
|
1020
|
+
const Address = v.object({
|
|
1021
|
+
postalCode: v.pipe(v.string(), v.regex(/^[0-9]{5}$/, 'Enter a five-digit postal code')),
|
|
1022
|
+
});
|
|
1023
|
+
export type AddressIssues = Partial<Record<'postalCode', string>>;
|
|
1024
|
+
|
|
1025
|
+
export async function saveAddress(form: FormData) {
|
|
1026
|
+
'use server';
|
|
1027
|
+
const parsed = v.safeParse(Address, Object.fromEntries(form));
|
|
1028
|
+
if (!parsed.success) {
|
|
1029
|
+
const issues: AddressIssues = {};
|
|
1030
|
+
for (const issue of parsed.issues) {
|
|
1031
|
+
const field = issue.path?.[0]?.key as keyof AddressIssues | undefined;
|
|
1032
|
+
if (field && !issues[field]) issues[field] = issue.message;
|
|
1033
|
+
}
|
|
1034
|
+
throw respond({ issues }, { status: 400 });
|
|
1035
|
+
}
|
|
1036
|
+
await database.addresses.save(parsed.output);
|
|
1037
|
+
return redirect('/checkout/shipping');
|
|
1038
|
+
}
|
|
1039
|
+
```
|
|
1040
|
+
|
|
1041
|
+
Keep server functions in `src/data/`, not under `src/routes` (with method discovery
|
|
1042
|
+
on, a `.ts` file there is an API route). `throw respond(...)` is intentional control
|
|
1043
|
+
flow — the status and value survive production sanitization, unlike a plain thrown
|
|
1044
|
+
`Error`. Pass 3 renders the messages inline from the recorded submission:
|
|
1045
|
+
|
|
1046
|
+
```tsx
|
|
1047
|
+
import { action, useSubmissions } from '@solidjs/router';
|
|
1048
|
+
|
|
1049
|
+
const submitAddress = action(saveAddress);
|
|
1050
|
+
const submissions = useSubmissions(submitAddress);
|
|
1051
|
+
const issues = () => (submissions.at(-1)?.error as { issues?: AddressIssues } | undefined)?.issues ?? {};
|
|
1052
|
+
|
|
1053
|
+
<Show when={issues().postalCode}>{(message) => <p role="alert">{message()}</p>}</Show>
|
|
1054
|
+
```
|
|
1055
|
+
|
|
1056
|
+
`issues()` is a plain derived function; `aria-invalid` on the input, `role="alert"`
|
|
1057
|
+
on the message. Redirect outcomes do not stay in the list; the no-JS outcome arrives
|
|
1058
|
+
via flash cookie as the same submission shape. Pending state is CSS on the automatic
|
|
1059
|
+
attribute — `form[aria-busy] button[type="submit"] { opacity: .6 }` — no component
|
|
1060
|
+
code. `submission.clear()` on dismiss/resubmit only, never from an effect (errors
|
|
1061
|
+
would flash and vanish). Live per-keystroke validation is a controlled input plus a
|
|
1062
|
+
memo message (`aria-live="polite"`), keeping `name` so `FormData` still works; leave
|
|
1063
|
+
the rest uncontrolled.
|
|
1064
|
+
|
|
786
1065
|
**`query` is the read cache** (optional, only with the router). Wrap a read,
|
|
787
1066
|
give it a stable name, and consume it through a Solid async primitive:
|
|
788
1067
|
|
|
@@ -811,7 +1090,8 @@ router mutation, `revalidate(...)` — not core `refresh()`, and not
|
|
|
811
1090
|
refresh cached data: `return reload({ revalidate: "todos" })`). `live()` sources
|
|
812
1091
|
update through the open stream; do not `revalidate` them. Replace SolidStart /
|
|
813
1092
|
Router 0.x leftovers: no `createAsync`, `useSubmission` (singular), `cache()`,
|
|
814
|
-
router `json()`, or `<FileRoutes />` (`fileRoutes(pageRoutes)`
|
|
1093
|
+
router `json()`, or `<FileRoutes />` (`fileRoutes(pageRoutes)` from `@solidjs/router/fs`
|
|
1094
|
+
over `pageRoutes` from `virtual:file-routes` instead).
|
|
815
1095
|
|
|
816
1096
|
### Server functions — `"use server"`
|
|
817
1097
|
|
|
@@ -836,7 +1116,9 @@ reference runs in-process (no HTTP); in the browser it is HTTP. Anything
|
|
|
836
1116
|
referenced only inside the `"use server"` body (db client, secrets) never
|
|
837
1117
|
reaches the client — the directive is the privacy boundary. Mutations that should
|
|
838
1118
|
revalidate: `redirect` / `reload` from `@solidjs/web` (see the server-function
|
|
839
|
-
guides).
|
|
1119
|
+
guides). Return or throw a redirect — guards read `if (!session?.userId) throw
|
|
1120
|
+
redirect("/sign-in")`. `redirect()` and `respond()` accept the same `revalidate`
|
|
1121
|
+
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
1122
|
to "refresh the page" — `reload` / `revalidate` (and the router's single-flight
|
|
841
1123
|
response) close that loop. For 404 during SSR: `httpStatus(404)` from `@solidjs/web`.
|
|
842
1124
|
`throw new Error("…")` is stripped to `"Internal Server Error"` in production —
|
|
@@ -847,7 +1129,13 @@ caller unwraps. JSON-encodable arguments only unless `enableRichArguments()`
|
|
|
847
1129
|
was called once in the client entry (`Date` / `Map` / `Set` throw without it).
|
|
848
1130
|
`GET()` is only for idempotent reads (URLs leak into logs/history);
|
|
849
1131
|
import it from `@solidjs/web/server-functions`, never `@solidjs/start`.
|
|
850
|
-
|
|
1132
|
+
Reads are function calls too: `GET(async (id) => { "use server"; ... })` is invoked
|
|
1133
|
+
as `await getUser(id)` — Solid owns the endpoint, serialization, and decoding, the
|
|
1134
|
+
database access stays on the server, and argument/return types flow to the caller.
|
|
1135
|
+
The HTTP method mirrors the operation (reads GET, mutations POST); when encoded
|
|
1136
|
+
arguments would exceed the URL limit the client falls back to a read-only POST.
|
|
1137
|
+
Cache GET reads with HTTP, not a hand-rolled Solid cache: responses default to
|
|
1138
|
+
`Cache-Control: no-store`, so supply the policy via `respond()` — the caller still
|
|
851
1139
|
receives the value; headers ride the transport.
|
|
852
1140
|
|
|
853
1141
|
```ts
|
|
@@ -943,8 +1231,9 @@ how to reuse. Prefer the form on the right.
|
|
|
943
1231
|
| `const rest = { ...props }` | `const rest = omit(props, "label")` |
|
|
944
1232
|
| `const user = createMemo(async () => { await fetch(...); return id(); })` | read `id()` **before** `await` |
|
|
945
1233
|
| `action(async function* () { await save(); setX(v); })` | `yield save()` or `await save(); yield; setX(v)` |
|
|
1234
|
+
| `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
1235
|
| `todos()` on a store | `todos.length` / `todo.title` (property reads) |
|
|
947
|
-
| `
|
|
1236
|
+
| `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
1237
|
| `createEffect(() => user, (u) => log(u.name))` | `createEffect(() => user.name, (name) => log(name))` |
|
|
949
1238
|
| `<Loading fallback={<PageSkeleton />}>{/* header + data */}</Loading>` | wrap only the data slot; chrome stays outside |
|
|
950
1239
|
| `<Loading on={id} fallback={...}>` (the accessor) | `on={id()}` — a value, so identity changes can show fallback |
|
|
@@ -962,6 +1251,7 @@ how to reuse. Prefer the form on the right.
|
|
|
962
1251
|
| `<For each={rows.slice(from, from + n)}>` | `<Repeat from={from()} count={n}>` |
|
|
963
1252
|
| `{count() && <Badge />}` | `<Show when={count()}>` — `0` must not render as text |
|
|
964
1253
|
| `throw new Error("expired")` from a server function (prod) | `throw markSafeError(...)` or `throw respond(body, { status })` |
|
|
1254
|
+
| `const r = await api.x(); if (!r.ok) ...` result-object checks at every read | throw unusable responses; contain each region with `<Errored>` |
|
|
965
1255
|
| `GET(async (id) => { "use server"; await db.delete(id) })` | mutations stay on POST; `GET()` is for idempotent reads |
|
|
966
1256
|
| `live()` yields as an event log to append | each yield **replaces** the current answer; yield current state first |
|
|
967
1257
|
| `<article innerHTML={html()}>…children…</article>` | `innerHTML` **or** children, not both |
|
|
@@ -975,6 +1265,7 @@ how to reuse. Prefer the form on the right.
|
|
|
975
1265
|
| `renderToString(() => <App />)` for an async or lazy-route tree | `await renderToStream(() => <App />)` (one consumer: `pipe` / `pipeTo` / `readable`). String render emits `<Loading>` fallbacks |
|
|
976
1266
|
| `clientOnly(() => import("./Map"))` for a rarely shown widget | same + `{ lazy: true }` so the import waits until first render (default starts at declaration) |
|
|
977
1267
|
| `<NoHydration><Chart /></NoHydration>` to skip SSR of a widget | `clientOnly(() => import("./Chart"))` — `NoHydration`/`Hydration` split ownership, they do not choose visible content |
|
|
1268
|
+
| `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
1269
|
| `await flush()` / `flush()` to wait on a pending memo | `await resolve(() => value())` (or Testing Library async queries); `flush()` only drains staged *sync* writes |
|
|
979
1270
|
| `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
1271
|
| `createContext` + a root provider for a true app singleton | module-level signal/store (SSR: shared across requests). Context is subtree scoping |
|
|
@@ -990,6 +1281,8 @@ how to reuse. Prefer the form on the right.
|
|
|
990
1281
|
| `const View = tab() ? A : B; return <View />` | `const View = dynamic(() => (tab() ? A : B))` — the ternary at setup freezes the choice |
|
|
991
1282
|
| `<For each={resolved()}>` after `children()` | `{resolved.toArray()}` — resolved children are not a reactive data list |
|
|
992
1283
|
| `<input onChange={...}>` for keystrokes | `onInput` — native `onChange` is blur/commit |
|
|
1284
|
+
| debounce as an effect copying one signal into another | debounce at the handler: `onInput={debounce((e) => setQuery(e.currentTarget.value), 150)}` |
|
|
1285
|
+
| `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
1286
|
| `createOptimistic` for a local edit draft | writable derivation / plain store. Optimistic is for an in-flight mutation |
|
|
994
1287
|
| `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
1288
|
| `setSubmitted(true)` + an effect that watches it | do the work in the handler or an `action` |
|
|
@@ -997,6 +1290,9 @@ how to reuse. Prefer the form on the right.
|
|
|
997
1290
|
| `action={"/_server/" + id}` in app JSX | `action={createTodo.url}` or a router `action` |
|
|
998
1291
|
| `<form method="get" action={update.url}>` for a mutation | GET forms only for idempotent search; mutations are POST |
|
|
999
1292
|
| `window.location.href = ...` / `history.pushState` | `useNavigate()` or `<a href={Router.paths...}>` |
|
|
1293
|
+
| `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 |
|
|
1294
|
+
| `new URLSearchParams(location.search)` hand-rolled query parsing | `useSearchParams()` (setter merges, no scroll); typed via a route `search` schema + `useSearchParams(Router.paths.search)` |
|
|
1295
|
+
| `Router.paths.about()` / string-concatenated URLs | static nodes are properties (`Router.paths.about`); calls bind params, then search object and hash |
|
|
1000
1296
|
| `httpStatus(404)` in a click handler | call it bare in the component / error-fallback body (a scope declaration, not a mutation) |
|
|
1001
1297
|
| `try { user() } catch (e) { /* NotReadyError */ }` | `<Loading>` — app code should not catch `NotReadyError` |
|
|
1002
1298
|
| `{latest(() => results())}` as the visible list | settled read + `isPending()` for the indicator; `latest` is for previews |
|
|
@@ -1015,7 +1311,10 @@ how to reuse. Prefer the form on the right.
|
|
|
1015
1311
|
| `Object.assign({}, props)` / expecting `merge` to skip `undefined` like 1.x `mergeProps` | `omit` / `merge` — explicit `undefined` **overrides** (Object.assign) |
|
|
1016
1312
|
| `isPending(user())` / `latest(user())` | `isPending(user)` / `latest(user)` — pass the accessor (or `() => user()`). Calling it first evaluates the read before the helper runs |
|
|
1017
1313
|
| `isPending(...)` as the first-load spinner | `<Loading>` owns first flight; `isPending` is the *refetch* indicator after a settled answer exists |
|
|
1314
|
+
| a held write with no feedback anywhere | pair it with `isPending()` / `latest()` / an optimistic value / `affects()` — otherwise dev + attribution reports `[SILENT_HOLD]` |
|
|
1018
1315
|
| `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 |
|
|
1316
|
+
| `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 |
|
|
1317
|
+
| `submission.clear()` from an effect | on dismiss/resubmit only — from an effect, errors flash and vanish |
|
|
1019
1318
|
| `revalidate(liveSource)` | `live()` updates through the open stream; do not revalidate it |
|
|
1020
1319
|
| `refresh(user)` without `affects` when the reload should look pending | pair `affects(user)` with `refresh(user)` — a bare `refresh` re-asks quietly |
|
|
1021
1320
|
| `<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 +1469,17 @@ always-applied rules installed alongside this skill.
|
|
|
1170
1469
|
is a writable derivation (`createSignal(() => { source(); return initial; })`), not
|
|
1171
1470
|
an effect calling the setter.
|
|
1172
1471
|
- [ ] Event handlers wrap setters (`onClick={() => setX(...)}`), use `currentTarget`,
|
|
1173
|
-
and `onInput` for keystrokes.
|
|
1472
|
+
and `onInput` for keystrokes. Debounce at the handler, never as an effect copying
|
|
1473
|
+
signals. `innerHTML` is sanitized and not mixed with children.
|
|
1174
1474
|
- [ ] Async memos are passed as values (`user={user()}`); JSX props are lazy.
|
|
1175
1475
|
Types stay `User` — not `Promise<User>` / `Accessor<User>`. `<Loading>` wraps
|
|
1176
1476
|
the *read*, not the memo, not chrome. Extracting `const u = user()` at the
|
|
1177
1477
|
parent is the read that throws. Nested child fetches run in parallel
|
|
1178
1478
|
(components do not suspend); a real waterfall is `fetchB(a().id)`.
|
|
1179
1479
|
`isPending` is per-expression (`isPending(() => props.story)` works);
|
|
1180
|
-
`latest(selectedId)` is opt-in, not the default highlight.
|
|
1480
|
+
`latest(selectedId)` is opt-in, not the default highlight. Inputs feeding a request
|
|
1481
|
+
bind `latest(query)`; failures throw to `<Errored>`, never `{ success: false }`
|
|
1482
|
+
result objects checked at every read.
|
|
1181
1483
|
- [ ] Lists via `<For>` (server/refetched rows keyed by stable id), conditionals via
|
|
1182
1484
|
ternary/`<Show>`; no `{list().map(...)}` in JSX; no `key` props; no `value()!`.
|
|
1183
1485
|
- [ ] Effects are two-phase and only at imperative boundaries; apply does not read stores.
|
|
@@ -1198,8 +1500,8 @@ always-applied rules installed alongside this skill.
|
|
|
1198
1500
|
is static `dist/client`. Do not delay `renderToStream` for visual order
|
|
1199
1501
|
(`<Reveal>` does).
|
|
1200
1502
|
- [ ] External collections reconcile into stores (function-form `createStore` /
|
|
1201
|
-
`reconcile`), never wholesale draft assignment
|
|
1202
|
-
|
|
1503
|
+
`reconcile`), never wholesale draft assignment of a fresh tree
|
|
1504
|
+
(`setTodos((t) => t.filter(...))` for removal is fine — survivors keep identity).
|
|
1203
1505
|
- [ ] Rest props via `omit`, not `{ ...props }`. Conditional `class` via objects, not string concat.
|
|
1204
1506
|
- [ ] Input filters write back to the DOM on reject (inputs do not rewind).
|
|
1205
1507
|
- [ ] No Solid 1.x imports or APIs (`solid-js/store`, `createResource`, `onMount`, `Suspense`, ...).
|
|
@@ -1207,12 +1509,25 @@ always-applied rules installed alongside this skill.
|
|
|
1207
1509
|
`solid-js`; DOM `JSX` types come from `@solidjs/web`.
|
|
1208
1510
|
- [ ] Inspect children with `children()`; code-split with `lazy` + `<Loading>`; reactive
|
|
1209
1511
|
component choice with `dynamic()`. No `React.lazy`, no effects inside ref callbacks.
|
|
1210
|
-
- [ ] Browser-only code uses `isServer` / `clientOnly
|
|
1512
|
+
- [ ] Browser-only code uses `isServer` / `clientOnly` (components) / `ssrSource`
|
|
1513
|
+
(values), not `typeof window`.
|
|
1211
1514
|
Component teardown is `onSettled` (return cleanup), not `onCleanup`.
|
|
1515
|
+
- [ ] Dev console shows no diagnostics: reads in tracking scopes (no
|
|
1516
|
+
`[STRICT_READ_UNTRACKED]`), held writes paired with `isPending` / `latest` /
|
|
1517
|
+
optimistic values / `affects()` (no `[SILENT_HOLD]`). Attribution-only costs
|
|
1518
|
+
(`[IMMUTABLE_UPDATE_IN_STORE]`, `[UNSTABLE_LIST_IDENTITY]`, `[EFFECT_*]`)
|
|
1519
|
+
checked via `attribution` from `solid-js/attribution` when touching stores,
|
|
1520
|
+
lists, or effects. Name interrogated scopes (`{ name: "total" }`).
|
|
1212
1521
|
- [ ] If the project has a router: `createRouter({ routes })`, not JSX `<Route>` / `<A>`.
|
|
1213
1522
|
One instance, no nested `<Router>`. Navigate with `useNavigate` / `<a href>`, not
|
|
1214
1523
|
`window.location`. Router `action`/`query` come from `@solidjs/router` (POST forms + cache), not
|
|
1215
|
-
core `action`/`refresh`. Forms: `<form action={save} method="post">`.
|
|
1524
|
+
core `action`/`refresh`. Forms: `<form action={save} method="post">`. Core `action`
|
|
1525
|
+
bodies are generators (`function*` / `async function*`) suspending on `yield`,
|
|
1526
|
+
never plain `async` functions. Link state is automatic attributes + CSS
|
|
1527
|
+
(`aria-current` / `data-active` / `data-pending`; `useLinkState` / `useIsRouting`
|
|
1528
|
+
in JSX). Search params via `useSearchParams`. Forms validate on the server
|
|
1529
|
+
(`throw respond({ issues }, { status: 400 })`) with inline errors from
|
|
1530
|
+
`useSubmissions`.
|
|
1216
1531
|
- [ ] `render(() => <App />, root)` — a function, not `render(<App />)`. `hydrate` when
|
|
1217
1532
|
HTML already exists. Stream async/lazy trees (`await renderToStream(...)`) with
|
|
1218
1533
|
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.1",
|
|
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",
|