solid2-agent-kit 0.4.2

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.
@@ -0,0 +1,222 @@
1
+ # Solid 2.0 — not React, not Solid 1.x
2
+
3
+ This project uses Solid 2.0 (`solid-js` 2.x). The JSX looks like React but the
4
+ execution model is different: **a component function runs exactly once, at mount**. Updates
5
+ flow from signals/stores directly to the JSX expressions that read them. Components never
6
+ re-render. Solid 2.0 is also **incompatible with Solid 1.x** — treat all Solid 1.x knowledge
7
+ (pre-2.0 tutorials, blog posts, Stack Overflow answers) as wrong for this codebase.
8
+
9
+ Before writing non-trivial Solid code, read the `solid-2` skill
10
+ (`.cursor/skills/solid-2/SKILL.md` for Cursor, `.claude/skills/solid-2/SKILL.md` for
11
+ Claude Code) for full patterns, decision tables, and official documentation URLs.
12
+
13
+ ## Hard rules (React reflexes to unlearn)
14
+
15
+ 1. **Never destructure props.** `function C({ name })` and `const { name } = props` read the
16
+ prop getter once at setup and kill reactivity. Keep `props.x` and read it inside JSX, a
17
+ memo, or an effect compute function. Defaults go at the read site: `props.x ?? fallback`.
18
+ Rest props: `omit(props, "label", "value")`, never `const rest = { ...props }` (a snapshot).
19
+ JSX `{...rest}` is fine once `rest` is a reactive proxy. Rule of thumb: the string `props.`
20
+ must never appear at component-body top level *as a read* (`const x = props.x`); nested
21
+ functions (`() => props.x`, `omit(props, ...)`, `children(() => props.children)`) are fine.
22
+ Mechanically enforced by `solid2-kit check` — run it after editing TSX.
23
+ 2. **Never read reactive values at component-body top level.** The body runs once, untracked;
24
+ `const v = count()` there is a frozen snapshot (Solid warns in dev). Carry the accessor
25
+ (the function itself) into JSX and call it there.
26
+ 3. Signals are read by **calling** them: `{count()}` in JSX, not `{count}`.
27
+ 4. **Derived state is a function, not state + effect.** `const full = () => a() + b()`.
28
+ Never write "a signal plus an effect that syncs it". Use `createMemo` only for expensive
29
+ computations, multiple consumers, or an equality boundary. When several writes can land
30
+ before a flush, use the setter updater (`setCount((c) => c + 1)`); `setCount(count() + 1)`
31
+ reads the last *committed* value and drops the other staged writes. To store a *function*
32
+ in a signal, wrap it: `setHandler(() => myHandler)` — `setHandler(myHandler)` treats it
33
+ as an updater.
34
+ 5. `class`, not `className`; `for`, not `htmlFor`. `class` accepts strings, conditional
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
38
+ (`"background-color"`, not `backgroundColor`) and numbers do NOT auto-append `px`
39
+ (`width: `${n}px``).
40
+ 6. **`onInput` for per-keystroke handling.** Solid's `onChange` is the native change event
41
+ (fires on blur/commit), not React's per-keystroke `onChange`. Handlers receive native DOM
42
+ events and run untracked — reading a signal inside a handler always gives the current
43
+ value (stale closures cannot happen; `useCallback` equivalents are unnecessary).
44
+ Checkboxes/radios use `checked={...}`, not `value`. Read fields with
45
+ `event.currentTarget` (the element that owns the handler), not `event.target`
46
+ (a nested child). Never pass a setter as the handler (`onClick={setCount}`) —
47
+ that writes the event object; wrap it: `onClick={() => setCount((c) => c + 1)}`.
48
+ 7. **Lists use `<For>`**, never `{list().map(...)}` in reactive JSX and never `key` props.
49
+ `{todos().map((t) => <Row todo={t} />)}` *renders*, then recreates every row on each
50
+ update. Row identity: default = item reference; `keyed={(item) => item.id}` = key function
51
+ (child receives item as an accessor); `keyed={false}` = positional. **Rows from
52
+ server/refetched data (fresh object references on every update — fetch results,
53
+ subscription payloads) must use a key function on a stable id**, or every update
54
+ recreates every row. Reference keying is for local arrays whose item identities are
55
+ stable (e.g. store rows). Match the child signature to the mode: default item is the
56
+ **raw** value (`todo.title`); `keyed={(t) => t.id}` / `keyed={false}` pass an
57
+ **accessor** (`todo().title`). Mixing those still “runs” (you render a function, or
58
+ throw). Empty lists: `fallback` on `<For>`, not a `.length` branch around it.
59
+ Sliding windows: `<Repeat from={from()} count={n}>`, not `list.slice(from, from+n)`
60
+ fed to `<For>` (that rebuilds the window array every time).
61
+ 8. **Effects have two phases**: `createEffect(compute, apply)`. All reactive reads go in
62
+ `compute`; its return value feeds `apply`, which does imperative work and may return a
63
+ cleanup. Single-argument `createEffect(fn)` is an error in Solid 2. Do not substitute
64
+ `createTrackedEffect` for that — it is an advanced one-callback form that cannot nest
65
+ primitives. Most React `useEffect` code should not become an effect at all — see the skill.
66
+ 9. **Stores update by mutating a draft**: `setStore(draft => { draft.user.name = "Ada" })`.
67
+ Never rebuild with spreads — that destroys property-level subscriptions. Returning a new
68
+ array from the setter (`setTodos((t) => t.filter(...))`) *renders* but is unkeyed — same
69
+ identity loss as wholesale assignment; mutate the draft or `reconcile`. Stores are
70
+ **property reads** (`todos.length`, `todo.title`), not accessors — never `todos()`.
71
+ 10. **Async data is an async computation**: `createMemo(async () => ...)` read under
72
+ `<Loading>` / `<Errored>` boundaries. Read every reactive input **before the first
73
+ `await`** — post-`await` reads do not subscribe, and in production the computation can
74
+ sit pending with no retry. No `useEffect` + `setState` fetching, no `createResource`.
75
+ `<Loading>` wraps the data slot, not page chrome. After first paint it keeps content
76
+ during refetch (`isPending` for the indicator). Use `on={id()}` (the *value*, not the
77
+ accessor) only when that identity change should show the fallback again. Pass an async
78
+ memo to a child as the **accessor** (`user={user}`), and read `props.user()` under that
79
+ 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
+ `createSignal(() => props.value)` or `createStore(() => props.value, fallback)`.
82
+ 11. Context: the context object is the provider — `<MyContext value={{ theme, setTheme }}>`
83
+ (no `.Provider`). Pass accessors/setters/stores through context, never snapshot values.
84
+ `createContext<T>()` without a default throws when read outside a provider (good) — use
85
+ that for any reactive payload. `createContext("light")` is only for a primitive static
86
+ fallback; a dummy default on a store/signal context *runs* and then silently no-ops
87
+ without a provider.
88
+ 12. Refs: `let el!: HTMLDivElement` + `ref={el}` or `ref={(node) => (el = node)}`, or forward
89
+ `props.ref`. Compose with arrays: `ref={[props.ref, (node) => (el = node)]}`. No
90
+ `useRef`/`.current`. Ref callbacks run untracked and **without an owner**; their
91
+ return values are ignored. Do not create effects or register cleanup inside a ref
92
+ callback. For reusable directives, create owned primitives (`onSettled`) in a
93
+ factory and return only the element callback.
94
+ 13. **Writes are staged** and commit on the next microtask. Event handlers need nothing
95
+ special; tests and imperative integration code must call `flush()` before observing
96
+ updated state or DOM. Never call `flush()` inside an `action`.
97
+ 14. Do not port these React tools — they have no Solid equivalent because the problems they
98
+ solve don't exist: `useCallback`, `React.memo`, `forwardRef`, `useSyncExternalStore`,
99
+ dependency arrays, `startTransition`/`useTransition` (updates are held and coordinated
100
+ automatically; use `isPending` for indicators).
101
+ 15. **Components return once.** Never early-return or conditionally return based on reactive
102
+ values — the branch is chosen once at setup and frozen forever. Put conditionals inside
103
+ JSX (`<Show>`, ternary, `<Switch>`/`<Match>`). Early returns on genuinely non-reactive
104
+ values (build-time config, missing env) are fine.
105
+ 16. Prefer `textContent` for text-only content. Use `innerHTML` only for trusted or
106
+ sanitized markup — never interpolate user input into it, never React's
107
+ `dangerouslySetInnerHTML`, and never combine `innerHTML` with JSX children (they fight
108
+ over the same contents).
109
+ 17. **Never narrow a reactive read with a non-null assertion** (`error()!.message`,
110
+ `user()!`). Use `<Show when={value()}>` with a function child — it passes a
111
+ **narrowed accessor**, so no `!` is needed:
112
+ `<Show when={error()}>{(err) => <p>{err().message}</p>}</Show>`. For non-reactive
113
+ zero-arg calls, use an explicit guard variable instead of `!`.
114
+ Mechanically enforced by `solid2-kit check`.
115
+ 18. **Never hand-roll async UI state.** No `[loading, setLoading]` signals, no
116
+ `data() === undefined` readiness branches, no `{ data, error }` signal pairs from
117
+ integration layers. Model async as a computation (return a promise or async iterable
118
+ from a memo/store) and let the framework own the states: first load = `<Loading>`
119
+ fallback; refetch indicator = `isPending()`; errors = thrown into the graph and caught
120
+ by `<Errored>`; speculative value = `latest()`. Mechanically enforced by
121
+ `solid2-kit check` (loading-signal naming, zero-arg-call undefined checks).
122
+ 19. **Refetch is `refresh(source)`** — rarely needed, but when it is, never fake it with a
123
+ counter/version signal read inside the computation. Input-driven refetch is automatic
124
+ (tracked inputs re-run the computation) and subscriptions push. Legit uses: after a
125
+ mutation inside an `action`, and explicit reload buttons. Pair with `affects(source)`
126
+ when the reload should present as pending.
127
+ 20. **External collections flow into stores through reconciliation.** First choice:
128
+ function-form `createStore(() => source, fallback)` / `createProjection` (reconcile
129
+ automatically, keyed by `"id"`). Manual merge: `setStore(reconcile(fresh, "id"))` or
130
+ apply `reconcile(fresh, "id")(draft.slot)` inside a draft setter. Plain wholesale
131
+ assignment (`draft.todos = fresh`) renders correctly but notifies every subscriber
132
+ under the path and destroys row identity — same class of problem as unkeyed `<For>`.
133
+ 21. **Solid inputs do not rewind the DOM.** `value={v()}` writes only when `v` changes; a
134
+ React-style controlled input that ignores invalid keystrokes silently breaks. To reject
135
+ input, write back explicitly:
136
+ `onInput={(e) => { if (valid(e.currentTarget.value)) setV(e.currentTarget.value); else e.currentTarget.value = v(); }}`.
137
+ 22. Miscellaneous defaults: SSR-stable element ids come from `createUniqueId()` (never
138
+ `Math.random()` or hardcoded duplicates); pass store data to `structuredClone` /
139
+ `postMessage` / logs via `snapshot(store)` (proxies fail or leak reactivity); render
140
+ modals/tooltips/overlays through `<Portal>` from `@solidjs/web`; mutations whose writes
141
+ cross an async gap default to `action` + `createOptimistic`/`createOptimisticStore` —
142
+ except reactive clients (e.g. Convex) whose subscriptions already push authoritative
143
+ state after mutations. Compiler: `"jsxImportSource": "@solidjs/web"` (not `"solid-js"`);
144
+ Vite plugin is `@solidjs/vite-plugin` (not `vite-plugin-solid`). Server vs browser:
145
+ `isServer` / `isDev` from `@solidjs/web`, or `clientOnly(() => import("./Widget"))` for
146
+ browser-only components — never `typeof window` as the SSR boundary. `JSON.stringify(store)`
147
+ / `structuredClone(store)` without `snapshot(store)` can throw or leak proxies. Async SSR
148
+ without `<Loading>` *works* but blocks the HTML shell until every read settles — wrap
149
+ reads, or stream with `renderToStream`.
150
+ 23. **Composition.** Pass `props.children` through when you only render them. Inspect or
151
+ iterate children with `children(() => props.children)` (then `.toArray()`), never a
152
+ setup-time snapshot. Code-split with `lazy(() => import("./X"))` under `<Loading>` —
153
+ not `React.lazy` / `<Suspense>`. Named exports: `lazy(() => import("./pages"), { export: "About" })`
154
+ — `lazy(() => import("./pages").then((m) => ({ default: m.About })))` hydrates wrong
155
+ because the named export is not a call-site literal. Select a component from reactive
156
+ state with `dynamic(() => ...)` from `@solidjs/web`
157
+ (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`.
160
+
161
+ ## Lint heritage: eslint-plugin-solid (Solid 1.x) intents carried into this file
162
+
163
+ `eslint-plugin-solid` targets Solid 1.x. **Do not install it** — its analyzer predates the
164
+ Solid 2 model and misreads two-phase `createEffect`, writable derivations
165
+ (`createSignal(fn)`), and draft setters as errors. Its still-valid intents are enforced here
166
+ instead, and `solid2-kit check` checks the mechanically detectable ones on the source
167
+ directory (default `src/`).
168
+
169
+ | 1.x lint rule | Status in Solid 2 | Where |
170
+ |---|---|---|
171
+ | `solid/no-destructure` | still critical | rule 1 + `solid2-kit check` |
172
+ | `solid/reactivity` | still critical | rules 2–3 (+ Solid 2 dev-mode warnings) |
173
+ | `solid/components-return-once` | still valid | rule 15 |
174
+ | `solid/no-react-specific-props` | still valid | rule 5 + `solid2-kit check` |
175
+ | `solid/no-react-deps` | still valid — never pass React-style dependency arrays | rule 8 + `solid2-kit check` react-hook check |
176
+ | `solid/prefer-for` | still valid — `{list().map}` still renders | rule 7 + `solid2-kit check` |
177
+ | `solid/prefer-show` | now a recommended default when narrowing | rule 17 + `solid2-kit check` |
178
+ | `solid/no-innerhtml` | still valid | rule 16 |
179
+ | `solid/style-prop` | still valid | rule 5 + `solid2-kit check` |
180
+ | `solid/imports` | changed paths | banned-API table + `solid2-kit check` |
181
+ | `solid/event-handlers` | still valid | rule 6 |
182
+ | `solid/jsx-no-undef`, `jsx-no-duplicate-props` | covered by TypeScript | project typecheck |
183
+ | `solid/prefer-classlist` | obsolete — `classList` removed | `solid2-kit check` bans `classList` |
184
+ | `solid/no-unknown-namespaces` | obsolete — `on:`/`use:`/`attr:` removed in 2.0 | banned-API table + `solid2-kit check` |
185
+ | `solid/no-proxy-apis` | obsolete — Solid 2 requires Proxy | not carried |
186
+
187
+ ## Banned Solid 1.x APIs (Solid 2 replacements)
188
+
189
+ | Never write (Solid 1.x) | Write instead (Solid 2) |
190
+ |---|---|
191
+ | `import ... from "solid-js/store"` or `"solid-js/web"` | stores/`merge`/`omit` from `"solid-js"`; `render`/`hydrate`/`Portal`/`Dynamic` from `"@solidjs/web"` |
192
+ | `createResource` | async `createMemo` + `<Loading>`/`<Errored>`; `refresh()`, `latest()`, `isPending()` |
193
+ | `createEffect(fn)` (one arg), `on(...)` | `createEffect(compute, apply)`; deps belong in `compute` |
194
+ | `onMount` | `onSettled` (return cleanup from its callback) |
195
+ | `batch(...)` | delete it — writes auto-batch; `flush()` only to observe synchronously |
196
+ | `<Suspense>`, `<ErrorBoundary>`, `<SuspenseList>` | `<Loading>`, `<Errored>` (fallback gets an error *accessor*), `<Reveal>` |
197
+ | `<Index>` | `<For keyed={false}>` |
198
+ | `<Ctx.Provider value={...}>` | `<Ctx value={...}>` |
199
+ | `setState("a", "b", value)` path setters, `produce(...)` | draft setter: `setState(draft => { ... })` |
200
+ | `mergeProps` / `splitProps` / `unwrap` | `merge` / `omit` / `snapshot` |
201
+ | `classList={...}` | `class` object/array form |
202
+ | `createMutable`, `modifyMutable` | `createStore` + draft setters |
203
+ | `createComputed`, `createSelector`, `createDeferred` | `createMemo`, `createProjection`, external scheduling |
204
+ | `startTransition`, `useTransition` | automatic held updates + `isPending` |
205
+ | `onError` / `catchError` | `<Errored>` or effect bundle `{ effect, error }` |
206
+ | `from(...)` / `observable(...)` (the solid-js helpers) | inbound: async iterable from a memo; outbound: split effect |
207
+ | `createDynamic(...)` | `dynamic(source)` from `@solidjs/web` |
208
+ | `renderToStringAsync(...)` | `await renderToStream(() => <App />)` (one consumer: `pipe` / `pipeTo` / `readable`) |
209
+ | `clearDelegatedEvents` | delete — delegated listeners are scoped to each render root |
210
+ | `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()` |
212
+ | `<MetaProvider>` | no provider — render `<Title>` / `<Meta>` / `<Link>` from `@solidjs/meta` anywhere |
213
+ | `use:directive`, `on:`/`oncapture:`, `attr:`/`bool:`, `/*@once*/` | `ref` callbacks, camelCase event props, standard attributes, keep values reactive |
214
+ | `resource.loading` / `resource.error` | `<Loading>` boundary / `<Errored>` boundary |
215
+
216
+ When unsure about any API, verify against the official Solid 2.0 docs — fetchable URLs are
217
+ listed in `references/official-docs.md` next to the `solid-2` skill. Do not guess from
218
+ Solid 1.x or React memory.
219
+
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.