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,
|
|
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 (
|
|
28
|
-
|
|
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,
|
|
59
|
-
`npx github:...` form —
|
|
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
|
package/bin/solid2-kit.mjs
CHANGED
|
@@ -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" : ""}`}`
|
|
37
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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>`.
|
|
159
|
-
|
|
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
|
|
221
|
-
`createEffect` unless a single tracked callback is
|
|
222
|
-
`storePath` except while converting 1.x
|
|
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
|
-
|
|
|
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
|
|
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(
|
|
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(
|
|
254
|
-
Freshest in-flight value for
|
|
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
|
-
//
|
|
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
|
-
|
|
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.
|
|
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
|
|
560
|
-
|
|
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.
|
|
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. `
|
|
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
|
|
595
|
-
|
|
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
|
|
718
|
-
`
|
|
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.
|
|
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",
|