solid2-agent-kit 0.4.2 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,7 +16,7 @@ Supported agents: **Cursor** and **Claude Code**.
16
16
  | Always-applied hard rules | `.cursor/rules/solid-2.mdc` (glob-attached to `*.tsx`/`*.jsx`) | managed block in `CLAUDE.md` | 23 hard rules + banned Solid 1.x API table + eslint-plugin-solid lint-heritage table. Injected without the agent having to decide to read anything |
17
17
  | Agent skill | `.cursor/skills/solid-2/` | `.claude/skills/solid-2/` | Execution-model primer, decision tables, canonical patterns, a “still runs / write the other form” table, review checklist, official-doc URL index |
18
18
  | Shared agent context | managed block in `AGENTS.md` | same (Claude Code users can reference it from `CLAUDE.md`) | Core principles and pointers, always in context |
19
- | Mechanical gate | `solid2-kit check` | same | Whole-file regex guard over `src/**/*.{ts,tsx}`: fails on props destructuring (including multi-line signatures), `{ ...props }` rest copies, `{list().map(...)}` in JSX, React imports/hooks/JSX props/`React.lazy`, Solid 1.x imports/APIs/components/JSX namespaces, `vite-plugin-solid`, old-router `<Route>`/`<HashRouter>`, `MetaProvider`, `Context.Provider`, camelCase style keys, `key` props, `value()!` hand-narrowing, hand-rolled loading signals, and `=== undefined` readiness branches |
19
+ | Mechanical gate | `solid2-kit check` | same | Whole-file regex guard over `src/**/*.{ts,tsx}`: 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), and `<Dynamic>` (use `dynamic()`) |
20
20
 
21
21
  Managed blocks are delimited with `<!-- solid2-agent-kit:*:start/end -->` markers; everything
22
22
  outside them is yours. Kit-owned files (`solid-2.mdc`, the skill directories) are overwritten
@@ -24,8 +24,8 @@ on sync.
24
24
 
25
25
  ## Usage
26
26
 
27
- GitHub is the canonical distribution channel (the kit is intentionally not published to
28
- npm; consumers pin a commit through their lockfile):
27
+ GitHub is the canonical distribution channel (consumers pin a commit through their
28
+ lockfile); the package is also published to npm as `solid2-agent-kit`:
29
29
 
30
30
  ```sh
31
31
  # install guidance for both Cursor and Claude Code (default)
@@ -55,8 +55,8 @@ resolve the project-local bin, so the bare name is safe there):
55
55
  ```
56
56
 
57
57
  Update later with `bun update solid2-agent-kit` (or the npm/pnpm equivalent) followed by
58
- `npx solid2-agent-kit sync`. Outside a project that has the kit installed, always use the
59
- `npx github:...` form — the bare package name is not claimed on the npm registry.
58
+ `npx solid2-agent-kit sync`. Outside a project that has the kit installed, prefer the
59
+ `npx github:...` form — it always tracks this repo directly.
60
60
 
61
61
  ## Why not eslint-plugin-solid?
62
62
 
@@ -96,6 +96,14 @@ scripts/check-docs-drift.mjs weekly docs cross-check
96
96
  tests/ mechanical-gate fixtures (`npm test`)
97
97
  ```
98
98
 
99
+ ## Publishing (npm)
100
+
101
+ Bump `version` in `package.json` on `main` and CI publishes automatically
102
+ (`.github/workflows/publish.yml`) — it skips when the version already exists on npm.
103
+ Auth is npm Trusted Publishing (OIDC), so no NPM_TOKEN secret exists or is needed.
104
+ One-time setup on npm: package → Settings → Trusted Publisher → GitHub Actions, owner
105
+ `lightsound`, repo `solid2-agent-kit`, workflow `publish.yml`, environment left empty.
106
+
99
107
  ## License
100
108
 
101
109
  MIT
@@ -177,7 +177,7 @@ const CHECKS = [
177
177
  {
178
178
  id: 'react-hook',
179
179
  pattern:
180
- /(?<![.\w])use(?:State|Effect|Memo|Ref|Callback|Reducer|LayoutEffect|Transition|SyncExternalStore|ImperativeHandle|Id|DeferredValue|InsertionEffect)\s*\(/g,
180
+ /(?<![.\w])use(?:State|Effect|Memo|Ref|Callback|Reducer|LayoutEffect|Transition|SyncExternalStore|ImperativeHandle|Id|DeferredValue|InsertionEffect|Optimistic|ActionState|FormStatus)\s*\(/g,
181
181
  message: 'React hook. See the primitive mapping in the solid-2 rules.',
182
182
  },
183
183
  {
@@ -281,15 +281,68 @@ const CHECKS = [
281
281
  },
282
282
  {
283
283
  id: 'solid1-router',
284
- pattern: /<(?:HashRouter|MemoryRouter|Route|Navigate)\b/g,
284
+ pattern: /<(?:HashRouter|MemoryRouter|Route|Navigate|A|FileRoutes|StartClient|StartServer)\b/g,
285
285
  message:
286
- 'Solid Router 0.x/1.x JSX. Define routes with createRouter({ routes }) and plain <a href={Router.paths...}>.',
286
+ 'Solid Router 0.x/1.x or SolidStart JSX. Define routes with createRouter({ routes }) / fileRoutes(pageRoutes) and plain <a href={Router.paths...}>.',
287
287
  },
288
288
  {
289
289
  id: 'meta-provider',
290
290
  pattern: /<MetaProvider\b/g,
291
291
  message: 'Solid Meta 1.x has no provider. Render <Title>/<Meta>/<Link> from "@solidjs/meta" anywhere.',
292
292
  },
293
+ {
294
+ id: 'solidstart-import',
295
+ pattern: /from\s+['"](?:@solidjs\/start|vinxi|h3|nitropack)(?:['"]|\/)/g,
296
+ message:
297
+ 'SolidStart leftover. Import GET from "@solidjs/web/server-functions"; routes are createRouter / fileRoutes(pageRoutes), not @solidjs/start, vinxi, h3, or nitropack.',
298
+ },
299
+ {
300
+ id: 'solidstart-api',
301
+ pattern: /(?<![.\w])(?:createAsync|createAsyncStore|useSubmission|defineEventHandler)\s*\(/g,
302
+ message:
303
+ 'SolidStart / Router 0.x leftover. Read query() through createMemo(() => getUser(id())); settled results via useSubmissions (plural); replace cache()/json() with query/respond; rewrite H3 handlers against Request/Response.',
304
+ },
305
+ {
306
+ id: 'use-client',
307
+ pattern: /['"]use client['"]/g,
308
+ message: 'React/Next client directive. Solid has no "use client"; server functions use "use server" only.',
309
+ },
310
+ {
311
+ id: 'next-import',
312
+ pattern: /from\s+['"]next(?:['"]|\/)/g,
313
+ message: 'Next.js import in a Solid 2.0 codebase. Use @solidjs/router, @solidjs/meta, and @solidjs/web.',
314
+ },
315
+ {
316
+ id: 'render-jsx-element',
317
+ pattern: /\b(?:render|hydrate|renderToString|renderToStream)\(\s*</g,
318
+ message:
319
+ 'Pass a function to render/hydrate/renderToString/renderToStream so Solid creates the root first: render(() => <App />, root).',
320
+ },
321
+ {
322
+ id: 'typeof-window',
323
+ pattern: /typeof\s+window\b/g,
324
+ message:
325
+ '`typeof window` is not the SSR boundary. Use isServer / isDev from "@solidjs/web", or clientOnly(() => import("./Widget")).',
326
+ },
327
+ {
328
+ id: 'next-nav',
329
+ pattern:
330
+ /(?<![.\w])(?:useRouter|usePathname|revalidatePath|revalidateTag|notFound|hydrateRoot)\s*\(/g,
331
+ message:
332
+ 'Next.js / React DOM leftover. In-app navigation is useNavigate / <a href={Router.paths...}>; 404 is httpStatus(404); hydrate is hydrate(() => <App />, root).',
333
+ },
334
+ {
335
+ id: 'react-dom-event',
336
+ pattern: /\b(?:onDoubleClick|onKeyPress|suppressHydrationWarning)=/g,
337
+ message:
338
+ 'React DOM prop. Native dblclick is onDblClick; keypress is onKeyDown; Solid has no suppressHydrationWarning.',
339
+ },
340
+ {
341
+ id: 'history-nav',
342
+ pattern: /\b(?:history\.pushState|history\.replaceState|window\.location\.href\s*=)/g,
343
+ message:
344
+ 'Bypass of the router. In-app navigation is useNavigate() or <a href={Router.paths...}>.',
345
+ },
293
346
  {
294
347
  // A zero-arg call compared against undefined/null is a manual async
295
348
  // readiness branch (`data() === undefined ? ... : ...`).
@@ -298,6 +351,20 @@ const CHECKS = [
298
351
  message:
299
352
  'Readiness branch on a zero-arg call. Read async values under <Loading> (first load) / isPending() (refetch) and let errors reach <Errored>. For non-reactive utils, assign to a variable and test that.',
300
353
  },
354
+ {
355
+ // `isPending(user())` / `latest(data())` evaluate the read before the
356
+ // helper runs. `isPending(user)` and `isPending(() => user())` do not match.
357
+ id: 'pending-accessor-call',
358
+ pattern: /(?<![.\w])(?:isPending|latest)\(\s*[A-Za-z_$][\w$]*\(\s*\)/g,
359
+ message:
360
+ 'Pass the accessor to isPending/latest, not the call: isPending(user), not isPending(user()). Calling it first evaluates the read before the helper runs.',
361
+ },
362
+ {
363
+ id: 'dynamic-jsx',
364
+ pattern: /<Dynamic[\s/>]/g,
365
+ message:
366
+ '`<Dynamic>` is a JSX convenience wrapper. Application code should use dynamic(() => ...) from "@solidjs/web" so the component identity stays stable.',
367
+ },
301
368
  ];
302
369
 
303
370
  // .ts / .tsx sources, excluding .d.ts declaration files.
@@ -3,4 +3,4 @@
3
3
  - All TSX in this project is Solid 2.0. Components run **once**; reactivity flows through signals/stores to JSX. React patterns (props destructuring, `className`, per-keystroke `onChange`, state-synced-by-effect, `.map()` lists) and Solid 1.x APIs (`createResource`, `onMount`, `solid-js/store`, `Suspense`, path setters) are bugs here.
4
4
  - Before writing TSX, read the `solid-2` skill (`.cursor/skills/solid-2/SKILL.md` for Cursor, `.claude/skills/solid-2/SKILL.md` for Claude Code): patterns, decision tables, official doc URLs. Hard rules auto-attach from `.cursor/rules/solid-2.mdc` in Cursor and live in a managed block in `CLAUDE.md` for Claude Code.
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`. Do not install `eslint-plugin-solid` (built for Solid 1.x; misreads Solid 2 idioms).
6
- - 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>`.
6
+ - 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`.
@@ -33,8 +33,9 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
33
33
  as an updater.
