solid2-agent-kit 0.8.1 → 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 +33 -3
- package/files/shared/agents-section.md +1 -1
- package/files/shared/rules-body.md +108 -24
- package/files/skills/solid-2/SKILL.md +562 -61
- package/files/skills/solid-2/references/official-docs.md +22 -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,9 +17,39 @@ 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
|
-
-
|
|
21
|
-
|
|
22
|
-
|
|
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) => ...`);
|
|
23
|
+
- `<Loading>`/`<Errored>` wrapping page chrome instead of the data slot, or
|
|
24
|
+
`const u = user()` extracted then passed (a real parent-side read). Passing
|
|
25
|
+
`user={user()}` is the colorless form — do not "fix" it into accessors or
|
|
26
|
+
`Promise<User>` props;
|
|
27
|
+
- `latest(selectedId)` as the default highlight (hold is the default);
|
|
28
|
+
`isPending` treated as a global spinner instead of a per-expression question;
|
|
29
|
+
- treating `<Errored>` as a terminal ErrorBoundary, or routing per-row mutation
|
|
30
|
+
failures through it (those belong in the action / a projection-folded map);
|
|
31
|
+
- nested fetches assumed to waterfall, or `<Loading>` lifted along with a lifted fetch;
|
|
32
|
+
- rewriting App with loading/error branches, or snapshot/restore, when wrapping
|
|
33
|
+
a client store in server functions; disabling optimistic rows until ack;
|
|
34
|
+
- tRPC / type-gen around `"use server"`, a client `fetch` after a mutation,
|
|
35
|
+
refetch in `hydrate`/`onSettled`, a custom Worker/Express adapter, or
|
|
36
|
+
delaying `renderToStream` for visual order (`<Reveal>` owns display);
|
|
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;
|
|
23
53
|
- context values passed as snapshots instead of accessors/setters/stores;
|
|
24
54
|
- components with conditional/early returns on reactive values.
|
|
25
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 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).
|
|
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,28 +74,51 @@ 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
|
-
10. **Async data is an async computation**: `createMemo(async () => ...)`
|
|
79
|
-
`<Loading>` / `<Errored
|
|
86
|
+
10. **Async data is an async computation**: `createMemo(async () => ...)` (or
|
|
87
|
+
`createMemo(() => fetchUser(id()))`) read under `<Loading>` / `<Errored>`.
|
|
88
|
+
**Fetch high, block low** — creating/reading are like sync; consuming (await)
|
|
89
|
+
and blocking (the boundary) are independent. Create the memo early, wrap only
|
|
90
|
+
the read with `<Loading>`. Do not glue the boundary to the memo, and do not
|
|
91
|
+
retarget intermediate props to `Promise<User>` / `Accessor<User>` — types stay
|
|
92
|
+
`User`. Nested trees do not suspend: they fetch in parallel; a real waterfall
|
|
93
|
+
looks like `fetchB(a().id)`. JSX component props are lazy (passing isn't reading):
|
|
94
|
+
`<Child user={user()} />` then `{props.user.name}` under the child's
|
|
95
|
+
`<Loading>` is the colorless form. `const u = user()` at the parent *is* a
|
|
96
|
+
read and throws/snapshots there. Pass the memo itself only when the child
|
|
97
|
+
must `refresh()` that source — `isPending(() => props.user)` works on the
|
|
98
|
+
value. Read every reactive input **before the first
|
|
80
99
|
`await`** — post-`await` reads do not subscribe, and in production the computation can
|
|
81
100
|
sit pending with no retry. No `useEffect` + `setState` fetching, no `createResource`.
|
|
82
101
|
`<Loading>` wraps the data slot, not page chrome. After first paint it keeps content
|
|
83
102
|
during refetch (`isPending` for the indicator). Use `on={id()}` (the *value*, not the
|
|
84
|
-
accessor) only when that identity change should show the fallback again.
|
|
85
|
-
memo to a child as the **accessor** (`user={user}`), and read `props.user()` under that
|
|
86
|
-
child's `<Loading>` — `user={user()}` throws not-ready at the parent, so the child's
|
|
87
|
-
boundary never sees it. Do not start `fetch` (or any request) at component-body top
|
|
103
|
+
accessor) only when that identity change should show the fallback again. Do not start `fetch` (or any request) at component-body top
|
|
88
104
|
level — that runs once at mount and is not a reactive source. Do not `try/catch` `NotReadyError` around a read, and do not
|
|
89
105
|
use `loadingValue` / `seedLoadingValue` as the default first-flight UI — those skip
|
|
90
|
-
`<Loading>`. `{latest(() => x())}` is a preview, not the visible answer.
|
|
91
|
-
|
|
106
|
+
`<Loading>`. `{latest(() => x())}` is a preview, not the visible answer.
|
|
107
|
+
Default navigation holds: keep `selectedId()` so highlight and content stay
|
|
108
|
+
consistent; `latest(selectedId)` only when the highlight should move first.
|
|
109
|
+
Pair `class={{ pending: isPending(selectedId) }}` with a short CSS
|
|
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.
|
|
112
|
+
`isPending` is a per-expression question, not a global spinner: ask it of the
|
|
113
|
+
async source, a derived prop (`isPending(() => props.story)`), or the write
|
|
114
|
+
(`isPending(selectedId)`), even when the fetch lives in a child. It is
|
|
115
|
+
the *refetch* indicator after a settled answer exists — do not use it as the
|
|
92
116
|
first-load spinner (`<Loading>` owns that). Pass the accessor: `isPending(user)`,
|
|
93
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()`.
|
|
94
122
|
"Prop with local edits" = writable derivation:
|
|
95
123
|
`createSignal(() => props.value)` or `createStore(() => props.value, fallback)`.
|
|
96
124
|
`createOptimistic` is for an in-flight mutation, not a local editing session.
|
|
@@ -143,13 +171,26 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
143
171
|
integration layers. Model async as a computation (return a promise or async iterable
|
|
144
172
|
from a memo/store) and let the framework own the states: first load = `<Loading>`
|
|
145
173
|
fallback; refetch indicator = `isPending()`; errors = thrown into the graph and caught
|
|
146
|
-
by `<Errored>`; speculative value = `latest()`.
|
|
147
|
-
|
|
174
|
+
by `<Errored>`; speculative value = `latest()`. `<Errored>` is graph status, not a
|
|
175
|
+
terminal React ErrorBoundary: it heals when the source succeeds again (refresh,
|
|
176
|
+
live reconnect, input change). `reset` retries the collected *sources*, not a UI
|
|
177
|
+
remount. Per-row mutation failures belong in an errors map the projection folds
|
|
178
|
+
in (survives the optimistic overlay), not in `<Errored>` around the whole list.
|
|
179
|
+
Making a client store "real" is additive: same setters, wrap mutations in
|
|
180
|
+
`action`, swap `createOptimisticStore` and a file of server functions — do
|
|
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.
|
|
185
|
+
Mechanically enforced by `solid2-kit check` (loading-signal naming, zero-arg-call undefined checks).
|
|
148
186
|
19. **Refetch is `refresh(source)`** — rarely needed, but when it is, never fake it with a
|
|
149
187
|
counter/version signal read inside the computation. Input-driven refetch is automatic
|
|
150
188
|
(tracked inputs re-run the computation) and subscriptions push. Legit uses: after a
|
|
151
189
|
mutation inside an `action`, and explicit reload buttons. Pair with `affects(source)`
|
|
152
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).
|
|
153
194
|
These are three different APIs: core `refresh(source)` reruns a reactive source;
|
|
154
195
|
router `revalidate(getUser.key)` invalidates the query cache; server-function
|
|
155
196
|
`return reload({ revalidate: "todos" })` asks the integration to refresh cached
|
|
@@ -169,7 +210,14 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
169
210
|
`postMessage` / logs via `snapshot(store)` (proxies fail or leak reactivity); render
|
|
170
211
|
modals/tooltips/overlays through `<Portal>` from `@solidjs/web`; mutations whose writes
|
|
171
212
|
cross an async gap default to `action` + `createOptimistic`/`createOptimisticStore` —
|
|
172
|
-
|
|
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
|
|
216
|
+
`pending` on the record rather than a second copy of state. Do not snapshot
|
|
217
|
+
and restore, disable optimistic rows until ack, mutex rapid clicks, or freeze
|
|
218
|
+
unrelated writes because one action is in flight; failed-action replay is
|
|
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
|
|
173
221
|
state after mutations. Compiler: `"jsxImportSource": "@solidjs/web"` (not `"solid-js"`);
|
|
174
222
|
Vite plugin is `@solidjs/vite-plugin` (not `vite-plugin-solid`) — run `solid2-kit doctor`
|
|
175
223
|
after touching `package.json` / tsconfig / root configs; it fails on React and Solid 1.x
|
|
@@ -179,6 +227,11 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
179
227
|
`clientOnly` starts loading at declaration; `{ lazy: true }` defers until first
|
|
180
228
|
render. `NoHydration` / `Hydration` split hydration *ownership*; they do not choose
|
|
181
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.
|
|
182
235
|
Pass a **function** to `render` / `hydrate` / `renderToString` / `renderToStream`
|
|
183
236
|
(`render(() => <App />, root)`), never `render(<App />, root)`. `hydrate` when the
|
|
184
237
|
container already holds SSR HTML; `render` for an empty mount. Client mode (the
|
|
@@ -214,14 +267,20 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
214
267
|
at module scope — not JSX `<Route>` / `<A>` / `<HashRouter>`. That package's `action` /
|
|
215
268
|
`query` are URL-addressable POST forms and a read cache — not core `action` /
|
|
216
269
|
`refresh` from `solid-js`. Router mutations: `<form action={save} method="post">`
|
|
217
|
-
(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
|
|
218
274
|
reads: `query(fn, "name")` then `createMemo(() => getUser(id()))`; after a mutation
|
|
219
275
|
`revalidate(getUser.key)`. Do not wrap mutations in `query` (an undeclared server
|
|
220
276
|
function becomes GET). `Router.paths` is not the current location (`useLocation` /
|
|
221
|
-
`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 —
|
|
222
279
|
Solid Router does not support nested `<Router>` / nested `createRouter`.
|
|
223
280
|
In-app navigation is `useNavigate` or `<a href={Router.paths...}>`, not
|
|
224
|
-
`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
|
|
225
284
|
`getRequestEvent()`, never a caller-supplied user id. Unscripted forms use
|
|
226
285
|
the function `.url` (or a router `action`), not a hand-built `/_server/`
|
|
227
286
|
path. Client history adapters do not select the SSR URL — pass
|
|
@@ -230,11 +289,36 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
230
289
|
`<Title>` / `<Meta>` from `@solidjs/meta` with **no** `MetaProvider`. A static
|
|
231
290
|
`<title>` in the document shell is the fallback; do not hardcode other tags
|
|
232
291
|
(`<meta name="description">`) that Solid Meta should manage.
|
|
233
|
-
Return `respond(value, { status })` from a `"use server"` function, not
|
|
292
|
+
Return `respond(value, { status, headers })` from a `"use server"` function, not
|
|
234
293
|
`Response.json` — a raw `Response` is HTTP-handler control flow; scripted
|
|
235
|
-
callers should receive the value.
|
|
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 —
|
|
300
|
+
validate inside the function; do not invent tRPC / RPC type-gen.
|
|
301
|
+
Locals only the `"use server"` body reads (db, secrets) stay off the client.
|
|
302
|
+
Do not add an API-route file just to wrap a database call — `"use server"`
|
|
303
|
+
is the RPC. During SSR the call is in-process (no HTTP). After a mutation, `reload` /
|
|
304
|
+
`revalidate` / single-flight — never a client `fetch` or an `onSettled` /
|
|
305
|
+
`hydrate` refetch to "refresh the page". In-flight Promises serialize as
|
|
306
|
+
Promises; `live()` embeds the first value in HTML and continues the stream on
|
|
307
|
+
the client. Do not delay `renderToStream` for visual order (`<Reveal>` owns
|
|
308
|
+
display). Do not write subscribe/unsubscribe in the component around `live()`.
|
|
309
|
+
When the app has a server bundle (`ssr` or server functions), production is
|
|
310
|
+
`handleRequest(request)` from the built `dist/server/server.js` (or its
|
|
311
|
+
Fetchable `fetch`); platform Vite plugins adopt that handler (Node: the
|
|
312
|
+
template `server.js`). Client-only start mode is static `dist/client` —
|
|
313
|
+
there is no handler to wrap. Request middleware is
|
|
314
|
+
`start: { middleware: "./src/middleware.ts" }`, not Express `app.use`. Do not return
|
|
315
|
+
components from `"use server"` unless the project already enabled the
|
|
316
|
+
experimental `serverFunctions.components` flag. JSON-encodable server-function arguments
|
|
236
317
|
only, unless `enableRichArguments()` was called once in the client entry
|
|
237
|
-
(`Date` / `Map` / `Set` throw without it).
|
|
318
|
+
(`Date` / `Map` / `Set` throw without it). `live()` connection state is
|
|
319
|
+
`source.onstatus` (`"connected"` / `"reconnecting"` / `"closed"`), never a
|
|
320
|
+
field in the yielded value. Sibling `<Loading>` reveal order is `<Reveal>`
|
|
321
|
+
(`collapsed` suppresses tail skeletons under sequential order).
|
|
238
322
|
|
|
239
323
|
## Lint heritage: eslint-plugin-solid (Solid 1.x) intents carried into this file
|
|
240
324
|
|
|
@@ -287,7 +371,7 @@ directory (default `src/`).
|
|
|
287
371
|
| `renderToStringAsync(...)` | `await renderToStream(() => <App />)` (one consumer: `pipe` / `pipeTo` / `readable`) |
|
|
288
372
|
| `clearDelegatedEvents` | delete — delegated listeners are scoped to each render root |
|
|
289
373
|
| `vite-plugin-solid` / `jsxImportSource: "solid-js"` | `@solidjs/vite-plugin` / `"jsxImportSource": "@solidjs/web"` |
|
|
290
|
-
| 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) |
|
|
291
375
|
| `createAsync` / `createAsyncStore` / `useSubmission` / router `json()` / `cache()` | `createMemo(() => getUser(id()))`, `useSubmissions`, `respond()` from `@solidjs/web`, `query` |
|
|
292
376
|
| `import ... from "@solidjs/start"` / `vinxi` / `h3` / `"use client"` | `GET` from `@solidjs/web/server-functions`; Solid has no `"use client"` |
|
|
293
377
|
| `render(<App />, root)` | `render(() => <App />, root)` (same for `hydrate` / `renderToString` / `renderToStream`) |
|