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.
- package/LICENSE +21 -0
- package/README.md +101 -0
- package/bin/solid2-kit.mjs +368 -0
- package/files/shared/agents-section.md +6 -0
- package/files/shared/rules-body.md +222 -0
- package/files/skills/solid-2/SKILL.md +731 -0
- package/files/skills/solid-2/references/official-docs.md +154 -0
- package/package.json +42 -0
|
@@ -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.
|