34
34
  5. `class`, not `className`; `for`, not `htmlFor`. `class` accepts strings, conditional
35
35
  objects, and nested arrays: `class={["btn", { active: selected() }]}`. Put *conditional*
36
- names in an object — `class={`btn ${on() ? "on" : ""}`}` works but rebuilds the whole
37
- string, so Solid cannot add/remove one token. `style` objects use CSS property names
36
+ names in an object — `class={`btn ${on() ? "on" : ""}`}`, `clsx(...)`, and
37
+ `.filter(Boolean).join(" ")` all work but rebuild the whole string, so Solid cannot
38
+ add/remove one token. `style` objects use CSS property names
38
39
  (`"background-color"`, not `backgroundColor`) and numbers do NOT auto-append `px`
39
40
  (`width: `${n}px``).
40
41
  6. **`onInput` for per-keystroke handling.** Solid's `onChange` is the native change event
@@ -77,23 +78,39 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
77
78
  accessor) only when that identity change should show the fallback again. Pass an async
78
79
  memo to a child as the **accessor** (`user={user}`), and read `props.user()` under that
79
80
  child's `<Loading>` — `user={user()}` throws not-ready at the parent, so the child's
80
- boundary never sees it. "Prop with local edits" = writable derivation:
81
+ boundary never sees it. Do not start `fetch` (or any request) at component-body top
82
+ level — that runs once at mount and is not a reactive source. Do not `try/catch` `NotReadyError` around a read, and do not
83
+ use `loadingValue` / `seedLoadingValue` as the default first-flight UI — those skip
84
+ `<Loading>`. `{latest(() => x())}` is a preview, not the visible answer. `isPending`
85
+ is the *refetch* indicator after a settled answer exists — do not use it as the
86
+ first-load spinner (`<Loading>` owns that). Pass the accessor: `isPending(user)`,
87
+ not `isPending(user())` (that evaluates the read before `isPending` runs).
88
+ "Prop with local edits" = writable derivation:
81
89
  `createSignal(() => props.value)` or `createStore(() => props.value, fallback)`.
90
+ `createOptimistic` is for an in-flight mutation, not a local editing session.
82
91
  11. Context: the context object is the provider — `<MyContext value={{ theme, setTheme }}>`
83
92
  (no `.Provider`). Pass accessors/setters/stores through context, never snapshot values.
84
93
  `createContext<T>()` without a default throws when read outside a provider (good) — use
85
94
  that for any reactive payload. `createContext("light")` is only for a primitive static
86
95
  fallback; a dummy default on a store/signal context *runs* and then silently no-ops
87
- without a provider.
96
+ without a provider. Truly app-wide singletons (theme, session, locale) are a
97
+ **module-level** signal/store — Context is subtree scoping, not a React-style
98
+ app store. Module state is shared across SSR requests.
88
99
  12. Refs: `let el!: HTMLDivElement` + `ref={el}` or `ref={(node) => (el = node)}`, or forward
89
100
  `props.ref`. Compose with arrays: `ref={[props.ref, (node) => (el = node)]}`. No
90
101
  `useRef`/`.current`. Ref callbacks run untracked and **without an owner**; their
91
102
  return values are ignored. Do not create effects or register cleanup inside a ref
92
103
  callback. For reusable directives, create owned primitives (`onSettled`) in a
