solid2-agent-kit 0.7.0 → 0.8.1
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 +1 -1
- package/bin/solid2-kit.mjs +100 -2
- package/files/shared/agents-section.md +1 -1
- package/files/shared/rules-body.md +14 -2
- package/files/skills/solid-2/SKILL.md +50 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ Supported agents: **Cursor** and **Claude Code**.
|
|
|
16
16
|
| Always-applied hard rules | `.cursor/rules/solid-2.mdc` (glob-attached to `*.tsx`/`*.jsx`) | managed block in `CLAUDE.md` | 23 hard rules + banned Solid 1.x API table + eslint-plugin-solid lint-heritage table. Injected without the agent having to decide to read anything |
|
|
17
17
|
| Agent skill | `.cursor/skills/solid-2/` | `.claude/skills/solid-2/` | Execution-model primer, decision tables, canonical patterns, a “still runs / write the other form” table, review checklist, official-doc URL index |
|
|
18
18
|
| Shared agent context | managed block in `AGENTS.md` | same (Claude Code users can reference it from `CLAUDE.md`) | Core principles and pointers, always in context |
|
|
19
|
-
| Mechanical gate | `solid2-kit check` | same | Comment-stripped regex guard over `src/**/*.{ts,tsx,jsx}` (or explicit `[files...]`): fails on props destructuring (including multi-line signatures), `{ ...props }` rest copies, `{list().map(...)}` in JSX, React imports/hooks/JSX props/`React.lazy`/`useOptimistic`/`onDoubleClick`, Solid 1.x imports/APIs/components/JSX namespaces, `vite-plugin-solid`, old-router `<Route>`/`<A>`/`<HashRouter>`/`<FileRoutes>`, `MetaProvider`, `Context.Provider`, camelCase style keys, `key` props, `value()!` hand-narrowing, hand-rolled loading signals, `=== undefined` readiness branches, SolidStart leftovers (`@solidjs/start`, `h3`, `createAsync`, `useSubmission`, `"use client"`), Next.js imports/`useRouter`/`typeof window`, `history.pushState`, `render(<App />)` (pass a function), `isPending(user())` / `latest(user())` (pass the accessor),
|
|
19
|
+
| Mechanical gate | `solid2-kit check` | same | Comment-stripped regex guard over `src/**/*.{ts,tsx,jsx}` (or explicit `[files...]`): fails on props destructuring (including multi-line signatures), `{ ...props }` rest copies, `{list().map(...)}` in JSX, React imports/hooks/JSX props/`React.lazy`/`useOptimistic`/`onDoubleClick`, Solid 1.x imports/APIs/components/JSX namespaces, `vite-plugin-solid`, old-router `<Route>`/`<A>`/`<HashRouter>`/`<FileRoutes>`, `MetaProvider`, `Context.Provider`, camelCase style keys, `key` props, `value()!` hand-narrowing, hand-rolled loading signals, `=== undefined` readiness branches, SolidStart leftovers (`@solidjs/start`, `h3`, `createAsync`, `useSubmission`, `"use client"`), Next.js imports/`useRouter`/`typeof window`, `history.pushState`, `render(<App />)` (pass a function), `isPending(user())` / `latest(user())` (pass the accessor), `<Dynamic>` (use `dynamic()`), `import type { JSX } from "solid-js"` (no `JSX` namespace there — `Element` from `solid-js`, DOM `JSX` types from `@solidjs/web`), and two-phase effects whose apply only writes a local signal (use a writable derivation) |
|
|
20
20
|
| Edit-time hook | `postToolUse` entry in `.cursor/hooks.json` | `PostToolUse` entry in `.claude/settings.json` (matcher includes `Bash`) | Runs the mechanical gate automatically on **every agent file edit — including edits made through shell commands** (sed, heredocs, codemods; source paths mentioned in the command are checked after it ran) — and feeds findings straight back into the conversation (Cursor: `additional_context`; Claude Code: stderr + exit 2), so enforcement does not depend on the agent remembering to run `check`. Wired only when the kit is a local devDependency; opt out with `--no-hooks` |
|
|
21
21
|
| Turn-end gate | — | `Stop` entry in `.claude/settings.json` | Whole-project `check` + `doctor` when the agent tries to end its turn: with findings in place the stop is blocked (exit 2) and the findings are fed back, so a turn cannot finish with React/Solid 1.x patterns left behind. Loop-safe via `stop_hook_active` (one forced continuation, never an infinite loop) |
|
|
22
22
|
| Pre-execution guard | `preToolUse` + `beforeShellExecution` entries in `.cursor/hooks.json` | `PreToolUse` entry in `.claude/settings.json` | Denies actions **before they run**: edits/deletes of kit-owned guardrail files (an agent blocked by a gate will try to remove the gate), edits that would strip the kit's hook entries or managed blocks, uninspectable shell rewrites of those files, and `npm/pnpm/yarn/bun install` of banned dependencies (`react`, `vite-plugin-solid`, `eslint-plugin-solid`, SolidStart, ...) — caught proactively instead of post-hoc by `doctor` |
|
package/bin/solid2-kit.mjs
CHANGED
|
@@ -274,6 +274,86 @@ function init() {
|
|
|
274
274
|
|
|
275
275
|
// --- check ------------------------------------------------------------------
|
|
276
276
|
|
|
277
|
+
// Balanced-scan helpers for the effect-sync check: regexes cannot pair the
|
|
278
|
+
// parentheses of a createEffect(...) call, so scan (string-aware; comments
|
|
279
|
+
// are already stripped) for the matching closer from an opening bracket.
|
|
280
|
+
function scanBalanced(content, openIndex) {
|
|
281
|
+
let depth = 0;
|
|
282
|
+
let quote = null;
|
|
283
|
+
for (let i = openIndex; i < content.length; i += 1) {
|
|
284
|
+
const ch = content[i];
|
|
285
|
+
if (quote !== null) {
|
|
286
|
+
if (ch === '\\') i += 1;
|
|
287
|
+
else if (ch === quote) quote = null;
|
|
288
|
+
} else if (ch === "'" || ch === '"' || ch === '`') {
|
|
289
|
+
quote = ch;
|
|
290
|
+
} else if (ch === '(' || ch === '{' || ch === '[') {
|
|
291
|
+
depth += 1;
|
|
292
|
+
} else if (ch === ')' || ch === '}' || ch === ']') {
|
|
293
|
+
depth -= 1;
|
|
294
|
+
if (depth === 0) return i;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return -1;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function splitTopLevelArgs(argsText) {
|
|
301
|
+
const parts = [];
|
|
302
|
+
let depth = 0;
|
|
303
|
+
let quote = null;
|
|
304
|
+
let start = 0;
|
|
305
|
+
for (let i = 0; i < argsText.length; i += 1) {
|
|
306
|
+
const ch = argsText[i];
|
|
307
|
+
if (quote !== null) {
|
|
308
|
+
if (ch === '\\') i += 1;
|
|
309
|
+
else if (ch === quote) quote = null;
|
|
310
|
+
} else if (ch === "'" || ch === '"' || ch === '`') {
|
|
311
|
+
quote = ch;
|
|
312
|
+
} else if (ch === '(' || ch === '{' || ch === '[') {
|
|
313
|
+
depth += 1;
|
|
314
|
+
} else if (ch === ')' || ch === '}' || ch === ']') {
|
|
315
|
+
depth -= 1;
|
|
316
|
+
} else if (ch === ',' && depth === 0) {
|
|
317
|
+
parts.push(argsText.slice(start, i));
|
|
318
|
+
start = i + 1;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
parts.push(argsText.slice(start));
|
|
322
|
+
return parts;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// An apply phase that is nothing but one (optionally if-guarded) call to a
|
|
326
|
+
// local signal setter is "a signal plus an effect that syncs it" — rule 4.
|
|
327
|
+
// Restricting to setters declared via createSignal destructuring in the same
|
|
328
|
+
// file keeps external set* functions (setDefaultOptions, setTimeout) out.
|
|
329
|
+
function soleLocalSetterApply(apply, localSetters) {
|
|
330
|
+
const head = apply.match(
|
|
331
|
+
/^(?:async\s*)?(?:\([^()]*\)|[\w$]+)\s*=>\s*\{?\s*(?:if\s*\((?:[^()]|\([^()]*\))*\)\s*\{?\s*)?(set[A-Z][\w$]*)\s*\(/,
|
|
332
|
+
);
|
|
333
|
+
if (!head || !localSetters.has(head[1])) return false;
|
|
334
|
+
const close = scanBalanced(apply, head[0].length - 1);
|
|
335
|
+
if (close === -1) return false;
|
|
336
|
+
return /^[\s;}]*$/.test(apply.slice(close + 1));
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const LOCAL_SETTER_DECL = /\[\s*[\w$]+\s*,\s*(set[A-Z][\w$]*)\s*\]\s*=\s*createSignal\b/g;
|
|
340
|
+
|
|
341
|
+
function effectSyncFindings(content) {
|
|
342
|
+
const localSetters = new Set([...content.matchAll(LOCAL_SETTER_DECL)].map((m) => m[1]));
|
|
343
|
+
if (localSetters.size === 0) return [];
|
|
344
|
+
const matches = [];
|
|
345
|
+
for (const call of content.matchAll(/\bcreateEffect\s*(?:<[^>()]*>)?\s*\(/g)) {
|
|
346
|
+
const open = call.index + call[0].length - 1;
|
|
347
|
+
const close = scanBalanced(content, open);
|
|
348
|
+
if (close === -1) continue;
|
|
349
|
+
const args = splitTopLevelArgs(content.slice(open + 1, close));
|
|
350
|
+
if (args.length >= 2 && soleLocalSetterApply(args[1].trim(), localSetters)) {
|
|
351
|
+
matches.push({ index: call.index });
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return matches;
|
|
355
|
+
}
|
|
356
|
+
|
|
277
357
|
const CHECKS = [
|
|
278
358
|
{
|
|
279
359
|
id: 'props-destructure-param',
|
|
@@ -298,6 +378,20 @@ const CHECKS = [
|
|
|
298
378
|
message:
|
|
299
379
|
'Solid 1.x import path. Stores/merge/omit come from "solid-js"; render/hydrate/Portal/Dynamic come from "@solidjs/web".',
|
|
300
380
|
},
|
|
381
|
+
{
|
|
382
|
+
id: 'jsx-namespace-import',
|
|
383
|
+
pattern: /import\s+(?:type\s+)?\{[^}]*\bJSX\b[^}]*\}\s*from\s+['"]solid-js['"]/g,
|
|
384
|
+
message:
|
|
385
|
+
'Solid 2 does not export a `JSX` namespace from "solid-js" (TS2305). Children/return types are `Element` from "solid-js"; DOM-specific JSX types (JSX.IntrinsicElements, JSX.CSSProperties) come from "@solidjs/web".',
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
// A two-phase createEffect whose apply is nothing but one (optionally
|
|
389
|
+
// if-guarded) call to a setter declared via createSignal in this file.
|
|
390
|
+
id: 'effect-sync-signal',
|
|
391
|
+
find: effectSyncFindings,
|
|
392
|
+
message:
|
|
393
|
+
'Effect that only syncs a local signal — "state + effect" (rule 4). Derive the value instead; to reset state when another reactive value changes, use a writable derivation: createSignal(() => { source(); return initial; }).',
|
|
394
|
+
},
|
|
301
395
|
{
|
|
302
396
|
id: 'react-jsx-prop',
|
|
303
397
|
pattern: /\b(?:className|htmlFor)=/g,
|
|
@@ -572,7 +666,7 @@ function fileFindings(file, relativeTo) {
|
|
|
572
666
|
const lines = raw.split('\n');
|
|
573
667
|
const findings = [];
|
|
574
668
|
for (const rule of CHECKS) {
|
|
575
|
-
for (const match of content.matchAll(rule.pattern)) {
|
|
669
|
+
for (const match of rule.find ? rule.find(content) : content.matchAll(rule.pattern)) {
|
|
576
670
|
const lineNumber = content.slice(0, match.index).split('\n').length;
|
|
577
671
|
findings.push(
|
|
578
672
|
`${relative(relativeTo, file) || file}:${lineNumber} [${rule.id}] ${rule.message}\n > ${lines[lineNumber - 1].trim()}`,
|
|
@@ -965,7 +1059,11 @@ function doctorFindings(target) {
|
|
|
965
1059
|
for (const guidance of GUIDANCE_FILES) {
|
|
966
1060
|
const path = join(target, guidance);
|
|
967
1061
|
if (!existsSync(path)) continue;
|
|
968
|
-
|
|
1062
|
+
// The version must end on a word character so the sentence punctuation
|
|
1063
|
+
// that follows the marker ("… v0.8.0. Do not edit …", "…v0.8.0**.") is
|
|
1064
|
+
// not captured — with `[\w.-]*` alone the trailing period made every
|
|
1065
|
+
// freshly synced file compare as "0.8.0." !== "0.8.0" (always stale).
|
|
1066
|
+
const installed = readFileSync(path, 'utf8').match(/solid2-agent-kit v(\d(?:[\w.-]*\w)?)/)?.[1];
|
|
969
1067
|
if (installed && installed !== VERSION) {
|
|
970
1068
|
report(
|
|
971
1069
|
'stale-guidance',
|
|
@@ -5,4 +5,4 @@
|
|
|
5
5
|
- After editing TSX, run the `solid2-kit check` mechanical gate — it fails on React/Solid 1.x tokens and props destructuring. Use the project's `lint:solid` script if one exists; otherwise `npx solid2-agent-kit check` (with the kit as a devDependency) or `npx github:lightsound/solid2-agent-kit check`. An edit-time hook may also run this gate automatically after each file edit — treat its findings as errors to fix immediately, not warnings. Do not install `eslint-plugin-solid` (built for Solid 1.x; misreads Solid 2 idioms).
|
|
6
6
|
- After editing `package.json`, `tsconfig*.json`, or root config files (Vite, ESLint), run `solid2-kit doctor` — it fails on React/Solid 1.x wiring: `react`/`vite-plugin-solid`/`eslint-plugin-solid`/SolidStart dependencies, 1.x `solid-js` ranges, and `jsx`/`jsxImportSource` values other than `preserve`/`@solidjs/web`.
|
|
7
7
|
- The kit's guidance and gate files (`.cursor/rules/solid-2.mdc`, the `solid-2` skill directories, hook entries, managed blocks in this file and `CLAUDE.md`) are guardrails, not obstacles. Never edit, weaken, bypass, or delete them to make a failure go away — fix the flagged code. If a guardrail seems wrong, stop and tell the user.
|
|
8
|
-
- Solid 2.0 shipped recently and breaks with 1.x — never trust pre-2.0 Solid docs, tutorials, or training knowledge. Verify APIs against https://v2-rebuild--solid-docs-v2.netlify.app/llms.txt (markdown mirror of https://v2.solidjs.com/, which blocks non-browser fetchers). If the project has `@solidjs/router`, server functions, or `@solidjs/meta`, use those official pages (see the skill's official-docs index) — not React Router, Next.js, SolidStart 1.x, or Solid Router 0.x/1.x JSX `<Route>`/`<A>`. Core `action` from `solid-js` is a generator transaction; router `action`/`query` from `@solidjs/router` are POST forms and a read cache. Core `refresh(source)`, router `revalidate(key)`, and server-function `return reload(...)` are three different APIs. Pass the accessor to `isPending(user)`, not `isPending(user())`. Pass a function to `render`/`hydrate`: `render(() => <App />, root)`. Component teardown is `onSettled`, not `onCleanup`. One router instance; in-app navigation is `useNavigate` / `<a href>`, not `window.location`.
|
|
8
|
+
- Solid 2.0 shipped recently and breaks with 1.x — never trust pre-2.0 Solid docs, tutorials, or training knowledge. Verify APIs against https://v2-rebuild--solid-docs-v2.netlify.app/llms.txt (markdown mirror of https://v2.solidjs.com/, which blocks non-browser fetchers). If the project has `@solidjs/router`, server functions, or `@solidjs/meta`, use those official pages (see the skill's official-docs index) — not React Router, Next.js, SolidStart 1.x, or Solid Router 0.x/1.x JSX `<Route>`/`<A>`. Core `action` from `solid-js` is a generator transaction; router `action`/`query` from `@solidjs/router` are POST forms and a read cache. Core `refresh(source)`, router `revalidate(key)`, and server-function `return reload(...)` are three different APIs. Pass the accessor to `isPending(user)`, not `isPending(user())`. Pass a function to `render`/`hydrate`: `render(() => <App />, root)`. Component teardown is `onSettled`, not `onCleanup`. One router instance; in-app navigation is `useNavigate` / `<a href>`, not `window.location`. Types: `solid-js` exports no `JSX` namespace — children/return types are `Element` from `solid-js`; DOM `JSX` types come from `@solidjs/web`. Reset-a-signal-on-change is a writable derivation (`createSignal(() => { source(); return initial; })`), never an effect calling the setter. Client mode mounts with `render()` into an empty body — no hydration mismatch exists, so never defer initial values to post-mount React-style (that can overwrite persisted state).
|
|
@@ -25,7 +25,13 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
25
25
|
(the function itself) into JSX and call it there.
|
|
26
26
|
3. Signals are read by **calling** them: `{count()}` in JSX, not `{count}`.
|
|
27
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".
|
|
28
|
+
Never write "a signal plus an effect that syncs it". That includes **reset**: to clear
|
|
29
|
+
a signal when another reactive value changes, use a writable derivation —
|
|
30
|
+
`createSignal(() => { source(); return initial; })` (re-runs when `source()` changes;
|
|
31
|
+
the setter still works) — never a `createEffect` whose apply calls the setter. The
|
|
32
|
+
derivation also moves the `source()` read to the read site, under its boundaries.
|
|
33
|
+
Mechanically enforced by `solid2-kit check` (effects whose apply only writes a local
|
|
34
|
+
signal). Use `createMemo` only for expensive
|
|
29
35
|
computations, multiple consumers, or an equality boundary. When several writes can land
|
|
30
36
|
before a flush, use the setter updater (`setCount((c) => c + 1)`); `setCount(count() + 1)`
|
|
31
37
|
reads the last *committed* value and drops the other staged writes. To store a *function*
|
|
@@ -175,7 +181,12 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
175
181
|
visible content — `clientOnly` is for components that must never run on the server.
|
|
176
182
|
Pass a **function** to `render` / `hydrate` / `renderToString` / `renderToStream`
|
|
177
183
|
(`render(() => <App />, root)`), never `render(<App />, root)`. `hydrate` when the
|
|
178
|
-
container already holds SSR HTML; `render` for an empty mount.
|
|
184
|
+
container already holds SSR HTML; `render` for an empty mount. Client mode (the
|
|
185
|
+
`@solidjs/vite-plugin` default) mounts with `render()` into an empty body — there is
|
|
186
|
+
**no server HTML and no hydration mismatch**, so React's mismatch defenses
|
|
187
|
+
("server-safe" initial values adopted after mount, two-pass rendering) are unnecessary
|
|
188
|
+
and can clobber persisted state; read `localStorage` / `matchMedia` directly when
|
|
189
|
+
creating the signal. `renderToString`
|
|
179
190
|
emits `<Loading>` fallbacks; async or lazy-route trees need
|
|
180
191
|
`await renderToStream(() => <App />)` (one consumer: `pipe` / `pipeTo` / `readable`).
|
|
181
192
|
When the app owns the document, emit `<HydrationScript />` once before app markup;
|
|
@@ -256,6 +267,7 @@ directory (default `src/`).
|
|
|
256
267
|
| Never write (Solid 1.x) | Write instead (Solid 2) |
|
|
257
268
|
|---|---|
|
|
258
269
|
| `import ... from "solid-js/store"` or `"solid-js/web"` | stores/`merge`/`omit` from `"solid-js"`; `render`/`hydrate`/`Portal`/`Dynamic` from `"@solidjs/web"` |
|
|
270
|
+
| `import type { JSX } from "solid-js"` / `JSX.Element` as the children type | `solid-js` exports no `JSX` namespace: children/returns are `Element` from `"solid-js"`; DOM-specific `JSX` types (`JSX.IntrinsicElements`, `JSX.CSSProperties`) from `"@solidjs/web"` |
|
|
259
271
|
| `createResource` | async `createMemo` + `<Loading>`/`<Errored>`; `refresh()`, `latest()`, `isPending()` |
|
|
260
272
|
| `createEffect(fn)` (one arg), `on(...)` | `createEffect(compute, apply)`; deps belong in `compute` |
|
|
261
273
|
| `onMount` | `onSettled` (return cleanup from its callback) |
|
|
@@ -55,6 +55,7 @@ Most React `useEffect` code should NOT become `createEffect`:
|
|
|
55
55
|
| Fetch data in an effect, copy into state | `createMemo(async () => ...)` + `<Loading>` |
|
|
56
56
|
| React to a user interaction | do the work in the event handler or an `action` |
|
|
57
57
|
| Sync one state into another | delete the second state; derive it |
|
|
58
|
+
| Reset/clear state when another reactive value changes | writable derivation: `createSignal(() => { source(); return initial; })` — not an effect calling the setter |
|
|
58
59
|
| Push a settled reactive value into a non-Solid system (DOM API, third-party widget, analytics, subscription) | `createEffect(compute, apply)` — this is the only real use |
|
|
59
60
|
| One-time setup after mount (`onMount`) | `onSettled(() => { ...; return cleanup })` |
|
|
60
61
|
| Measure DOM / observe size after paint | ref **directive factory** + `onSettled` (writes a *new* input; not an effect that copies state) |
|
|
@@ -222,6 +223,28 @@ Reads inside `apply` do not track. Extract every needed reactive value in `compu
|
|
|
222
223
|
and reading `user.name` there *runs once* and never retriggers. Compute-phase errors can be
|
|
223
224
|
intercepted with the bundle form `createEffect(compute, { effect, error })`.
|
|
224
225
|
|
|
226
|
+
A two-phase effect whose apply only calls a local signal setter is "state + effect" in
|
|
227
|
+
disguise — formally legal Solid 2, still rule 4 (`solid2-kit check` flags it). The reset
|
|
228
|
+
form of a writable derivation replaces it, and moves the source read to the read site
|
|
229
|
+
(under that site's boundaries) instead of an effect compute:
|
|
230
|
+
|
|
231
|
+
```tsx
|
|
232
|
+
// WRONG — signal synced by an effect; organizer() errors bypass the read site's <Errored>
|
|
233
|
+
const [signInError, setSignInError] = createSignal<string | null>(null);
|
|
234
|
+
createEffect(
|
|
235
|
+
() => organizer(),
|
|
236
|
+
(id) => {
|
|
237
|
+
if (id !== null) setSignInError(null);
|
|
238
|
+
},
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
// CORRECT — writable derivation: resets when organizer() changes; the setter still works
|
|
242
|
+
const [signInError, setSignInError] = createSignal<string | null>(() => {
|
|
243
|
+
organizer();
|
|
244
|
+
return null;
|
|
245
|
+
});
|
|
246
|
+
```
|
|
247
|
+
|
|
225
248
|
### Async data
|
|
226
249
|
|
|
227
250
|
```tsx
|
|
@@ -533,6 +556,20 @@ To turn those dev diagnostics into a hard gate, patch `console.warn` in the test
|
|
|
533
556
|
file to rethrow unexpected warnings — then any top-level reactive read or unowned write
|
|
534
557
|
an agent sneaks in fails the suite instead of scrolling by.
|
|
535
558
|
|
|
559
|
+
## Client mode: no server HTML, no hydration reflexes
|
|
560
|
+
|
|
561
|
+
The `@solidjs/vite-plugin` default (client mode, the `bare` tier) serves an empty-body
|
|
562
|
+
shell and mounts with `render()`. Nothing is server-rendered and nothing hydrates, so
|
|
563
|
+
**React's hydration-mismatch playbook does not transfer**: do not start signals with a
|
|
564
|
+
"server-safe" initial value and adopt `localStorage` / `matchMedia` after mount, do not
|
|
565
|
+
two-pass render, and there is no `suppressHydrationWarning`. Read client sources directly
|
|
566
|
+
when creating state — `createSignal(storedPreference())` is correct in client mode. A
|
|
567
|
+
deferred "safe" initial value is worse than unnecessary: if anything persists the value
|
|
568
|
+
(an effect writing `localStorage`), the placeholder **overwrites the user's stored
|
|
569
|
+
preference** before the real value is adopted. Mismatch only becomes a concept in
|
|
570
|
+
start/SSR mode, where `hydrate()` mounts onto server HTML — and the Solid answer there is
|
|
571
|
+
`isServer` / `clientOnly`, still not React-style deferred adoption.
|
|
572
|
+
|
|
536
573
|
## App stack (only if the project has these packages)
|
|
537
574
|
|
|
538
575
|
This kit's always-applied rules cover `solid-js` + `@solidjs/web` TSX. Official
|
|
@@ -547,8 +584,12 @@ Solid Router 0.x/1.x stand-ins.** Patterns below; fetch the matching pages from
|
|
|
547
584
|
{ "compilerOptions": { "jsx": "preserve", "jsxImportSource": "@solidjs/web" } }
|
|
548
585
|
```
|
|
549
586
|
|
|
550
|
-
|
|
551
|
-
`
|
|
587
|
+
`solid-js` does **not** export a `JSX` namespace — `import type { JSX } from "solid-js"`
|
|
588
|
+
is a type error (TS2305), and `JSX.Element` as the children type is a React reflex.
|
|
589
|
+
Children/return types are the renderer-neutral `Element` from `solid-js`
|
|
590
|
+
(`children?: Element`), alongside `Component` / `ParentProps` / `Accessor` / `Setter`.
|
|
591
|
+
DOM-specific `JSX` types (`JSX.IntrinsicElements`, `JSX.CSSProperties`) and
|
|
592
|
+
`ComponentProps` come from `@solidjs/web`. Vite: `import solid from "@solidjs/vite-plugin"`
|
|
552
593
|
with `start: true` / `ssr: true` / `serverFunctions: true` only when the
|
|
553
594
|
project already uses start mode. Tiers are `bare` → `basic` (router) →
|
|
554
595
|
`fullstack` (SSR + server functions); do not jump a tier without cause.
|
|
@@ -794,6 +835,8 @@ how to reuse. Prefer the form on the right.
|
|
|
794
835
|
| `createMemo(() => { void save(); return x(); })` / an effect that calls an action | invoke actions from handlers, not from memos or effects |
|
|
795
836
|
| `createSignal(props.value)` / `createStore(props.items)` | `createSignal(() => props.value)` / `createStore(() => props.items, fallback)` — a bare `props.x` at setup is a snapshot |
|
|
796
837
|
| `createRenderEffect` / `createTrackedEffect` as the default effect | two-phase `createEffect(compute, apply)` |
|
|
838
|
+
| `createEffect(() => source(), () => setError(null))` to reset a signal | `createSignal(() => { source(); return null; })` — the writable derivation resets on change; a sole-setter apply is rule 4 |
|
|
839
|
+
| a "server-safe" initial value adopted after mount (React hydration-mismatch fix) | client mode mounts with `render()` into an empty body — no hydration, no mismatch; read `localStorage` / `matchMedia` directly at signal creation |
|
|
797
840
|
|
|
798
841
|
```tsx
|
|
799
842
|
// WRONG — post-await read never subscribes; production can hang pending
|
|
@@ -913,7 +956,9 @@ always-applied rules installed alongside this skill.
|
|
|
913
956
|
- [ ] No props destructuring; no `props.` at component-body top level.
|
|
914
957
|
- [ ] Every reactive read (`signal()`, `props.x`, `store.x`) sits in JSX, a memo, an effect
|
|
915
958
|
compute, or a boundary — not the component body.
|
|
916
|
-
- [ ] No signal-synced-by-effect; derived values are functions or memos.
|
|
959
|
+
- [ ] No signal-synced-by-effect; derived values are functions or memos. Reset-on-change
|
|
960
|
+
is a writable derivation (`createSignal(() => { source(); return initial; })`), not
|
|
961
|
+
an effect calling the setter.
|
|
917
962
|
- [ ] Event handlers wrap setters (`onClick={() => setX(...)}`), use `currentTarget`,
|
|
918
963
|
and `onInput` for keystrokes. `innerHTML` is sanitized and not mixed with children.
|
|
919
964
|
- [ ] Async memos are passed as accessors (`user={user}`), not `user={user()}`, so
|
|
@@ -933,6 +978,8 @@ always-applied rules installed alongside this skill.
|
|
|
933
978
|
- [ ] Rest props via `omit`, not `{ ...props }`. Conditional `class` via objects, not string concat.
|
|
934
979
|
- [ ] Input filters write back to the DOM on reject (inputs do not rewind).
|
|
935
980
|
- [ ] No Solid 1.x imports or APIs (`solid-js/store`, `createResource`, `onMount`, `Suspense`, ...).
|
|
981
|
+
No `JSX` imported from `solid-js` — children/return types are `Element` from
|
|
982
|
+
`solid-js`; DOM `JSX` types come from `@solidjs/web`.
|
|
936
983
|
- [ ] Inspect children with `children()`; code-split with `lazy` + `<Loading>`; reactive
|
|
937
984
|
component choice with `dynamic()`. No `React.lazy`, no effects inside ref callbacks.
|
|
938
985
|
- [ ] Browser-only code uses `isServer` / `clientOnly`, not `typeof window`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "solid2-agent-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
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",
|