93
- factory and return only the element callback.
104
+ factory and return only the element callback. Component setup/teardown is
105
+ `onSettled(() => { ...; return cleanup })` — `onCleanup` is for custom
106
+ primitives and captured owners, not component bodies. Each `onSettled`
107
+ registers a **single** fire (reads inside are untracked); ongoing
108
+ imperative work is `createEffect`.
94
109
  13. **Writes are staged** and commit on the next microtask. Event handlers need nothing
95
110
  special; tests and imperative integration code must call `flush()` before observing
96
- updated state or DOM. Never call `flush()` inside an `action`.
111
+ updated *synchronous* state or DOM. Waiting on an async memo is
112
+ `await resolve(() => value())` (or Testing Library async queries), not `flush()`.
113
+ Never call `flush()` inside an `action`.
97
114
  14. Do not port these React tools — they have no Solid equivalent because the problems they
98
115
  solve don't exist: `useCallback`, `React.memo`, `forwardRef`, `useSyncExternalStore`,
99
116
  dependency arrays, `startTransition`/`useTransition` (updates are held and coordinated
@@ -101,7 +118,8 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
101
118
  15. **Components return once.** Never early-return or conditionally return based on reactive
102
119
  values — the branch is chosen once at setup and frozen forever. Put conditionals inside
103
120
  JSX (`<Show>`, ternary, `<Switch>`/`<Match>`). Early returns on genuinely non-reactive
104
- values (build-time config, missing env) are fine.
121
+ values (build-time config, missing env) are fine. `<Switch>` is first-wins: later
122
+ truthy `<Match>`es are skipped, not “all matching branches”.
105
123
  16. Prefer `textContent` for text-only content. Use `innerHTML` only for trusted or
106
124
  sanitized markup — never interpolate user input into it, never React's
107
125
  `dangerouslySetInnerHTML`, and never combine `innerHTML` with JSX children (they fight
@@ -111,6 +129,8 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
111
129
  **narrowed accessor**, so no `!` is needed:
112
130
  `<Show when={error()}>{(err) => <p>{err().message}</p>}</Show>`. For non-reactive
113
131
  zero-arg calls, use an explicit guard variable instead of `!`.
132
+ Do not add `keyed` by default (the React `key` reflex) — default `Show`/`Match`
133
+ keep children mounted; `keyed` remounts when identity changes.
114
134
  Mechanically enforced by `solid2-kit check`.
115
135
  18. **Never hand-roll async UI state.** No `[loading, setLoading]` signals, no
116
136
  `data() === undefined` readiness branches, no `{ data, error }` signal pairs from
@@ -123,7 +143,11 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
123
143
  counter/version signal read inside the computation. Input-driven refetch is automatic
124
144
  (tracked inputs re-run the computation) and subscriptions push. Legit uses: after a
125
145
  mutation inside an `action`, and explicit reload buttons. Pair with `affects(source)`
126
- when the reload should present as pending.
146
+ when the reload should present as pending — a bare `refresh()` re-asks quietly.
147
+ These are three different APIs: core `refresh(source)` reruns a reactive source;
148
+ router `revalidate(getUser.key)` invalidates the query cache; server-function
149
+ `return reload({ revalidate: "todos" })` asks the integration to refresh cached
150
+ data. Do not mix them.
127
151
  20. **External collections flow into stores through reconciliation.** First choice:
128
152
  function-form `createStore(() => source, fallback)` / `createProjection` (reconcile
129
153
  automatically, keyed by `"id"`). Manual merge: `setStore(reconcile(fresh, "id"))` or
@@ -143,10 +167,29 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
143
167
  state after mutations. Compiler: `"jsxImportSource": "@solidjs/web"` (not `"solid-js"`);
144
168
  Vite plugin is `@solidjs/vite-plugin` (not `vite-plugin-solid`). Server vs browser:
145
169
  `isServer` / `isDev` from `@solidjs/web`, or `clientOnly(() => import("./Widget"))` for
146
- browser-only components — never `typeof window` as the SSR boundary. `JSON.stringify(store)`
170
+ browser-only components — never `typeof window` as the SSR boundary. Default
171
+ `clientOnly` starts loading at declaration; `{ lazy: true }` defers until first
172
+ render. `NoHydration` / `Hydration` split hydration *ownership*; they do not choose
173
+ visible content — `clientOnly` is for components that must never run on the server.
174
+ Pass a **function** to `render` / `hydrate` / `renderToString` / `renderToStream`
175
+ (`render(() => <App />, root)`), never `render(<App />, root)`. `hydrate` when the
176
+ container already holds SSR HTML; `render` for an empty mount. `renderToString`
177
+ emits `<Loading>` fallbacks; async or lazy-route trees need
178
+ `await renderToStream(() => <App />)` (one consumer: `pipe` / `pipeTo` / `readable`).
179
+ When the app owns the document, emit `<HydrationScript />` once before app markup;
180
+ extra roots get a distinct `renderId`. Import `GET` from
181
+ `@solidjs/web/server-functions`, never `@solidjs/start`. `httpStatus` /
182
+ `httpHeader` are scope *declarations* (call them bare in a component or
183
+ fallback body), not event-time mutations. `JSON.stringify(store)`
147
184
  / `structuredClone(store)` without `snapshot(store)` can throw or leak proxies. Async SSR
148
185
  without `<Loading>` *works* but blocks the HTML shell until every read settles — wrap
149
- reads, or stream with `renderToStream`.
186
+ reads, or stream with `renderToStream`. `pipe` / `pipeTo` / `readable` each consume
187
+ a stream render — use exactly one. `createRoot` is for tests, libraries, and
188
+ non-render entry points; inside a component let `render` / the component owner
189
+ own the scope. Async reads inside `<Portal>` start on the client — hoist the
190
+ read above the portal. Navigation-shaped updates (a setter, then async computeds holding previous values)
191
+ do not need core `action` — reach for it only when writes happen *after* async work.
192
+ Invoke actions from handlers, not from memos or effects.
150
193
  23. **Composition.** Pass `props.children` through when you only render them. Inspect or
151
194
  iterate children with `children(() => props.children)` (then `.toArray()`), never a
152
195
  setup-time snapshot. Code-split with `lazy(() => import("./X"))` under `<Loading>` —
@@ -155,8 +198,30 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
155
198
  because the named export is not a call-site literal. Select a component from reactive
156
199
  state with `dynamic(() => ...)` from `@solidjs/web`
157
200
  (stable identity). If the project has `@solidjs/router`, routes are `createRouter({ routes })`
158
- at module scope — not JSX `<Route>` / `<A>` / `<HashRouter>`. Document head is
159
- `<Title>` / `<Meta>` from `@solidjs/meta` with **no** `MetaProvider`.
201
+ at module scope — not JSX `<Route>` / `<A>` / `<HashRouter>`. That package's `action` /
202
+ `query` are URL-addressable POST forms and a read cache — not core `action` /
203
+ `refresh` from `solid-js`. Router mutations: `<form action={save} method="post">`
204
+ (POST only; `.with(id)` binds args into the URL). `useAction` is JS-only. Cache
205
+ reads: `query(fn, "name")` then `createMemo(() => getUser(id()))`; after a mutation
206
+ `revalidate(getUser.key)`. Do not wrap mutations in `query` (an undeclared server
207
+ function becomes GET). `Router.paths` is not the current location (`useLocation` /
208
+ `useParams`); a route `preload` result is `props.data`. One router instance —
209
+ Solid Router does not support nested `<Router>` / nested `createRouter`.
210
+ In-app navigation is `useNavigate` or `<a href={Router.paths...}>`, not
211
+ `window.location` / `history.pushState`. Trusted identity is
212
+ `getRequestEvent()`, never a caller-supplied user id. Unscripted forms use
213
+ the function `.url` (or a router `action`), not a hand-built `/_server/`
214
+ path. Client history adapters do not select the SSR URL — pass
215
+ `<Router url={request.url}>` (or rely on the request event). Secrets belong in
216
+ `virtual:env/server`, never the `client` env map / `import.meta.env`. Document head is
217
+ `<Title>` / `<Meta>` from `@solidjs/meta` with **no** `MetaProvider`. A static
218
+ `<title>` in the document shell is the fallback; do not hardcode other tags
219
+ (`<meta name="description">`) that Solid Meta should manage.
220
+ Return `respond(value, { status })` from a `"use server"` function, not
221
+ `Response.json` — a raw `Response` is HTTP-handler control flow; scripted
222
+ callers should receive the value. JSON-encodable server-function arguments
223
+ only, unless `enableRichArguments()` was called once in the client entry
224
+ (`Date` / `Map` / `Set` throw without it).
160
225
 
161
226
  ## Lint heritage: eslint-plugin-solid (Solid 1.x) intents carried into this file
162
227
 
@@ -208,7 +273,10 @@ directory (default `src/`).
208
273
  | `renderToStringAsync(...)` | `await renderToStream(() => <App />)` (one consumer: `pipe` / `pipeTo` / `readable`) |
209
274
  | `clearDelegatedEvents` | delete — delegated listeners are scoped to each render root |
210
275
  | `vite-plugin-solid` / `jsxImportSource: "solid-js"` | `@solidjs/vite-plugin` / `"jsxImportSource": "@solidjs/web"` |
211
- | JSX `<Route>` / `<A>` / `<HashRouter>` / `<Navigate>` | `createRouter({ routes })`, plain `<a href={Router.paths...}>`, `hashHistory()` |
276
+ | JSX `<Route>` / `<A>` / `<HashRouter>` / `<Navigate>` / `<FileRoutes>` | `createRouter({ routes })`, `fileRoutes(pageRoutes)`, plain `<a href={Router.paths...}>`, `hashHistory()` |
277
+ | `createAsync` / `createAsyncStore` / `useSubmission` / router `json()` / `cache()` | `createMemo(() => getUser(id()))`, `useSubmissions`, `respond()` from `@solidjs/web`, `query` |
278
+ | `import ... from "@solidjs/start"` / `vinxi` / `h3` / `"use client"` | `GET` from `@solidjs/web/server-functions`; Solid has no `"use client"` |
279
+ | `render(<App />, root)` | `render(() => <App />, root)` (same for `hydrate` / `renderToString` / `renderToStream`) |
212
280
  | `<MetaProvider>` | no provider — render `<Title>` / `<Meta>` / `<Link>` from `@solidjs/meta` anywhere |
213
281
  | `use:directive`, `on:`/`oncapture:`, `attr:`/`bool:`, `/*@once*/` | `ref` callbacks, camelCase event props, standard attributes, keep values reactive |
214
282
  | `resource.loading` / `resource.error` | `<Loading>` boundary / `<Errored>` boundary |
@@ -217,6 +285,7 @@ When unsure about any API, verify against the official Solid 2.0 docs — fetcha
217
285
  listed in `references/official-docs.md` next to the `solid-2` skill. Do not guess from
218
286
  Solid 1.x or React memory.
219
287
 
220
- `createTrackedEffect` and `storePath` exist in Solid 2 but are not defaults: use two-phase
221
- `createEffect` unless a single tracked callback is required, and draft setters instead of
222
- `storePath` except while converting 1.x path setters.
288
+ `createTrackedEffect`, `createRenderEffect`, and `storePath` exist in Solid 2 but
289
+ are not defaults: use two-phase `createEffect` unless a single tracked callback is
290
+ required, and draft setters instead of `storePath` except while converting 1.x
291
+ path setters.
@@ -67,8 +67,9 @@ Most React `useEffect` code should NOT become `createEffect`:
67
67
  | Inspect, count, or iterate children | `children(() => props.children)` then `.toArray()` |
68
68
  | Code-split a component | `lazy(() => import("./X"))` read under `<Loading>` |
69
69
  | Named export from a lazy module | `lazy(() => import("./pages"), { export: "About" })` |
70
- | Pick a component/tag from reactive state | `dynamic(() => ...)` from `@solidjs/web` (stable identity) |
71
- | Browser-only widget (charts, maps, `window`) | `clientOnly(() => import("./Chart"))` from `@solidjs/web` |
70
+ | Pick a component/tag from reactive state | `dynamic(() => ...)` from `@solidjs/web` (stable identity; prefer over `<Dynamic>`) |
71
+ | Overlay / modal | `<Portal>` — hoist async reads *above* the portal (reads inside start on the client) |
72
+ | Browser-only widget (charts, maps, `window`) | `clientOnly(() => import("./Chart"))` from `@solidjs/web` (`{ lazy: true }` defers the import until first render) |
72
73
  | Server vs browser branch | `isServer` / `isDev` from `@solidjs/web` (build-time constants), not `typeof window` |
73
74
  | SSR-stable `id` / `for` / `aria-*` pairing | `createUniqueId()` |
74
75
 
@@ -149,7 +150,8 @@ the component must **inspect or iterate** them. It returns an accessor with
149
150
  `.toArray()`. The helper call belongs in the component body; the read of
150
151
  `props.children` is inside the accessor (a tracking scope), so this is not a
151
152
  setup-time snapshot. Do not assign `const kids = props.children` and do not
152
- treat resolved children as a reactive data list for `<For>`:
153
+ treat resolved children as a reactive data list for `<For>` —
154
+ render `{resolved.toArray()}`, not `<For each={resolved()}>`:
153
155
 
154
156
  ```tsx
155
157
  import { children, type ParentProps } from 'solid-js';
@@ -197,7 +199,9 @@ const Result = dynamic(() => (detailed() ? Detailed : Compact));
197
199
 
198
200
  `dynamic()` returns a **stable** component whose source can be a component, an
199
201
  intrinsic tag name, a promise, or empty. Prefer it over swapping a component
200
- variable in JSX. `<Dynamic>` is the JSX spelling of the same primitive.
202
+ variable in JSX (`const View = tab() ? A : B` freezes the choice at setup).
203
+ `<Dynamic>` is the JSX spelling of the same primitive; application code should
204
+ use `dynamic()` so the component identity stays stable.
201
205
 
202
206
  ### Two-phase effect (imperative boundary only)
203
207
 
@@ -243,15 +247,18 @@ return (
243
247
  where the memo was created, and not of page chrome (header/nav) that should stay mounted.
244
248
  Boundary placement is purely a UX decision — it does not change when fetches start
245
249
  (no waterfalls: nested components set up and fetch in parallel). After first paint the
246
- boundary keeps settled content during refetch; `isPending(() => results())` is the
250
+ boundary keeps settled content during refetch; `isPending(results)` (pass the accessor,
251
+ not `isPending(results())`) is the
247
252
  indicator. Use `on={query()}` (the **value**, not the accessor `on={query}`) only when
248
253
  that identity should put the fallback back on screen.
254
+ Do not use `isPending` as the first-load spinner.
249
255
  - Read every reactive input **before the first `await`** in an async computation. A read
250
256
  after `await` does not subscribe; production can sit pending with no retry.
251
257
  - `<Errored>` function fallbacks receive an error **accessor** and a `reset` callback:
252
258
  `fallback={(error, reset) => ...}`.
253
- - Refetch: `refresh(results)`. In-flight indicator: `isPending(() => results())`.
254
- Freshest in-flight value for imperative code: `latest(results)`.
259
+ - Refetch: `refresh(results)`. In-flight indicator: `isPending(results)` (or
260
+ `isPending(() => results())`). Freshest in-flight value for a preview:
261
+ `latest(results)`. Do not start `fetch` at component-body top level.
255
262
  - Coordinate reveal order of sibling `<Loading>` boundaries with `<Reveal order="sequential" | "together" | "natural">`.
256
263
 
257
264
  **Never hand-roll these states.** No `[loading, setLoading]` signal, no
@@ -287,7 +294,8 @@ function createSubscriptionQuery<T>(
287
294
  const queue: T[] = [];
288
295
  let failure: unknown;
289
296
  let wake = () => {};
290
- // Component-owned setup: unsubscribe is tied to the caller's owner.
297
+ // Custom primitive: unsubscribe is tied to the caller's owner via onCleanup.
298
+ // Component bodies use onSettled and return cleanup instead.
291
299
  onCleanup(subscribe(
292
300
  (value) => { queue.push(value); wake(); },
293
301
  (error) => { failure = error; wake(); },
@@ -320,11 +328,23 @@ const addTodo = action(function* (todo: Todo) {
320
328
 
321
329
  Ordinary signal/store writes inside an action are held until it settles. Never call
322
330
  `flush()` inside an action. Invoke actions from handlers, not from component/computation bodies.
331
+ Do not set a `submitted` signal and watch it from an effect — that work belongs in the
332
+ handler or the `action`.
333
+
334
+ `createOptimistic` / `createOptimisticStore` are for a tentative value during an
335
+ active mutation, not a local editing session (use a writable derivation or a plain
336
+ store for that).
323
337
 
324
338
  **`yield`, not a bare `await`, is the transaction boundary.** `await api.save()` then
325
339
  `setTodos(...)` *runs*, but the write commits immediately and optimistic rollback is lost.
326
340
  Either `yield saveTodo(todo)` or, when you need a typed result, `const saved = await api.save(); yield; setTodos(...)`.
327
341
 
342
+ Navigation-shaped updates do **not** need core `action`. A plain setter is enough:
343
+ reads pull the async, and downstream async computeds hold previous values until the
344
+ new ones are ready (`isPending` / `latest`). Reach for `action` from `solid-js` only
345
+ when writes happen *after* async work. Router form `action` from `@solidjs/router` is
346
+ a different API (URL + POST) — see [App stack](#app-stack-only-if-the-project-has-these-packages).
347
+
328
348
  ### Lists: `<For>` child signatures per keying mode
329
349
 
330
350
  ```tsx
@@ -380,7 +400,8 @@ Plain ternaries remain fine when no narrowing is involved (`{open() ? <A /> : <B
380
400
  Default `Show` keeps children mounted across truthy changes. Add `keyed` to remount when the
381
401
  value's identity changes (child then receives the raw value, like React's `key` reset).
382
402
  Prefer the default; use `keyed` only when internal state must reset. Multi-branch:
383
- `<Switch>` / `<Match>`.
403
+ `<Switch>` / `<Match>` — **first truthy `<Match>` wins**; later matches are skipped,
404
+ even if they are also truthy.
384
405
 
385
406
  Components must **return once**: never early-return based on a reactive value — the branch
386
407
  is picked at setup and frozen. Early returns on non-reactive values (build-time config, a
@@ -426,7 +447,9 @@ function ThemeButton() {
426
447
 
427
448
  Pass accessors/setters/stores through context — never `value={theme()}` (a dead snapshot).
428
449
  No `useMemo` for the value object, no context splitting: the object is created once and
429
- fine-grained updates flow through the signals inside it.
450
+ fine-grained updates flow through the signals inside it. Context is **subtree scoping**.
451
+ A true app-wide singleton (theme, session, locale) is a module-level signal/store —
452
+ do not invent a root provider for that. Module state is shared across SSR requests.
430
453
 
431
454
  ### Refs
432
455
 
@@ -479,7 +502,9 @@ expect(count()).toBe(2);
479
502
  ```
480
503
 
481
504
  Wait for an async expression to settle in tests: `await resolve(() => value())`.
482
- Run tests in dev mode first — Solid 2 emits diagnostics for top-level reactive reads,
505
+ `flush()` only drains staged **synchronous** writes — it is not a stand-in for waiting
506
+ on a pending resource. `onSettled` is also a **single** fire (untracked): it is not
507
+ `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,
483
508
  writes from owned scopes, and async reads outside `<Loading>`. Fix them; don't suppress.
484
509
 
485
510
  Component tests use `@solidjs/testing-library`. Pass a **function** to `render` so
@@ -554,10 +579,72 @@ export default function App() {
554
579
  ```
555
580
 
556
581
  Create the instance at **module scope**. Nested layouts are `children` arrays
557
- on the route objects, not nested `<Route>` / nested routers. Navigate with
582
+ on the route objects, not nested `<Route>` / nested routers. Solid Router does
583
+ **not** support nested `<Router>` instances — compose one route tree (or a lazy
584
+ `children` thunk). Navigate with
558
585
  plain `<a href={Router.paths.about}>` (or `useNavigate`); there is no `<A>` /
559
- `<Navigate>` / `<HashRouter>`. Session location is `useLocation` / `useParams`,
560
- not `Router.paths`. One router per app.
586
+ `<Navigate>` / `<HashRouter>` / `<FileRoutes />`. Do not assign
587
+ `window.location.href` or call `history.pushState` for in-app navigation.
588
+ Session location is `useLocation` / `useParams`,
589
+ not `Router.paths`. A route `preload` result is `props.data` on the matched
590
+ component (and the factory `preload` result is the root render prop's `props.data`).
591
+ Link warming: `preload="false"` skips that link's *data* preload; `preloadLinks: false`
592
+ disables automatic link preloading. Intent values are `"initial"` / `"navigate"` /
593
+ `"native"` / `"preload"`. One router per app.
594
+
595
+ **Two different `action`s.** Core `action` from `solid-js` is a generator transaction
596
+ (optimistic writes that span an async gap). Router `action` from `@solidjs/router` is
597
+ a URL-addressable POST mutation (`useSubmissions`, `<form action={save} method="post">`).
598
+ Do not import one and use it as the other.
599
+
600
+ ```tsx
601
+ import { action } from '@solidjs/router';
602
+ import { redirect } from '@solidjs/web';
603
+
604
+ const save = action(async (form: FormData) => {
605
+ await saveProfile(form);
606
+ return redirect('/account');
607
+ }, 'update-profile');
608
+
609
+ <form action={save} method="post">
610
+ <input name="displayName" />
611
+ <button type="submit">Save</button>
612
+ </form>
613
+ ```
614
+
615
+ Only POST forms are accepted. Bind extra args with `.with(id)` (they go in the
616
+ action URL). `onSubmit={(e) => { e.preventDefault(); fetch(...) }}` *runs* and
617
+ drops the no-JS form fallback. `useAction` is JS-only for the same reason.
618
+
619
+ **`query` is the read cache** (optional, only with the router). Wrap a read,
620
+ give it a stable name, and consume it through a Solid async primitive:
621
+
622
+ ```tsx
623
+ import { createMemo } from 'solid-js';
624
+ import { query, revalidate } from '@solidjs/router';
625
+
626
+ export const getUser = query(async (id: string) => {
627
+ const response = await fetch(`/api/users/${id}`);
628
+ return response.json();
629
+ }, 'users');
630
+
631
+ const user = createMemo(() => getUser(params.id));
632
+ // after a mutation:
633
+ revalidate(getUser.key);
634
+ revalidate(getUser.keyFor('42'));
635
+ ```
636
+
637
+ A bare `createMemo(async () => fetchUser(id()))` still works; it just has no
638
+ shared cache, no preload reuse, and no `revalidate`. Do not wrap mutations in
639
+ `query` — wrapping an undeclared server function makes it GET. `query.get(key)`
640
+ throws if there is no entry; `query.set` does not accept a promise. Cache is
641
+ request-scoped on the server and application-scoped in the browser. After a
642
+ router mutation, `revalidate(...)` — not core `refresh()`, and not
643
+ `reload()` (that is a *server-function return* that asks the integration to
644
+ refresh cached data: `return reload({ revalidate: "todos" })`). `live()` sources
645
+ update through the open stream; do not `revalidate` them. Replace SolidStart /
646
+ Router 0.x leftovers: no `createAsync`, `useSubmission` (singular), `cache()`,
647
+ router `json()`, or `<FileRoutes />` (`fileRoutes(pageRoutes)` instead).
561
648
 
562
649
  ### Server functions — `"use server"`
563
650
 
@@ -570,16 +657,26 @@ export async function findUser(id: string) {
570
657
  ```
571
658
 
572
659
  A function-level server function cannot close over component locals — pass
573
- values as arguments. Treat every argument as untrusted. During SSR the same
660
+ values as arguments. Treat every argument as untrusted. Read trusted identity
661
+ from `getRequestEvent()`, never from a caller-supplied user id. During SSR the same
574
662
  reference runs in-process; in the browser it is HTTP. Mutations that should
575
663
  revalidate: `redirect` / `reload` from `@solidjs/web` (see the server-function
576
664
  guides). For 404 during SSR: `httpStatus(404)` from `@solidjs/web`.
577
665
  `throw new Error("…")` is stripped to `"Internal Server Error"` in production —
578
666
  use `markSafeError` or `respond(..., { status })` for intentional client-facing
579
- failures. `GET()` is only for idempotent reads (URLs leak into logs/history).
667
+ failures. Do not `return Response.json(...)` from `"use server"` — that is
668
+ HTTP-handler control flow; `respond(value, { status })` is what a scripted
669
+ caller unwraps. JSON-encodable arguments only unless `enableRichArguments()`
670
+ was called once in the client entry (`Date` / `Map` / `Set` throw without it).
671
+ `GET()` is only for idempotent reads (URLs leak into logs/history);
672
+ import it from `@solidjs/web/server-functions`, never `@solidjs/start`.
580
673
  `live()` yields **current state** (each yield replaces the last); do not treat
581
674
  it as an append-only event log, and hoist the memo so consumers share one
582
675
  connection.
676
+ Unscripted forms: `<form method="post" action={createTodo.url}>` (the reference
677
+ `.url`), not a client `preventDefault` + `fetch`, and not a hand-built
678
+ `/_server/` URL. GET forms (`method="get" action={search.url}`) are only for
679
+ idempotent search — do not use a GET form for a mutation.
583
680
 
584
681
  ### Document head — no MetaProvider
585
682
 
@@ -591,8 +688,12 @@ import { Title, Link, Meta } from '@solidjs/meta';
591
688
  ```
592
689
 
593
690
  Solid Meta 1.x has **no provider**. Render tags anywhere; later wins, unmount
594
- restores. `useHead` from `@solidjs/web` is the lower-level registry. Do not
595
- hardcode a second `<title>` in the document shell.
691
+ restores. `useHead` from `@solidjs/web` is the lower-level registry — prefer
692
+ `<Title>` / `<Meta>` / `<Script>` (JSON-LD) for application metadata. Several
693
+ tags that should swap as one unit belong in `<Head>`. A static `<title>`
694
+ in the document shell is the fallback when no `<Title>` is mounted; do not
695
+ hardcode a second `<meta name="description">` (the registry leaves foreign tags
696
+ alone, so both would coexist). Do not hardcode a second `<title>` in the document shell.
596
697
 
597
698
  ## These still run — write the other form
598
699
 
@@ -604,7 +705,7 @@ how to reuse. Prefer the form on the right.
604
705
  |---|---|
605
706
  | `{todos().map((t) => <Row todo={t} />)}` | `<For each={todos()} keyed={(t) => t.id}>` |
606
707
  | `<For each={todos().map(t => t)}>` | derive first, then `<For each={visible()}>` |
607
- | `class={`btn ${on() ? "on" : ""}`}` | `class={["btn", { on: on() }]}` |
708
+ | `class={`btn ${on() ? "on" : ""}`}` / `clsx("btn", on() && "on")` / `.filter(Boolean).join(" ")` | `class={["btn", { on: on() }]}` |
608
709
  | `style={{ width: 80 }}` (no unit) | `style={{ width: `${80}px` }}` |
609
710
  | `const rest = { ...props }` | `const rest = omit(props, "label")` |
610
711
  | `const user = createMemo(async () => { await fetch(...); return id(); })` | read `id()` **before** `await` |
@@ -632,6 +733,63 @@ how to reuse. Prefer the form on the right.
632
733
  | `live()` yields as an event log to append | each yield **replaces** the current answer; yield current state first |
633
734
  | `<article innerHTML={html()}>…children…</article>` | `innerHTML` **or** children, not both |
634
735
  | `fallback={(error) => <p>{error.message}</p>}` | `error` is an accessor: `error().message` (or `String(error())`) |
736
+ | `import { action } from "solid-js"` on a `<form>` | `import { action } from "@solidjs/router"` + `<form action={save} method="post">` (core `action` is a generator transaction, not a form URL) |
737
+ | `createMemo(async () => fetchUser(id()))` when `@solidjs/router` is present | `query(fetchUser, "users")` + `createMemo(() => getUser(id()))`; after mutations `revalidate(getUser.key)`, not core `refresh()` |
738
+ | `onSubmit={(e) => { e.preventDefault(); fetch(...) }}` for a router/server action | `<form action={save} method="post">` (POST only; `.with(id)` binds args into the URL). `useAction` is JS-only |
739
+ | `query(saveUser, "users")` wrapping a mutation | don't wrap mutations in `query` — an undeclared server function becomes GET |
740
+ | `render(<App />, root)` / Testing Library `render(<Counter />)` | `render(() => <App />, root)` — pass a function so Solid creates the root first |
741
+ | `render` onto SSR HTML / `hydrate` into an empty node | `hydrate(() => <App />, root)` when HTML already exists; `render` for an empty mount |
742
+ | `renderToString(() => <App />)` for an async or lazy-route tree | `await renderToStream(() => <App />)` (one consumer: `pipe` / `pipeTo` / `readable`). String render emits `<Loading>` fallbacks |
743
+ | `clientOnly(() => import("./Map"))` for a rarely shown widget | same + `{ lazy: true }` so the import waits until first render (default starts at declaration) |
744
+ | `<NoHydration><Chart /></NoHydration>` to skip SSR of a widget | `clientOnly(() => import("./Chart"))` — `NoHydration`/`Hydration` split ownership, they do not choose visible content |
745
+ | `await flush()` / `flush()` to wait on a pending memo | `await resolve(() => value())` (or Testing Library async queries); `flush()` only drains staged *sync* writes |
746
+ | `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 |
747
+ | `createContext` + a root provider for a true app singleton | module-level signal/store (SSR: shared across requests). Context is subtree scoping |
748
+ | `import { GET } from "@solidjs/start"` / `createAsync` / `useSubmission` / `<FileRoutes />` / `"use client"` | `GET` from `@solidjs/web/server-functions`; `createMemo(() => getUser(id()))`; `useSubmissions`; `fileRoutes(pageRoutes)`; Solid has no `"use client"` |
749
+ | `<Show keyed when={user()}>` by default | default `Show` (keeps children mounted); `keyed` only when internal state must reset |
750
+ | overlapping truthy `<Match>`es all expected to render | first truthy `<Match>` wins; later matches are skipped |
751
+ | missing `<HydrationScript />` / one script for many roots | once, before app markup, when the app owns the document; distinct `renderId` per extra root |
752
+ | `Router.paths` as the current URL; calling `preload()` and using the return as props | `useLocation` / `useParams`; factory/route `preload` result is `props.data` |
753
+ | `query.get(key)` with no guaranteed entry; `query.set(key, promise)` | `query.get` throws if missing; `query.set` does not accept a promise |
754
+ | `onCleanup(() => ...)` in a component body | `onSettled(() => { ...; return cleanup })` — `onCleanup` is for custom primitives |
755
+ | `onSettled` with reactive reads expecting re-runs | each call is a **single** fire; reads are untracked. Ongoing work is `createEffect` |
756
+ | `<Router>` nested inside `<Router>` | one instance; nest `children` arrays (or a lazy children thunk) |
757
+ | `const View = tab() ? A : B; return <View />` | `const View = dynamic(() => (tab() ? A : B))` — the ternary at setup freezes the choice |
758
+ | `<For each={resolved()}>` after `children()` | `{resolved.toArray()}` — resolved children are not a reactive data list |
759
+ | `<input onChange={...}>` for keystrokes | `onInput` — native `onChange` is blur/commit |
760
+ | `createOptimistic` for a local edit draft | writable derivation / plain store. Optimistic is for an in-flight mutation |
761
+ | `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`) |
762
+ | `setSubmitted(true)` + an effect that watches it | do the work in the handler or an `action` |
763
+ | `findUser(userId)` with the id from the client as identity | `getRequestEvent()!.locals.userId` |
764
+ | `action={"/_server/" + id}` in app JSX | `action={createTodo.url}` or a router `action` |
765
+ | `<form method="get" action={update.url}>` for a mutation | GET forms only for idempotent search; mutations are POST |
766
+ | `window.location.href = ...` / `history.pushState` | `useNavigate()` or `<a href={Router.paths...}>` |
767
+ | `httpStatus(404)` in a click handler | call it bare in the component / error-fallback body (a scope declaration, not a mutation) |
768
+ | `try { user() } catch (e) { /* NotReadyError */ }` | `<Loading>` — app code should not catch `NotReadyError` |
769
+ | `{latest(() => results())}` as the visible list | settled read + `isPending()` for the indicator; `latest` is for previews |
770
+ | `typeof window !== "undefined"` | `isServer` / `isDev` from `@solidjs/web` |
771
+ | `Object.assign({}, props)` / expecting `merge` to skip `undefined` like 1.x `mergeProps` | `omit` / `merge` — explicit `undefined` **overrides** (Object.assign) |
772
+ | `isPending(user())` / `latest(user())` | `isPending(user)` / `latest(user)` — pass the accessor (or `() => user()`). Calling it first evaluates the read before the helper runs |
773
+ | `isPending(...)` as the first-load spinner | `<Loading>` owns first flight; `isPending` is the *refetch* indicator after a settled answer exists |
774
+ | `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 |
775
+ | `revalidate(liveSource)` | `live()` updates through the open stream; do not revalidate it |
776
+ | `refresh(user)` without `affects` when the reload should look pending | pair `affects(user)` with `refresh(user)` — a bare `refresh` re-asks quietly |
777
+ | `<Dynamic component={tab() ? A : B} />` | `const View = dynamic(() => (tab() ? A : B))` — `<Dynamic>` is a JSX convenience; app code should use `dynamic()` for stable identity |
778
+ | `<meta name="description" content="...">` hardcoded in the document shell | `<Meta>` from `@solidjs/meta`. A static `<title>` is the fallback; other hardcoded tags coexist with the registry |
779
+ | `useHead({ tag: "meta", ... })` for ordinary page tags | `<Title>` / `<Meta>` / `<Script>` (JSON-LD). `useHead` is the lower-level registry |
780
+ | two `<Meta property="og:image">` meant as one replacement set | wrap them in `<Head>` — a later group replaces the earlier set as one unit |
781
+ | `createRoot(() => ...)` inside a component | let `render` / the component owner own the scope. `createRoot` is for tests, libraries, and non-render entry points |
782
+ | `JSON.stringify(store)` / `structuredClone(store)` | `snapshot(store)` — proxies throw or leak reactivity |
783
+ | `import { env } from "virtual:env/client"` (or `import.meta.env`) for a signing secret | `import { env } from "virtual:env/server"` — client map values are public |
784
+ | `renderToStream(...).pipe(res)` and also `.readable` / `await` on the same result | exactly one consumer: `pipe` / `pipeTo` / `readable` / await |
785
+ | `return Response.json(user, { status: 201 })` from `"use server"` | `return respond(user, { status: 201 })` — scripted callers receive `user`, not a `Response` |
786
+ | `<Portal>{user().name}</Portal>` for an async read | hoist the read above the portal (children start on the client); async UI inside one wants its own `<Loading>` |
787
+ | a client history adapter to pick the SSR location | `<Router url={request.url}>` (or the request event). Client adapters do not select the server URL |
788
+ | `fetch('/api')` at component-body top level | `createMemo(async () => { ... fetch ... })` under `<Loading>` — a top-level fetch runs once at mount and is not a reactive source |
789
+ | `<input type="checkbox" value={on()} />` | `checked={on()}` for the toggle; `value="string"` only when grouping radios or listing submitted values |
790
+ | `createMemo(() => { void save(); return x(); })` / an effect that calls an action | invoke actions from handlers, not from memos or effects |
791
+ | `createSignal(props.value)` / `createStore(props.items)` | `createSignal(() => props.value)` / `createStore(() => props.items, fallback)` — a bare `props.x` at setup is a snapshot |
792
+ | `createRenderEffect` / `createTrackedEffect` as the default effect | two-phase `createEffect(compute, apply)` |
635
793
 
636
794
  ```tsx
637
795
  // WRONG — post-await read never subscribes; production can hang pending
@@ -684,6 +842,53 @@ function Page() {
684
842
  }
685
843
  ```
686
844
 
845
+ ```tsx
846
+ // WRONG — two different `action`s; the core one is not a form URL
847
+ import { action } from 'solid-js';
848
+ const save = action(async function* (form: FormData) {
849
+ yield persist(form);
850
+ });
851
+ <form onSubmit={(e) => { e.preventDefault(); void save(new FormData(e.currentTarget)); }}>
852
+ <button type="submit">Save</button>
853
+ </form>
854
+
855
+ // CORRECT — router action is POST-addressable (only if `@solidjs/router` is in the project)
856
+ import { action } from '@solidjs/router';
857
+ const save = action(async (form: FormData) => persist(form), 'save');
858
+ <form action={save} method="post">
859
+ <button type="submit">Save</button>
860
+ </form>
861
+ ```
862
+
863
+ ```tsx
864
+ // WRONG — evaluating JSX before the root exists
865
+ import { render } from '@solidjs/web';
866
+ render(<App />, document.getElementById('app')!);
867
+
868
+ // CORRECT
869
+ render(() => <App />, document.getElementById('app')!);
870
+ ```
871
+
872
+ ```tsx
873
+ // WRONG — user() runs before isPending, so the helper never sees the accessor
874
+ <article aria-busy={isPending(user())}>{user().name}</article>
875
+
876
+ // CORRECT — pass the accessor (or () => user()); <Loading> still owns first flight
877
+ <Loading fallback={<p>Loading…</p>}>
878
+ <article aria-busy={isPending(user) ? 'true' : 'false'}>{user().name}</article>
879
+ </Loading>
880
+ ```
881
+
882
+ ```tsx
883
+ // WRONG — tab() is read once at setup; View never changes
884
+ const View = tab() ? Detailed : Compact;
885
+ return <View value="now" />;
886
+
887
+ // CORRECT
888
+ const View = dynamic(() => (tab() ? Detailed : Compact));
889
+ return <View value="now" />;
890
+ ```
891
+
687
892
  ## Checking the official docs
688
893
 
689
894
  Solid 2.0 docs: https://v2.solidjs.com/ — the site blocks non-browser fetchers, so from an
@@ -714,8 +919,10 @@ always-applied rules installed alongside this skill.
714
919
  - [ ] Effects are two-phase and only at imperative boundaries; apply does not read stores.
715
920
  - [ ] Async reads sit under `<Loading>` (the data slot, not chrome); errors under `<Errored>`.
716
921
  Reactive inputs of an async memo are read before the first `await`. No hand-rolled
717
- `loading`/`error` signals or `=== undefined` readiness branches; refetch via
718
- `refresh`, not counter signals.
922
+ `loading`/`error` signals or `=== undefined` readiness branches. First load is
923
+ `<Loading>`; refetch indicator is `isPending(user)` (the accessor, not `user()`).
924
+ Core `refresh(source)`, router `revalidate(key)`, and `return reload(...)` are
925
+ different APIs — do not mix them.
719
926
  - [ ] External collections reconcile into stores (function-form `createStore` /
720
927
  `reconcile`), never wholesale draft assignment or `setTodos((t) => t.filter(...))`
721
928
  as the identity-preserving update.
@@ -725,7 +932,16 @@ always-applied rules installed alongside this skill.
725
932
  - [ ] Inspect children with `children()`; code-split with `lazy` + `<Loading>`; reactive
726
933
  component choice with `dynamic()`. No `React.lazy`, no effects inside ref callbacks.
727
934
  - [ ] Browser-only code uses `isServer` / `clientOnly`, not `typeof window`.
935
+ Component teardown is `onSettled` (return cleanup), not `onCleanup`.
728
936
  - [ ] If the project has a router: `createRouter({ routes })`, not JSX `<Route>` / `<A>`.
937
+ One instance, no nested `<Router>`. Navigate with `useNavigate` / `<a href>`, not
938
+ `window.location`. Router `action`/`query` come from `@solidjs/router` (POST forms + cache), not
939
+ core `action`/`refresh`. Forms: `<form action={save} method="post">`.
940
+ - [ ] `render(() => <App />, root)` — a function, not `render(<App />)`. `hydrate` when
941
+ HTML already exists. Stream async/lazy trees (`await renderToStream(...)`) with
942
+ exactly one consumer (`pipe` / `pipeTo` / `readable`). Snapshot stores with
943
+ `snapshot(store)`, not `JSON.stringify`. Secrets live in `virtual:env/server`.
729
944
  - [ ] `jsxImportSource` is `@solidjs/web`; Vite plugin is `@solidjs/vite-plugin`.
945
+ No `@solidjs/start`, `vinxi`, `"use client"`, or Next.js imports.
730
946
  - [ ] Single return per component; no early returns on reactive conditions.
731
947
  - [ ] `solid2-kit check` and the project's typecheck pass.
@@ -98,6 +98,8 @@ read these before writing route JSX.
98
98
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/lifecycle-actions/affects.md
99
99
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/lifecycle-actions/on-settled.md
100
100
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/lifecycle-actions/refresh.md
101
+ - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/advanced/specialized-reactivity/on-cleanup.md
102
+ - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/advanced/store-advanced/snapshot.md
101
103
 
102
104
  ## Reference: components and JSX (solid-js)
103
105
 
@@ -113,6 +115,7 @@ read these before writing route JSX.
113
115
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/components-context/children.md
114
116
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/components-context/lazy.md
115
117
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/components-context/create-unique-id.md
118
+ - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/advanced/manual-hydration/no-hydration.md
116
119
 
117
120
  ## Reference: @solidjs/web
118
121
 
@@ -135,9 +138,13 @@ read these before writing route JSX.
135
138
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-web/jsx-properties/text-content.md
136
139
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-web/jsx-properties/inner-html.md
137
140
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-web/server-functions.md
141
+ - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-web/server-functions/get.md
142
+ - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-web/server-functions/rich-arguments.md
138
143
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-web/request-response.md
139
144
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-web/request-response/get-request-event.md
140
145
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-web/request-response/redirect.md
146
+ - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-web/request-response/reload.md
147
+ - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-web/request-response/respond.md
141
148
 
142
149
  ## Reference: @solidjs/router, @solidjs/meta, @solidjs/vite-plugin
143
150
 
@@ -145,9 +152,12 @@ read these before writing route JSX.
145
152
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-router/router-factory.md
146
153
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-router/navigation.md
147
154
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-router/data.md
155
+ - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-router/history.md
156
+ - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-router/filesystem.md
148
157
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-meta/title.md
149
158
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-meta/meta.md
150
159
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-meta/link.md
160
+ - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-meta/script.md
151
161
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-meta/head.md
152
162
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/vite-plugin-solid.md
153
163
  - https://v2-rebuild--solid-docs-v2.netlify.app/reference/vite-plugin-solid/options.md
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "solid2-agent-kit",
3
- "version": "0.4.2",
3
+ "version": "0.4.5",
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",