solid2-agent-kit 0.15.0 → 0.17.0
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 +9 -4
- package/bin/solid2-kit.mjs +120 -8
- package/files/commands/solid2-review.md +6 -4
- package/files/shared/agents-section.md +1 -1
- package/files/shared/rules-body.md +69 -37
- package/files/skills/solid-2/SKILL.md +112 -57
- package/files/skills/solid-2/references/official-docs.md +26 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -16,11 +16,11 @@ 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 `[
|
|
19
|
+
| Mechanical gate | `solid2-kit check` | same | Comment-stripped regex guard over `src/**/*.{ts,tsx,jsx}` (or explicit `[paths...]` — files are checked, directories are walked, a missing path fails): 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 (valueless `use:x` included), Solid 1.x `createEffect(fn)` / `createEffect(fn, init)` / `createMemo(fn, init)` signatures, `const [x] = createProjection(...)` (it returns the store, not a tuple), `vite-plugin-solid`, old-router `<Route>`/`<A>`/`<HashRouter>`/`<FileRoutes>`, `MetaProvider`, `Context.Provider`, camelCase style keys, `key` props (except on `@solidjs/meta` tags), `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 — import `JSX` from `@solidjs/web`, or use an aliased `Element` from `solid-js`), two-phase effects whose apply only writes a local signal (use a writable derivation), core `action(async ...)` with a plain async function instead of a generator (only `yield` re-enters the transaction), and `setStore(async ...)` on a `createStore` / `createOptimisticStore` setter (the draft is a synchronous transaction; dev throws `[ASYNC_STORE_SETTER]`) |
|
|
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` |
|
|
23
|
-
| Project-wiring gate | `solid2-kit doctor` | same | Config drift the source gate cannot see: React / `vite-plugin-solid` / `eslint-plugin-solid` / SolidStart deps in `package.json`, a Solid 1.x line of `solid-js` / `@solidjs/router` / `@solidjs/meta` (declared, or installed from the `latest` tag — Solid 2 releases are on `next`), `jsx` ≠ `preserve` or `jsxImportSource` ≠ `@solidjs/web` in tsconfig, Solid 1.x wiring in root config files, and stale installed guidance (kit updated but `sync` not re-run) |
|
|
23
|
+
| Project-wiring gate | `solid2-kit doctor` | same | Config drift the source gate cannot see: React / `vite-plugin-solid` / `eslint-plugin-solid` / SolidStart deps in `package.json`, a Solid 1.x line of `solid-js` / `@solidjs/router` / `@solidjs/meta` / `@solidjs/testing-library` / `@tanstack/solid-router` / `@tanstack/solid-query` (declared, or installed from the `latest` tag — Solid 2 releases are on `next`; TanStack Router's and Query's on `rc`), `jsx` ≠ `preserve` or `jsxImportSource` ≠ `@solidjs/web` in tsconfig, Solid 1.x wiring in root config files, and stale installed guidance (kit updated but `sync` not re-run) |
|
|
24
24
|
| Review command | `.cursor/commands/solid2-review.md` | `.claude/commands/solid2-review.md` | `/solid2-review` — on-demand deep review: runs both gates, then walks the diff against the skill checklist targeting the semantic failure classes regexes cannot see (post-`await` reads, effect misuse, unkeyed server rows, boundary placement, context snapshots) |
|
|
25
25
|
|
|
26
26
|
Managed blocks are delimited with `<!-- solid2-agent-kit:*:start/end -->` markers; everything
|
|
@@ -40,10 +40,15 @@ npx github:lightsound/solid2-agent-kit init
|
|
|
40
40
|
npx github:lightsound/solid2-agent-kit init --cursor
|
|
41
41
|
npx github:lightsound/solid2-agent-kit init --claude
|
|
42
42
|
|
|
43
|
-
# run the mechanical pattern gate (default source dir: src; or name files)
|
|
43
|
+
# run the mechanical pattern gate (default source dir: src; or name files and directories)
|
|
44
44
|
npx github:lightsound/solid2-agent-kit check
|
|
45
45
|
npx github:lightsound/solid2-agent-kit check --dir app
|
|
46
|
-
npx github:lightsound/solid2-agent-kit check src/App.tsx
|
|
46
|
+
npx github:lightsound/solid2-agent-kit check src/App.tsx src/routes
|
|
47
|
+
|
|
48
|
+
# only the files a change touched: a missing path always fails (exit 2), so
|
|
49
|
+
# --diff-filter=d keeps deleted files out of the list; -z / -0 keep paths with
|
|
50
|
+
# spaces whole, -r skips the run on an empty (or delete-only) diff
|
|
51
|
+
git diff --name-only -z --diff-filter=d main | xargs -0 -r npx github:lightsound/solid2-agent-kit check
|
|
47
52
|
|
|
48
53
|
# check the project wiring (deps, tsconfig, root configs)
|
|
49
54
|
npx github:lightsound/solid2-agent-kit doctor
|
package/bin/solid2-kit.mjs
CHANGED
|
@@ -405,10 +405,78 @@ function actionAsyncFindings(content) {
|
|
|
405
405
|
}));
|
|
406
406
|
}
|
|
407
407
|
|
|
408
|
+
// Solid 1.x signatures: `createEffect(fn)` (Solid 2 needs compute + apply),
|
|
409
|
+
// `createEffect(fn, initialValue)`, and `createMemo(fn, initialValue[, options])`
|
|
410
|
+
// (Solid 2's second memo argument is the options object). A second argument
|
|
411
|
+
// that is a primitive or array literal is an initial value; functions,
|
|
412
|
+
// identifiers, and object literals are left alone. A name this file imports
|
|
413
|
+
// from another library (effector's one-argument `createEffect`) or declares
|
|
414
|
+
// itself is not Solid's.
|
|
415
|
+
const INITIAL_VALUE_LITERAL = /^(?:-?\d|['"`[]|true\b|false\b|null\b|undefined\b)/;
|
|
416
|
+
const NOT_SOLID_CORE_MODULE = /^(?!(?:solid-js|@solidjs\/signals)$)/;
|
|
417
|
+
|
|
418
|
+
// Blank type-argument lists (`Record<string, number>`, `Map<K, () => void>`)
|
|
419
|
+
// so their commas do not split arguments. `<` counts only right after an
|
|
420
|
+
// identifier: formatted comparisons have a space before it.
|
|
421
|
+
function blankTypeArguments(text) {
|
|
422
|
+
let previous;
|
|
423
|
+
do {
|
|
424
|
+
previous = text;
|
|
425
|
+
text = text.replace(/(?<=[\w$])<(?:=>|[^<>=])*>/g, (typeArgs) => ' '.repeat(typeArgs.length));
|
|
426
|
+
} while (text !== previous);
|
|
427
|
+
return text;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function solid1SignatureFindings(content) {
|
|
431
|
+
const foreign = importedNames(content, NOT_SOLID_CORE_MODULE);
|
|
432
|
+
const matches = [];
|
|
433
|
+
for (const call of content.matchAll(
|
|
434
|
+
/(?<![.\w$])create(Effect|Memo)\s*(?:<(?:=>|[^<>]|<(?:=>|[^<>])*>)*>)?\s*\(/g,
|
|
435
|
+
)) {
|
|
436
|
+
const name = `create${call[1]}`;
|
|
437
|
+
if (foreign.has(name)) continue;
|
|
438
|
+
if (new RegExp(`\\b(?:function\\s*\\*?|const|let|var|class)\\s+${name}\\b`).test(content)) continue;
|
|
439
|
+
const open = call.index + call[0].length - 1;
|
|
440
|
+
const close = scanBalanced(content, open);
|
|
441
|
+
if (close === -1) continue;
|
|
442
|
+
const args = splitTopLevelArgs(blankTypeArguments(content.slice(open + 1, close)))
|
|
443
|
+
.map((arg) => arg.trim())
|
|
444
|
+
.filter(Boolean);
|
|
445
|
+
const initialValue = args.length >= 2 && INITIAL_VALUE_LITERAL.test(args[1]);
|
|
446
|
+
const legacy =
|
|
447
|
+
call[1] === 'Effect' ? args.length === 1 || initialValue : args.length >= 3 || initialValue;
|
|
448
|
+
if (legacy) matches.push({ index: call.index });
|
|
449
|
+
}
|
|
450
|
+
return matches;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// `key` is meaningless in Solid JSX except on the Solid Meta tags, where it
|
|
454
|
+
// names the tag's identity (`<Meta key="og-image" …>`). The enclosing tag is
|
|
455
|
+
// the nearest `<Name` (or `<Namespace.Name`) before the attribute.
|
|
456
|
+
const META_MODULE = /^@solidjs\/meta$/;
|
|
457
|
+
|
|
458
|
+
function reactKeyPropFindings(content) {
|
|
459
|
+
const metaTags = importedNames(content, META_MODULE);
|
|
460
|
+
const metaNamespaces = new Set(
|
|
461
|
+
[...content.matchAll(/\bimport\s+\*\s+as\s+([\w$]+)\s+from\s*['"]@solidjs\/meta['"]/g)].map((m) => m[1]),
|
|
462
|
+
);
|
|
463
|
+
const matches = [];
|
|
464
|
+
for (const attr of content.matchAll(/\skey=\{/g)) {
|
|
465
|
+
const opener = [...content.slice(0, attr.index).matchAll(/<([A-Za-z][\w$.]*)/g)].pop();
|
|
466
|
+
const tag = opener?.[1] ?? '';
|
|
467
|
+
if (metaTags.has(tag) || metaNamespaces.has(tag.split('.')[0])) continue;
|
|
468
|
+
matches.push({ index: attr.index });
|
|
469
|
+
}
|
|
470
|
+
return matches;
|
|
471
|
+
}
|
|
472
|
+
|
|
408
473
|
// TanStack Router for Solid exports names the Next.js / Solid Router 1.x rules
|
|
409
474
|
// target (`useRouter()`, `notFound()`, `<Navigate>`). A match is exempt only
|
|
410
475
|
// when this file binds that exact local name from one of these modules, so a
|
|
411
|
-
// stray Next or Router 1.x call in the same file is still reported.
|
|
476
|
+
// stray Next or Router 1.x call in the same file is still reported. TanStack
|
|
477
|
+
// Router 1.x (Solid 1) and 2.x (Solid 2) export the same names from the same
|
|
478
|
+
// module, so the major is invisible here; doctor's tanstack-router-version
|
|
479
|
+
// reads it from package.json / node_modules.
|
|
412
480
|
const TANSTACK_ROUTER_MODULE = /^@tanstack\/(?:solid-router|router-core)$/;
|
|
413
481
|
|
|
414
482
|
// Local names bound by `import [Default,] { a, b as c } from "<module>"`.
|
|
@@ -454,7 +522,19 @@ const CHECKS = [
|
|
|
454
522
|
id: 'jsx-namespace-import',
|
|
455
523
|
pattern: /import\s+(?:type\s+)?\{[^}]*\bJSX\b[^}]*\}\s*from\s+['"]solid-js['"]/g,
|
|
456
524
|
message:
|
|
457
|
-
'Solid 2 does not export a `JSX` namespace from "solid-js" (TS2305).
|
|
525
|
+
'Solid 2 does not export a `JSX` namespace from "solid-js" (TS2305). Import `JSX` from "@solidjs/web" (JSX.Element, JSX.IntrinsicElements, JSX.CSSProperties), or use `Element` from "solid-js" (aliased, e.g. `type Element as SolidElement`, where the DOM Element is also used).',
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
id: 'solid1-effect-memo-signature',
|
|
529
|
+
find: solid1SignatureFindings,
|
|
530
|
+
message:
|
|
531
|
+
'Solid 1.x signature. createEffect takes (compute, apply) — a single argument is an error in Solid 2. createMemo(fn, initialValue) is gone: the second argument is the options object; give the compute a default parameter instead (createMemo((prev = 0) => ...)).',
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
id: 'projection-destructure',
|
|
535
|
+
pattern: /\b(?:const|let|var)\s*\[[^\]]*\]\s*=\s*createProjection\b/g,
|
|
536
|
+
message:
|
|
537
|
+
'createProjection returns the store itself, not a [store, setter] tuple — destructuring it takes the first row. Write `const rows = createProjection(...)`.',
|
|
458
538
|
},
|
|
459
539
|
{
|
|
460
540
|
// A two-phase createEffect whose apply is nothing but one (optionally
|
|
@@ -508,8 +588,9 @@ const CHECKS = [
|
|
|
508
588
|
},
|
|
509
589
|
{
|
|
510
590
|
id: 'react-key-prop',
|
|
511
|
-
|
|
512
|
-
message:
|
|
591
|
+
find: reactKeyPropFindings,
|
|
592
|
+
message:
|
|
593
|
+
'React `key` prop has no meaning in Solid. Row identity belongs on <For keyed={...}>. (Only the @solidjs/meta tags take `key`, as the tag identity.)',
|
|
513
594
|
},
|
|
514
595
|
{
|
|
515
596
|
// `{todos().map((t) => <Row />)}` renders then recreates every row.
|
|
@@ -574,8 +655,14 @@ const CHECKS = [
|
|
|
574
655
|
message: 'Solid 1.x Vite plugin. Import `solid` from "@solidjs/vite-plugin".',
|
|
575
656
|
},
|
|
576
657
|
{
|
|
658
|
+
// Valueless attributes count too (`<input use:autofocus />`): followed by
|
|
659
|
+
// `/>`, `>`, or another attribute, unlike an unspaced object key
|
|
660
|
+
// (`{ on:true }`, `{ on:x = false }`, `{ attr:v as string }`); `as="font"`
|
|
661
|
+
// and SVG `in="…"` are still attributes. `prop:` is still a Solid 2
|
|
662
|
+
// namespace and is not listed.
|
|
577
663
|
id: 'solid1-jsx-namespace',
|
|
578
|
-
pattern:
|
|
664
|
+
pattern:
|
|
665
|
+
/(?<=\s)(?:use|on|oncapture|attr|bool):[A-Za-z][\w-]*(?==|\s*\/?>|\s+(?!(?:as|satisfies|in|instanceof)\b(?!\s*=))[A-Za-z_${])/g,
|
|
579
666
|
message:
|
|
580
667
|
'Solid 1.x JSX namespace. Use ref callbacks (and directive factories), camelCase event props, and standard attributes.',
|
|
581
668
|
},
|
|
@@ -787,10 +874,15 @@ function check() {
|
|
|
787
874
|
files = [];
|
|
788
875
|
scanned = pathArgs;
|
|
789
876
|
walked = false;
|
|
877
|
+
// A missing path is always an error: a deleted file, a misspelled file, and
|
|
878
|
+
// a renamed source directory look the same here, and only the list's
|
|
879
|
+
// producer knows which it was.
|
|
790
880
|
for (const arg of pathArgs) {
|
|
791
881
|
const path = resolve(target, arg);
|
|
792
882
|
if (!existsSync(path)) {
|
|
793
|
-
console.error(
|
|
883
|
+
console.error(
|
|
884
|
+
`solid2-kit check — path not found: ${path} (drop deleted files from a changed-file list with \`git diff --name-only --diff-filter=d\`)`,
|
|
885
|
+
);
|
|
794
886
|
process.exit(2);
|
|
795
887
|
}
|
|
796
888
|
if (statSync(path).isDirectory()) {
|
|
@@ -1129,8 +1221,10 @@ const BANNED_DEPS = {
|
|
|
1129
1221
|
};
|
|
1130
1222
|
|
|
1131
1223
|
// Packages whose npm `latest` dist-tag is still the Solid 1.x line (solid-js
|
|
1132
|
-
// 1.9, @solidjs/router 1.0, @solidjs/meta 0.29
|
|
1133
|
-
//
|
|
1224
|
+
// 1.9, @solidjs/router 1.0, @solidjs/meta 0.29, @solidjs/testing-library 0.8,
|
|
1225
|
+
// @tanstack/solid-router 1.x, @tanstack/solid-query 5.x): a bare
|
|
1226
|
+
// `pnpm add <name>` installs it. The Solid 2 releases are published under
|
|
1227
|
+
// `next` (TanStack: `rc` — solid-router 2.0.0-rc.x, solid-query 6.0.0-rc.x).
|
|
1134
1228
|
const SOLID2_LINE_PACKAGES = [
|
|
1135
1229
|
{ name: 'solid-js', id: 'solid-js-version', solid1: /^[\s^~=v]*[01](?:\.|$)/, fix: 'Install solid-js@next (^2).' },
|
|
1136
1230
|
{
|
|
@@ -1145,6 +1239,24 @@ const SOLID2_LINE_PACKAGES = [
|
|
|
1145
1239
|
solid1: /^[\s^~=v]*0(?:\.|$)/,
|
|
1146
1240
|
fix: 'Meta 0.x is built for Solid 1 (<MetaProvider> required); install @solidjs/meta@next (1.x).',
|
|
1147
1241
|
},
|
|
1242
|
+
{
|
|
1243
|
+
name: '@tanstack/solid-router',
|
|
1244
|
+
id: 'tanstack-router-version',
|
|
1245
|
+
solid1: /^[\s^~=v]*[01](?:\.|$)/,
|
|
1246
|
+
fix: 'TanStack Router 1.x has a solid-js ^1.9 peer; install @tanstack/solid-router@rc (2.x, solid-js 2 peer).',
|
|
1247
|
+
},
|
|
1248
|
+
{
|
|
1249
|
+
name: '@tanstack/solid-query',
|
|
1250
|
+
id: 'tanstack-query-version',
|
|
1251
|
+
solid1: /^[\s^~=v]*[0-5](?:\.|$)/,
|
|
1252
|
+
fix: 'TanStack Query 5.x has a solid-js ^1.6 peer; install @tanstack/solid-query@rc (6.x, solid-js 2 peer).',
|
|
1253
|
+
},
|
|
1254
|
+
{
|
|
1255
|
+
name: '@solidjs/testing-library',
|
|
1256
|
+
id: 'testing-library-version',
|
|
1257
|
+
solid1: /^[\s^~=v]*0(?:\.|$)/,
|
|
1258
|
+
fix: 'Testing Library 0.x renders through solid-js/web (Solid 1); install @solidjs/testing-library@next (1.x, solid-js 2 peer).',
|
|
1259
|
+
},
|
|
1148
1260
|
];
|
|
1149
1261
|
|
|
1150
1262
|
function installedVersion(target, name) {
|
|
@@ -39,7 +39,7 @@ Steps:
|
|
|
39
39
|
- app-wide state as a module-level signal/store instead of a provider at the root
|
|
40
40
|
of `App` (shared across SSR requests);
|
|
41
41
|
- value-form `createOptimisticStore([])` / `createOptimistic(v)` holding durable
|
|
42
|
-
data (writes vanish on settle) instead of the function form refreshed after `yield`;
|
|
42
|
+
data (writes vanish on settle) <!-- upstream:optimistic-value-overlay --> instead of the function form refreshed after `yield`;
|
|
43
43
|
- route `preload` returning data read as `props.data` (captured once per match)
|
|
44
44
|
instead of `void getX(params.id)` + `createMemo(() => getX(props.params.id))`;
|
|
45
45
|
- `throw redirect()` / `return reload()` in server functions called without Solid
|
|
@@ -47,10 +47,12 @@ Steps:
|
|
|
47
47
|
data), and POST forms built on a bare function's `.url` (TS2339) instead of a
|
|
48
48
|
router `action`;
|
|
49
49
|
error reporting done as a side effect inside `fallback` instead of
|
|
50
|
-
`configureClientErrors` / `configureServerErrors` / `render(
|
|
50
|
+
`configureClientErrors` / `configureServerErrors` / `render(fn, root, undefined, { onError })`
|
|
51
|
+
(options are `render`'s 4th argument — a 3rd-argument object is `init`);
|
|
51
52
|
- store setter callbacks that `await` (the draft closes when the callback returns —
|
|
52
|
-
`[ASYNC_STORE_SETTER]`), store setters called at
|
|
53
|
-
`latest()` used as a
|
|
53
|
+
`[ASYNC_STORE_SETTER]` <!-- upstream:async-store-setter-throws -->), store setters called at
|
|
54
|
+
component-body top level <!-- upstream:store-write-owned-scope -->, and `latest()` used as a
|
|
55
|
+
null-safe read of an unsettled source <!-- upstream:latest-throws-unsettled -->;
|
|
54
56
|
- nested fetches assumed to waterfall, or `<Loading>` lifted along with a lifted fetch;
|
|
55
57
|
- mutations on a value read through `live()` that settle on `yield save()` alone or
|
|
56
58
|
`refresh()` the live-derived store, instead of `yield until(predicate, { timeout })`
|
|
@@ -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 async values as values (`user={user()}`) — JSX props are lazy; types stay `User`, not `Promise<User>`. `<Loading>` wraps the read, not the memo. Nested fetches run in parallel (components do not suspend). `isPending` is a per-expression question (`isPending(() => props.story)` / `isPending(selectedId)`), not a global spinner; `latest(selectedId)` is opt-in so highlight and content stay consistent by default. `<Errored>` heals when the source succeeds. Pass a function to `render`/`hydrate`: `render(() => <App />, root)`. Component teardown is `onSettled`, not `onCleanup` — return the cleanup; `onCleanup`, memo/effect creation, and `flush()` throw inside the `onSettled` callback in dev. Effect `apply` functions use block bodies (`(t) => { document.title = t; }`): any return value other than a cleanup function halts reactivity. Stores are written only through the setter's draft — writes to the store itself are silently ignored, and `Map`/`Set`/`Date` inside a store are not tracked. Shared state, app-wide included, is created in a provider at the root of `App` (`createContext<T>()` + `useX()`), never a module-level signal/store (one instance serves every SSR request). Errors thrown out of an `action` never reach `<Errored>` — catch them in the action or `.catch` the call. One router instance; in-app navigation is `useNavigate` / `<a href>`, not `window.location`. Types: `solid-js` exports no `JSX` namespace —
|
|
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 async values as values (`user={user()}`) — JSX props are lazy; types stay `User`, not `Promise<User>`. `<Loading>` wraps the read, not the memo. Nested fetches run in parallel (components do not suspend). `isPending` is a per-expression question (`isPending(() => props.story)` / `isPending(selectedId)`), not a global spinner; `latest(selectedId)` is opt-in so highlight and content stay consistent by default. `<Errored>` heals when the source succeeds. Pass a function to `render`/`hydrate`: `render(() => <App />, root)`. Component teardown is `onSettled`, not `onCleanup` — return the cleanup; `onCleanup`, memo/effect creation, and `flush()` throw inside the `onSettled` callback in dev. Effect `apply` functions use block bodies (`(t) => { document.title = t; }`): any return value other than a cleanup function halts reactivity. Stores are written only through the setter's draft — writes to the store itself are silently ignored, and `Map`/`Set`/`Date` inside a store are not tracked. Shared state, app-wide included, is created in a provider at the root of `App` (`createContext<T>()` + `useX()`), never a module-level signal/store (one instance serves every SSR request). Errors thrown out of an `action` never reach `<Errored>` — catch them in the action or `.catch` the call. One router instance; in-app navigation is `useNavigate` / `<a href>`, not `window.location`. Types: `solid-js` exports no `JSX` namespace — markup types are `JSX.Element` from `@solidjs/web` or `Element` from `solid-js` (alias it `SolidElement` next to DOM code) <!-- upstream:jsx-namespace-absent -->. 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). Client → server is additive (same setters + `action` + server functions): do not rewrite App with loading/error branches, disable optimistic rows until ack, invent tRPC, or refetch in `hydrate`/`onSettled`. The graph continues across the wire (`live()` owns the client connection; `<Reveal>` not a delayed stream). A server bundle uses `handleRequest` / Fetchable `fetch`; client-only start is static `dist/client`. One browser-only value takes `ssrSource` on the memo/signal/derived store (`client`/`hybrid`), not a `clientOnly` component split. Router link state is automatic (`aria-current`/`data-active`/`data-pending` + CSS; `useLinkState`/`useIsRouting` in JSX); search params via `useSearchParams`; router forms validate on the server (`throw respond({issues},{400})`) with inline errors from `useSubmissions`. Confirmations on another channel — fire-and-forget sends, and mutations whose result is read through `live()` — wait with `yield until(predicate, {timeout})`, not `refresh()`. Declare `live(GET(fn))` (`live()` outermost). Debounce at the handler; inputs feeding a request bind `latest(query)`. Dev diagnostics are errors: `[STRICT_READ_UNTRACKED]` means a read outside a tracking scope, `[ASYNC_STORE_SETTER]` means a store setter callback awaited (the draft is a synchronous transaction — await first, then write) <!-- upstream:async-store-setter-throws -->, and a held write needs `isPending`/`latest`/optimistic/`affects` feedback or dev + attribution reports `[SILENT_HOLD]`. Error monitoring is `configureClientErrors` / `configureServerErrors` (or per root `render(fn, root, undefined, { onError })` — options are the 4th argument), not a side effect inside `<Errored fallback>`; the server hook must return a reference or nothing, never the error itself. `<Dynamic>` is deprecated — use `dynamic()`. <!-- upstream:dynamic-deprecated --> Observability is on by default in development: whenever a change touches stores, lists, async, actions, or effects, keep the dev console free of `[CODE]` findings and run an attribution session (`/__solid/diagnostics` `begin` → interact → `whyDidRun` / `costs` → `end` with `@solidjs/diagnostics` installed, otherwise `isDev`-guarded `attribution.enable({ log: false })` + `why()` / `costs()`) before finishing. `vite dev` is always the dev build; `solid({ observe: true })` is a production opt-in.
|
|
@@ -18,7 +18,7 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
18
18
|
Rest props: `omit(props, "label", "value")` (or a predicate: `omit(props, (k) => k.startsWith("data-"))`),
|
|
19
19
|
never `const rest = { ...props }` (a snapshot). `merge` / `omit` return read-only live
|
|
20
20
|
views — assigning onto them is a silent no-op; `{ ...merged }` gives an own object that
|
|
21
|
-
never updates again.
|
|
21
|
+
never updates again. <!-- upstream:merge-omit-readonly -->
|
|
22
22
|
JSX `{...rest}` is fine once `rest` is a reactive proxy. Rule of thumb: the string `props.`
|
|
23
23
|
must never appear at component-body top level *as a read* (`const x = props.x`); nested
|
|
24
24
|
functions (`() => props.x`, `omit(props, ...)`, `children(() => props.children)`) are fine.
|
|
@@ -42,7 +42,8 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
42
42
|
before a flush, use the setter updater (`setCount((c) => c + 1)`); `setCount(count() + 1)`
|
|
43
43
|
reads the last *committed* value and drops the other staged writes. To store a *function*
|
|
44
44
|
in a signal, wrap it: `setHandler(() => myHandler)` — `setHandler(myHandler)` treats it
|
|
45
|
-
as an updater.
|
|
45
|
+
as an updater. The initial value too: `createSignal(() => myHandler)` —
|
|
46
|
+
`createSignal(myHandler)` is the derived form (it calls `myHandler` and stores the result).
|
|
46
47
|
5. `class`, not `className`; `for`, not `htmlFor`. `class` accepts strings, conditional
|
|
47
48
|
objects, and nested arrays: `class={["btn", { active: selected() }]}`. Put *conditional*
|
|
48
49
|
names in an object — `class={`btn ${on() ? "on" : ""}`}`, `clsx(...)`, and
|
|
@@ -60,13 +61,15 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
60
61
|
that writes the event object; wrap it: `onClick={() => setCount((c) => c + 1)}`.
|
|
61
62
|
Debounce at the handler (`onInput={debounce(...)}`), never as an effect copying
|
|
62
63
|
one signal into another.
|
|
63
|
-
7. **Lists use `<For>`**, never `{list().map(...)}` in reactive JSX and never `key` props.
|
|
64
|
+
7. **Lists use `<For>`**, never `{list().map(...)}` in reactive JSX and never `key` props (only `@solidjs/meta` tags take `key`, as the tag's identity).
|
|
64
65
|
`{todos().map((t) => <Row todo={t} />)}` *renders*, then recreates every row on each
|
|
65
66
|
update. Row identity: default = item reference; `keyed={(item) => item.id}` = key function
|
|
66
67
|
(child receives item as an accessor); `keyed={false}` = positional. **Rows from
|
|
67
68
|
server/refetched data (fresh object references on every update — fetch results,
|
|
68
69
|
subscription payloads) must use a key function on a stable id**, or every update
|
|
69
|
-
recreates every row (dev + attribution flags the rebuilds as `[UNSTABLE_LIST_IDENTITY]`).
|
|
70
|
+
recreates every row (dev + attribution flags the rebuilds as `[UNSTABLE_LIST_IDENTITY]`).
|
|
71
|
+
Usually better still: load them into a function-form store (rule 20), whose proxies keep
|
|
72
|
+
identity per id, and keep default keying. Reference keying is for local arrays whose item identities are
|
|
70
73
|
stable (e.g. store rows). Match the child signature to the mode: default item is the
|
|
71
74
|
**raw** value (`todo.title`); `keyed={(t) => t.id}` / `keyed={false}` pass an
|
|
72
75
|
**accessor** (`todo().title`). Mixing those still “runs” (you render a function, or
|
|
@@ -83,8 +86,8 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
83
86
|
block body — make it a derivation. Register an effect's cleanup by returning it
|
|
84
87
|
from `apply` — `onCleanup` there never runs (`[NO_OWNER_CLEANUP]`).
|
|
85
88
|
Single-argument `createEffect(fn)` is an error in Solid 2. Do not substitute
|
|
86
|
-
`createTrackedEffect` for that — it is
|
|
87
|
-
|
|
89
|
+
`createTrackedEffect` for that — it is `@deprecated` since 2.0.0-rc.9 (kept only for
|
|
90
|
+
1.x migration) <!-- upstream:tracked-effect-deprecated -->. Skip the initial run with `{ defer: true }`. Most React `useEffect` code should not become an effect at all — see the skill.
|
|
88
91
|
9. **Stores update by mutating a draft**: `setStore(draft => { draft.user.name = "Ada" })`.
|
|
89
92
|
Never rebuild with spreads — that destroys property-level subscriptions. The return
|
|
90
93
|
form is for shapes where mutation is awkward — most commonly removal
|
|
@@ -93,6 +96,9 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
93
96
|
`reconcile` or the derived form (dev + attribution flags the store-level waste as
|
|
94
97
|
`[IMMUTABLE_UPDATE_IN_STORE]`). Stores are
|
|
95
98
|
**property reads** (`todos.length`, `todo.title`), not accessors — never `todos()`.
|
|
99
|
+
The setter takes only a function: `setState("a", v)` and the 1.x shallow merge
|
|
100
|
+
`setState({ a: 1 })` throw `fn is not a function`, and an object *returned* from the
|
|
101
|
+
callback replaces the whole store (keys it omits are deleted).
|
|
96
102
|
The store itself is read-only: `todos.push(x)`, `state.count++`, or
|
|
97
103
|
`todo.done = !todo.done` outside a setter is **silently ignored** (no throw, no
|
|
98
104
|
warning, and TypeScript accepts it) — every write goes through the setter's draft.
|
|
@@ -101,10 +107,10 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
101
107
|
array), or assign a new instance in the setter (`draft.tags = new Set([...draft.tags, t])`).
|
|
102
108
|
The setter callback is a **synchronous** transaction: `setStore(async (draft) => …)`
|
|
103
109
|
throws `[ASYNC_STORE_SETTER]` in dev (writes after the first `await` were silently
|
|
104
|
-
lost before)
|
|
110
|
+
lost before) <!-- upstream:async-store-setter-throws -->. `await` first, then call the setter (inside an `action`, `yield` first).
|
|
105
111
|
Mechanically enforced by `solid2-kit check`. A store setter called at
|
|
106
112
|
component-body top level is a write in an owned scope (`[REACTIVE_WRITE_IN_OWNED_SCOPE]`,
|
|
107
|
-
same as a signal setter) — seed the initial shape through the `createStore` argument.
|
|
113
|
+
same as a signal setter) <!-- upstream:store-write-owned-scope --> — seed the initial shape through the `createStore` argument.
|
|
108
114
|
10. **Async data is an async computation**: `createMemo(async () => ...)` (or
|
|
109
115
|
`createMemo(() => fetchUser(id()))`) read under `<Loading>` / `<Errored>`.
|
|
110
116
|
**Fetch high, block low** — creating/reading are like sync; consuming (await)
|
|
@@ -116,18 +122,22 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
116
122
|
`<Child user={user()} />` then `{props.user.name}` under the child's
|
|
117
123
|
`<Loading>` is the colorless form. `const u = user()` at the parent *is* a
|
|
118
124
|
read and throws/snapshots there. Pass the memo itself only when the child
|
|
119
|
-
must `refresh()` that source —
|
|
125
|
+
must `refresh()` that source — type that prop `SourceAccessor<User>` (`Accessor<User>`
|
|
126
|
+
is TS2345 at `refresh`) — `isPending(() => props.user)` works on the
|
|
120
127
|
value. Read every reactive input **before the first
|
|
121
128
|
`await`** — post-`await` reads do not subscribe, and in production the computation can
|
|
122
129
|
sit pending with no retry. No `useEffect` + `setState` fetching, no `createResource`.
|
|
123
130
|
`<Loading>` wraps the data slot, not page chrome. After first paint it keeps content
|
|
124
131
|
during refetch (`isPending` for the indicator). Use `on={id()}` (the *value*, not the
|
|
125
|
-
accessor) only when that identity change should show the fallback again.
|
|
132
|
+
accessor) only when that identity change should show the fallback again. `on` compares
|
|
133
|
+
with `!==` (several keys: one string such as `` `${a()}/${b()}` ``, never a fresh array)
|
|
134
|
+
and takes effect only when no reader outside the boundary waits on the same write
|
|
135
|
+
(that is how one slow panel stops holding the page). Do not start `fetch` (or any request) at component-body top
|
|
126
136
|
level — that runs once at mount and is not a reactive source. Do not `try/catch` `NotReadyError` around a read, and do not
|
|
127
137
|
use `loadingValue` / `seedLoadingValue` as the default first-flight UI — those skip
|
|
128
138
|
`<Loading>`. `{latest(() => x())}` is a preview, not the visible answer, and not a
|
|
129
139
|
null-safe probe: `latest(user)` before the first value throws `NotReadyError` in every
|
|
130
|
-
scope (event handlers included) — it never returns `undefined` for an unsettled source.
|
|
140
|
+
scope (event handlers included) — it never returns `undefined` for an unsettled source. <!-- upstream:latest-throws-unsettled -->
|
|
131
141
|
Default navigation holds: keep `selectedId()` so highlight and content stay
|
|
132
142
|
consistent; `latest(selectedId)` only when the highlight should move first.
|
|
133
143
|
Pair `class={{ pending: isPending(selectedId) }}` with a short CSS
|
|
@@ -196,8 +206,9 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
196
206
|
JSX (`<Show>`, ternary, `<Switch>`/`<Match>`). Early returns on genuinely non-reactive
|
|
197
207
|
values (build-time config, missing env) are fine. `<Switch>` is first-wins: later
|
|
198
208
|
truthy `<Match>`es are skipped, not “all matching branches”.
|
|
199
|
-
16.
|
|
200
|
-
|
|
209
|
+
16. Plain text is ordinary JSX children (`<p>{message()}</p>`); `textContent` is for an
|
|
210
|
+
element whose entire content is one text value, never together with children. Use
|
|
211
|
+
`innerHTML` only for trusted or sanitized markup — never interpolate user input into it, never React's
|
|
201
212
|
`dangerouslySetInnerHTML`, and never combine `innerHTML` with JSX children (they fight
|
|
202
213
|
over the same contents).
|
|
203
214
|
17. **Never narrow a reactive read with a non-null assertion** (`error()!.message`,
|
|
@@ -224,7 +235,8 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
224
235
|
overlay; otherwise a toast), and `.catch` the call in the handler for the rest.
|
|
225
236
|
Error monitoring is a hook, not a side effect in the fallback: the boundary-caught
|
|
226
237
|
failures no global handler sees reach `configureClientErrors({ onError })` from
|
|
227
|
-
`solid-js` (or `render(
|
|
238
|
+
`solid-js` (or per root: `render(() => <App />, root, undefined, { onError })` — options
|
|
239
|
+
are the 4th argument, the 3rd is `init` — and `hydrate(fn, root, { onError })`) and `configureServerErrors({ onError })`
|
|
228
240
|
from `@solidjs/web` — do not call `captureException` inside `<Errored fallback>`.
|
|
229
241
|
The server hook's return value replaces the sanitized error on the wire: return
|
|
230
242
|
nothing or a reference id, never the error itself (message/stack/secrets would
|
|
@@ -245,6 +257,9 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
245
257
|
On a derived store, `refresh(store)` re-runs the derivation (`fn`); refreshing a
|
|
246
258
|
plain signal accessor is a no-op. `refresh` returns a promise for the target's next
|
|
247
259
|
settled state (`await refresh(user)`, or `yield refresh(x)` inside an action).
|
|
260
|
+
Polling is `onSettled(() => { const t = setInterval(() => refresh(stats), 30_000); return () => clearInterval(t); })`.
|
|
261
|
+
Two components that each create the same memo make two requests (no automatic
|
|
262
|
+
dedup) — create it once and pass it down or through context (router: `query`).
|
|
248
263
|
These are three different APIs: core `refresh(source)` reruns a reactive source;
|
|
249
264
|
router `revalidate(getUser.key)` invalidates the query cache; server-function
|
|
250
265
|
`return reload({ revalidate: "todos" })` asks the integration to refresh cached
|
|
@@ -255,13 +270,19 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
255
270
|
apply `reconcile(fresh, "id")(draft.slot)` inside a draft setter. Plain wholesale
|
|
256
271
|
assignment (`draft.todos = fresh`) renders correctly but notifies every subscriber
|
|
257
272
|
under the path and destroys row identity — same class of problem as unkeyed `<For>`.
|
|
273
|
+
`createProjection` returns the store itself, not a tuple (`const [rows] = createProjection(…)`
|
|
274
|
+
type-checks and yields row 0). `reconcile` onto a single-object store whose `id`
|
|
275
|
+
changes throws in dev (`Cannot reconcile states with different identity`) — switch records
|
|
276
|
+
with the function form (`createStore(() => getUser(id()), seed)`) or a returned object.
|
|
258
277
|
21. **Solid inputs do not rewind the DOM.** `value={v()}` writes only when `v` changes; a
|
|
259
278
|
React-style controlled input that ignores invalid keystrokes silently breaks. To reject
|
|
260
279
|
input, write back explicitly:
|
|
261
280
|
`onInput={(e) => { if (valid(e.currentTarget.value)) setV(e.currentTarget.value); else e.currentTarget.value = v(); }}`.
|
|
262
281
|
22. Miscellaneous defaults: SSR-stable element ids come from `createUniqueId()` (never
|
|
263
282
|
`Math.random()` or hardcoded duplicates); pass store data to `structuredClone` /
|
|
264
|
-
`postMessage` / logs via `snapshot(store)` (proxies fail or leak reactivity)
|
|
283
|
+
`postMessage` / logs via `snapshot(store)` (proxies fail or leak reactivity) — a
|
|
284
|
+
view, not a copy: unchanged subtrees are the store's own objects, so mutating it (a
|
|
285
|
+
library's `arr.sort()`) silently changes the store; copy with `structuredClone(snapshot(store))`; render
|
|
265
286
|
modals/tooltips/overlays through `<Portal>` from `@solidjs/web`; mutations whose writes
|
|
266
287
|
cross an async gap default to `action` + `createOptimistic`/`createOptimisticStore` —
|
|
267
288
|
the sync mutation *is* the prediction (an overlay, discarded on settle). Durable data
|
|
@@ -269,7 +290,7 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
269
290
|
or one that reads a separate `createStore`); the value form
|
|
270
291
|
`createOptimisticStore([])` / `createOptimistic(false)` is a pure overlay with no
|
|
271
292
|
durable layer — action writes vanish on settle (a saved row disappears) and writes
|
|
272
|
-
outside an action do not stick — so use it only for in-flight flags. Write the
|
|
293
|
+
outside an action do not stick <!-- upstream:optimistic-value-overlay --> — so use it only for in-flight flags. Write the
|
|
273
294
|
body as a generator (`function*` / `async function*`) suspending on `yield`, never a
|
|
274
295
|
plain `async` function (`solid2-kit check` flags that). After a bare `await`, put a
|
|
275
296
|
bare `yield` before the next write **and** before anything that creates a reader
|
|
@@ -292,7 +313,12 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
292
313
|
after touching `package.json` / tsconfig / root configs; it fails on React and Solid 1.x
|
|
293
314
|
wiring. Server vs browser:
|
|
294
315
|
`isServer` / `isDev` from `@solidjs/web`, or `clientOnly(() => import("./Widget"))` for
|
|
295
|
-
browser-only components — never `typeof window` as the SSR boundary.
|
|
316
|
+
browser-only components — never `typeof window` as the SSR boundary. `isServer` picks
|
|
317
|
+
code paths, never markup: a JSX branch on it (or on `Date.now()` / `Math.random()`) is
|
|
318
|
+
a hydration mismatch. On the server the component body and memo/effect *computes*
|
|
319
|
+
run; effect `apply`, `onSettled`, and handlers do not. A `clientOnly` placeholder is
|
|
320
|
+
its `fallback` prop (`<Chart fallback={<Skeleton />} />`, rendered by SSR), not a
|
|
321
|
+
`<Loading>` around it; named exports take `{ export: "Chart" }`. Default
|
|
296
322
|
`clientOnly` starts loading at declaration; `{ lazy: true }` defers until first
|
|
297
323
|
render. `NoHydration` / `Hydration` split hydration *ownership*; they do not choose
|
|
298
324
|
visible content — `clientOnly` is for components that must never run on the server.
|
|
@@ -318,7 +344,8 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
318
344
|
fallback body), not event-time mutations. `JSON.stringify(store)`
|
|
319
345
|
/ `structuredClone(store)` without `snapshot(store)` can throw or leak proxies. Async SSR
|
|
320
346
|
without `<Loading>` *works* but blocks the HTML shell until every read settles — wrap
|
|
321
|
-
reads, or stream with `renderToStream`.
|
|
347
|
+
reads, or stream with `renderToStream`. A read that must be in the first HTML (SEO,
|
|
348
|
+
link previews) takes `{ deferStream: true }` on its memo; keep the boundary. `pipe` / `pipeTo` / `readable` each consume
|
|
322
349
|
a stream render — use exactly one. `createRoot` is for tests, libraries, and
|
|
323
350
|
non-render entry points; inside a component let `render` / the component owner
|
|
324
351
|
own the scope. Async reads inside `<Portal>` start on the client — hoist the
|
|
@@ -333,7 +360,7 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
333
360
|
because the named export is not a call-site literal. Select a component from reactive
|
|
334
361
|
state with `dynamic(() => ...)` from `@solidjs/web`
|
|
335
362
|
(stable identity); `<Dynamic component={...}>` is deprecated (`@deprecated` in the
|
|
336
|
-
types since 2.0.0-rc.9, still shipped in 2.0)
|
|
363
|
+
types since 2.0.0-rc.9, still shipped in 2.0) <!-- upstream:dynamic-deprecated -->. If the project has `@solidjs/router`, routes are `createRouter({ routes })`
|
|
337
364
|
at module scope — not JSX `<Route>` / `<A>` / `<HashRouter>`. That package's `action` /
|
|
338
365
|
`query` are URL-addressable POST forms and a read cache — not core `action` /
|
|
339
366
|
`refresh` from `solid-js`. Router mutations: `<form action={save} method="post">`
|
|
@@ -369,12 +396,13 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
369
396
|
`<Title>` / `<Meta>` from `@solidjs/meta` with **no** `MetaProvider`. A static
|
|
370
397
|
`<title>` in the document shell is the fallback; do not hardcode other tags
|
|
371
398
|
(`<meta name="description">`) that Solid Meta should manage.
|
|
372
|
-
|
|
373
|
-
`Response.json` — a raw `Response` is HTTP-handler control
|
|
374
|
-
callers should receive the value.
|
|
399
|
+
Success metadata is `return respond(value, { status: 201, headers })` from a
|
|
400
|
+
`"use server"` function, not `Response.json` — a raw `Response` is HTTP-handler control
|
|
401
|
+
flow; scripted callers should receive the value. A *returned* `respond()` resolves the
|
|
402
|
+
call whatever its status, so a failure is `throw respond(body, { status: 400 })`. Production sanitizes every thrown value that
|
|
375
403
|
crosses the wire — server-function throws **and** SSR render errors (an `<Errored>`
|
|
376
404
|
fallback printing `err().message` shows `"Internal Server Error"` in a production
|
|
377
|
-
server render) — so intentional client-facing failures are `markSafeError(...)` or
|
|
405
|
+
server render) <!-- upstream:ssr-error-sanitized --> — so intentional client-facing failures are `markSafeError(...)` or
|
|
378
406
|
`throw respond(body, { status })`. Reads are function calls too (`await getUser(id)`
|
|
379
407
|
on a `GET()`-declared read; the method mirrors the operation). `redirect()` /
|
|
380
408
|
`reload()` are signals for an integration: Solid Router's `action` / `query` and the
|
|
@@ -399,14 +427,18 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
399
427
|
display). Do not write subscribe/unsubscribe in the component around `live()`.
|
|
400
428
|
When the app has a server bundle (`ssr` or server functions), production is
|
|
401
429
|
`handleRequest(request)` from the built `dist/server/server.js` (or its
|
|
402
|
-
Fetchable `fetch`); platform Vite plugins adopt that handler
|
|
403
|
-
|
|
430
|
+
Fetchable `fetch`); platform Vite plugins adopt that handler. Node: set
|
|
431
|
+
`start: { node: true }` and run `node dist/server/node.js` (serves `dist/client`,
|
|
432
|
+
reads `PORT` / `HOST`; its exported `listener` mounts in an existing Express /
|
|
433
|
+
Fastify server) — `server.js` is only the handler module. Client-only start mode is static `dist/client` —
|
|
404
434
|
there is no handler to wrap. Request middleware is
|
|
405
435
|
`start: { middleware: "./src/middleware.ts" }`, not Express `app.use`. Do not return
|
|
406
436
|
components from `"use server"` unless the project already enabled the
|
|
407
|
-
experimental `serverFunctions.components` flag.
|
|
408
|
-
|
|
409
|
-
|
|
437
|
+
experimental `serverFunctions.components` flag. Server-function arguments are JSON:
|
|
438
|
+
send `Date` / `Map` / `Set` as ISO strings / arrays (they throw otherwise; one `File` /
|
|
439
|
+
`Blob` / `FormData` argument travels natively). `enableRichArguments()` from
|
|
440
|
+
`@solidjs/web/server-functions/rich-args` (at `src/App.tsx` module scope) lifts that,
|
|
441
|
+
but in rc.9 importing it fails `vite build` (`"./client" is not exported`) <!-- upstream:rich-args-vite-build https://github.com/solidjs/solid/issues/3627 -->. Declare live reads as `live(GET(fn))` —
|
|
410
442
|
`live()` outermost. A plain `async function*` server function is an event stream on
|
|
411
443
|
one connection (no reconnect when it drops); `live()` is one value that changes over
|
|
412
444
|
time and reconnects. `live()` connection state is
|
|
@@ -437,7 +469,7 @@ directory (default `src/`).
|
|
|
437
469
|
| `solid/event-handlers` | still valid | rule 6 |
|
|
438
470
|
| `solid/jsx-no-undef`, `jsx-no-duplicate-props` | covered by TypeScript | project typecheck |
|
|
439
471
|
| `solid/prefer-classlist` | obsolete — `classList` removed | `solid2-kit check` bans `classList` |
|
|
440
|
-
| `solid/no-unknown-namespaces` | obsolete — `on:`/`use:`/`attr:` removed in 2.0 | banned-API table + `solid2-kit check` |
|
|
472
|
+
| `solid/no-unknown-namespaces` | obsolete — `on:`/`use:`/`attr:` removed in 2.0 (`prop:` remains) | banned-API table + `solid2-kit check` |
|
|
441
473
|
| `solid/no-proxy-apis` | obsolete — Solid 2 requires Proxy | not carried |
|
|
442
474
|
|
|
443
475
|
## Banned Solid 1.x APIs (Solid 2 replacements)
|
|
@@ -445,15 +477,15 @@ directory (default `src/`).
|
|
|
445
477
|
| Never write (Solid 1.x) | Write instead (Solid 2) |
|
|
446
478
|
|---|---|
|
|
447
479
|
| `import ... from "solid-js/store"` or `"solid-js/web"` | stores/`merge`/`omit` from `"solid-js"`; `render`/`hydrate`/`Portal`/`Dynamic` from `"@solidjs/web"` |
|
|
448
|
-
| `import type { JSX } from "solid-js"`
|
|
480
|
+
| `import type { JSX } from "solid-js"` | `solid-js` exports no `JSX` namespace: markup types are `JSX.Element` from `"@solidjs/web"` (with `JSX.IntrinsicElements`, `JSX.CSSProperties`) or `Element` from `"solid-js"` — import the latter as `type Element as SolidElement` where the DOM `Element` is also used, or it shadows it <!-- upstream:jsx-namespace-absent --> |
|
|
449
481
|
| `createResource` | async `createMemo` + `<Loading>`/`<Errored>`; `refresh()`, `latest()`, `isPending()` |
|
|
450
|
-
| `createEffect(fn)` (one arg), `on(...)` | `createEffect(compute, apply)`; deps belong in `compute` |
|
|
482
|
+
| `createEffect(fn)` (one arg), `on(...)`, `createTrackedEffect` (deprecated in 2.0) <!-- upstream:tracked-effect-deprecated --> | `createEffect(compute, apply)`; deps belong in `compute`; one-time DOM work is `onSettled` |
|
|
451
483
|
| `onMount` | `onSettled(() => { ...; return cleanup })` — return the cleanup; `onCleanup`, memo/effect creation, and `flush()` throw inside the callback |
|
|
452
484
|
| `batch(...)` | delete it — writes auto-batch; `flush()` only to observe synchronously |
|
|
453
485
|
| `<Suspense>`, `<ErrorBoundary>`, `<SuspenseList>` | `<Loading>`, `<Errored>` (fallback gets an error *accessor*), `<Reveal>` |
|
|
454
486
|
| `<Index>` | `<For keyed={false}>` |
|
|
455
487
|
| `<Ctx.Provider value={...}>` | `<Ctx value={...}>` |
|
|
456
|
-
| `setState("a", "b", value)` path setters, `produce(...)` | draft setter: `setState(draft => { ... })` |
|
|
488
|
+
| `setState("a", "b", value)` path setters, `setState({ a })` merges, `produce(...)` | draft setter: `setState(draft => { ... })` (the others throw `fn is not a function`) |
|
|
457
489
|
| `mergeProps` / `splitProps` / `unwrap` | `merge` / `omit` / `snapshot` |
|
|
458
490
|
| `classList={...}` | `class` object/array form |
|
|
459
491
|
| `createMutable`, `modifyMutable` | `createStore` + draft setters (direct writes to the store are silently ignored) |
|
|
@@ -461,24 +493,24 @@ directory (default `src/`).
|
|
|
461
493
|
| `startTransition`, `useTransition` | automatic held updates + `isPending` |
|
|
462
494
|
| `onError` / `catchError` | `<Errored>` or effect bundle `{ effect, error }` |
|
|
463
495
|
| `from(...)` / `observable(...)` (the solid-js helpers) | inbound: async iterable from a memo; outbound: split effect |
|
|
464
|
-
| `createDynamic(...)`, `<Dynamic component={...}>` (deprecated in 2.0) | `dynamic(source)` from `@solidjs/web` |
|
|
496
|
+
| `createDynamic(...)`, `<Dynamic component={...}>` (deprecated in 2.0) <!-- upstream:dynamic-deprecated --> | `dynamic(source)` from `@solidjs/web` |
|
|
465
497
|
| `renderToStringAsync(...)` | `await renderToStream(() => <App />)` (one consumer: `pipe` / `pipeTo` / `readable`) |
|
|
466
498
|
| `clearDelegatedEvents` | delete — delegated listeners are scoped to each render root |
|
|
467
499
|
| `vite-plugin-solid` / `jsxImportSource: "solid-js"` | `@solidjs/vite-plugin` / `"jsxImportSource": "@solidjs/web"` |
|
|
468
500
|
| JSX `<Route>` / `<A>` / `<HashRouter>` / `<Navigate>` / `<FileRoutes>` | `createRouter({ routes })`, `fileRoutes(pageRoutes)` (`fileRoutes` from `@solidjs/router/fs`, `pageRoutes` from `virtual:file-routes`), plain `<a href={Router.paths...}>`, `hashHistory()`; link state via automatic `aria-current` / `data-active` / `data-pending` + CSS (`useLinkState` in JSX) |
|
|
501
|
+
| `useCurrentMatches`, `MemoryRouter` / `createMemoryHistory`, `redirect` / `reload` from `@solidjs/router`, `indexArray` | `useRouteMatches`, `createRouter({ routes, history: memoryHistory("/x") })`, `redirect` / `reload` from `@solidjs/web`, `mapArray(list, fn, { keyed: false })` |
|
|
469
502
|
| `createAsync` / `createAsyncStore` / `useSubmission` / router `json()` / `cache()` | `createMemo(() => getUser(id()))`, `useSubmissions`, `respond()` from `@solidjs/web`, `query` |
|
|
470
503
|
| `import ... from "@solidjs/start"` / `vinxi` / `h3` / `"use client"` | `GET` from `@solidjs/web/server-functions`; Solid has no `"use client"` |
|
|
471
504
|
| `render(<App />, root)` | `render(() => <App />, root)` (same for `hydrate` / `renderToString` / `renderToStream`) |
|
|
472
505
|
| `<MetaProvider>` | no provider — render `<Title>` / `<Meta>` / `<Link>` from `@solidjs/meta` anywhere |
|
|
473
|
-
| `use:directive`, `on:`/`oncapture:`, `attr:`/`bool:`, `/*@once*/` | `ref` callbacks, camelCase event props, standard attributes, keep values reactive |
|
|
506
|
+
| `use:directive`, `on:`/`oncapture:`, `attr:`/`bool:`, `/*@once*/` | `ref` callbacks, camelCase event props, standard attributes, keep values reactive; `prop:` remains for DOM properties (`prop:indeterminate`, objects/arrays on custom elements — plain props are attributes and stringify) |
|
|
474
507
|
| `resource.loading` / `resource.error` | `<Loading>` boundary / `<Errored>` boundary |
|
|
475
508
|
|
|
476
509
|
When unsure about any API, verify against the official Solid 2.0 docs — fetchable URLs are
|
|
477
510
|
listed in `references/official-docs.md` next to the `solid-2` skill. Do not guess from
|
|
478
511
|
Solid 1.x or React memory.
|
|
479
512
|
|
|
480
|
-
`
|
|
481
|
-
use two-phase `createEffect` unless a single tracked callback is required. `storePath`
|
|
513
|
+
`createRenderEffect` exists in Solid 2 but is not a default. `storePath`
|
|
482
514
|
(the 1.x path-setter migration helper) is no longer exported from `solid-js` as of
|
|
483
515
|
2.0.0-rc.9 — convert path setters to draft setters instead of importing it from
|
|
484
|
-
`@solidjs/signals`.
|
|
516
|
+
`@solidjs/signals`. <!-- upstream:store-path-removed -->
|
|
@@ -69,13 +69,13 @@ Most React `useEffect` code should NOT become `createEffect`:
|
|
|
69
69
|
| Inspect, count, or iterate children | `children(() => props.children)` then `.toArray()` |
|
|
70
70
|
| Code-split a component | `lazy(() => import("./X"))` read under `<Loading>` |
|
|
71
71
|
| Named export from a lazy module | `lazy(() => import("./pages"), { export: "About" })` |
|
|
72
|
-
| Pick a component/tag from reactive state | `dynamic(() => ...)` from `@solidjs/web` (stable identity; `<Dynamic>` is deprecated) |
|
|
72
|
+
| Pick a component/tag from reactive state | `dynamic(() => ...)` from `@solidjs/web` (stable identity; `<Dynamic>` is deprecated) <!-- upstream:dynamic-deprecated --> |
|
|
73
73
|
| Overlay / modal | `<Portal>` — hoist async reads *above* the portal (reads inside start on the client) |
|
|
74
74
|
| Async value used several layers down | Create the memo high; pass `value={memo()}` through intermediates (they do not wait); put `<Loading>` around the leaf read |
|
|
75
75
|
| Nested child with its own fetch | Leave it nested — it runs in parallel. Sequential only when the second call needs the first response (`fetchAuthor(story().authorId)`) |
|
|
76
|
-
| Browser-only widget (charts, maps, `window`) | `clientOnly(() => import("./Chart"))` from `@solidjs/web` (`{ lazy: true }` defers the import until first render) |
|
|
76
|
+
| Browser-only widget (charts, maps, `window`) | `clientOnly(() => import("./Chart"))` from `@solidjs/web` (`{ lazy: true }` defers the import until first render; `{ export: "Chart" }` for a named export). Placeholder: `<Chart fallback={<Skeleton />} />` — SSR renders the fallback; a `<Loading>` around it leaves the server HTML empty |
|
|
77
77
|
| Browser-only *value* (`localStorage`, viewport size) | `createMemo(..., { ssrSource: "client" })` — per-value policy at the data source, no component split (`"hybrid"` when server data mixes with client signals) |
|
|
78
|
-
| Server vs browser
|
|
78
|
+
| Server vs browser code path | `isServer` / `isDev` from `@solidjs/web` (build-time constants), not `typeof window` — never to choose markup (`{isServer ? <A /> : <B />}` is a hydration mismatch; use `clientOnly` / `ssrSource`) |
|
|
79
79
|
| SSR-stable `id` / `for` / `aria-*` pairing | `createUniqueId()` |
|
|
80
80
|
|
|
81
81
|
## Canonical patterns
|
|
@@ -125,7 +125,7 @@ usual defaults pattern. That is not Solid 1 `mergeProps`, which ignored
|
|
|
125
125
|
(`omit(props, (key) => key.startsWith("data-"))`). `const rest = { ...props }` compiles
|
|
126
126
|
and then never updates. Both helpers return **read-only live views** (since
|
|
127
127
|
2.0.0-rc.9 for every input, plain objects included): assigning onto the result is a
|
|
128
|
-
silent no-op, and `{ ...merged }` is an own object that carries no sources.
|
|
128
|
+
silent no-op, and `{ ...merged }` is an own object that carries no sources. <!-- upstream:merge-omit-readonly -->
|
|
129
129
|
|
|
130
130
|
### Store updates (draft mutation)
|
|
131
131
|
|
|
@@ -142,7 +142,7 @@ setProfile((draft) => {
|
|
|
142
142
|
The setter callback is a **synchronous transaction** — the draft closes when it
|
|
143
143
|
returns. `setProfile(async (draft) => { draft.name = await load(); })` used to commit
|
|
144
144
|
only the writes before the first `await` and silently drop the rest; since
|
|
145
|
-
2.0.0-rc.9 it throws `[ASYNC_STORE_SETTER]` in dev (`solid2-kit check` flags it too).
|
|
145
|
+
2.0.0-rc.9 it throws `[ASYNC_STORE_SETTER]` in dev (`solid2-kit check` flags it too). <!-- upstream:async-store-setter-throws -->
|
|
146
146
|
Await outside, then write synchronously (inside an `action`, `yield` first):
|
|
147
147
|
|
|
148
148
|
```tsx
|
|
@@ -156,7 +156,7 @@ setProfile((draft) => { draft.name = name; });
|
|
|
156
156
|
|
|
157
157
|
A signal may hold a promise, so `setSignal` has no such rule. A store setter called
|
|
158
158
|
at component-body top level is a write in an owned scope — dev throws
|
|
159
|
-
`[REACTIVE_WRITE_IN_OWNED_SCOPE]` (rc.9 closed the exemption store writes had) — so
|
|
159
|
+
`[REACTIVE_WRITE_IN_OWNED_SCOPE]` (rc.9 closed the exemption store writes had) <!-- upstream:store-write-owned-scope --> — so
|
|
160
160
|
seed the shape through the `createStore` argument or a derivation.
|
|
161
161
|
|
|
162
162
|
**The store itself is read-only; only the draft writes.** An assignment or mutating
|
|
@@ -195,7 +195,10 @@ subscribers under the path re-run and row identity is lost. In order of preferen
|
|
|
195
195
|
`setState((draft) => { reconcile(fresh, 'id')(draft.todos); })`. Positional data
|
|
196
196
|
(fixed-shape dashboards): `reconcile(next, null)`.
|
|
197
197
|
|
|
198
|
-
Plain non-reactive
|
|
198
|
+
Plain non-reactive view for logging/serialization: `snapshot(store)`. It is not a copy —
|
|
199
|
+
subtrees with no pending changes are the store's own objects, so mutating the snapshot
|
|
200
|
+
(or letting a library `sort()` it) changes the store with no notification. Edit
|
|
201
|
+
`structuredClone(snapshot(store))` instead.
|
|
199
202
|
Subscribe an effect compute to every nested change: `deep(store)`.
|
|
200
203
|
|
|
201
204
|
### Children: pass through, or resolve with `children()`
|
|
@@ -258,7 +261,7 @@ variable in JSX (`const View = tab() ? A : B` freezes the choice at setup).
|
|
|
258
261
|
`<Dynamic component={...}>` is the same primitive with a worse shape (the tag travels
|
|
259
262
|
in the props bag, so every instance merges and omits it and rebuilds a factory) and
|
|
260
263
|
is **deprecated** since 2.0.0-rc.9 — `@deprecated` in the types, no runtime warning,
|
|
261
|
-
still shipped in 2.0. Hoist a `dynamic()` per component instance (or per module for a
|
|
264
|
+
still shipped in 2.0. <!-- upstream:dynamic-deprecated --> Hoist a `dynamic()` per component instance (or per module for a
|
|
262
265
|
constant tag) instead.
|
|
263
266
|
|
|
264
267
|
### Two-phase effect (imperative boundary only)
|
|
@@ -432,7 +435,7 @@ return <StoryDetail story={story} />;
|
|
|
432
435
|
`isPending(() => results())`). Freshest in-flight value for a preview:
|
|
433
436
|
`latest(results)`. `latest` is not a null-safe probe: before the source's first value
|
|
434
437
|
it throws `NotReadyError` in every scope (since 2.0.0-rc.9 also in event handlers and
|
|
435
|
-
imperative code, which used to receive `undefined`)
|
|
438
|
+
imperative code, which used to receive `undefined`) <!-- upstream:latest-throws-unsettled -->. A write is visible to `latest()` /
|
|
436
439
|
`isPending()` only from the flush that carries it — `flush()` first when a test reads
|
|
437
440
|
its own write through them. Do not start `fetch` at component-body top level.
|
|
438
441
|
- Coordinate sibling `<Loading>` reveal with `<Reveal>` (`order="sequential"` default,
|
|
@@ -500,12 +503,15 @@ registers this hook must load before the server graph — put it in the plugin's
|
|
|
500
503
|
`start: { instrument: "./src/instrument.ts" }`, not at the top of an entry (see
|
|
501
504
|
[Production observability](#production-observability-the-observe-build)).
|
|
502
505
|
|
|
503
|
-
`render` / `hydrate` accept a per-root `onError` that wins over the ambient hook
|
|
506
|
+
`render` / `hydrate` accept a per-root `onError` that wins over the ambient hook —
|
|
507
|
+
`render(() => <App />, root, undefined, { onError })` (options are the 4th argument; a
|
|
508
|
+
3rd-argument object is taken as `init` and the hook is silently not installed) and
|
|
509
|
+
`hydrate(() => <App />, root, { onError })`.
|
|
504
510
|
Uncaught errors (`REACTIVITY_HALTED`) go to the platform's `reportError`, which every
|
|
505
511
|
monitor already listens on — do not report them twice. `renderToStream` /
|
|
506
512
|
`renderToString`'s `onError` option *is* the per-render server hook: it now hears every
|
|
507
513
|
handled failure, so filter on `context.handling === "failed"` when only request-failing
|
|
508
|
-
errors matter (the rc.8 one-argument form keeps working).
|
|
514
|
+
errors matter (the rc.8 one-argument form keeps working). <!-- upstream:render-onerror-handled -->
|
|
509
515
|
|
|
510
516
|
### Streams and subscriptions (websockets, reactive clients)
|
|
511
517
|
|
|
@@ -644,7 +650,8 @@ file of server functions. Do not rewrite `App.tsx` with loading/error branches,
|
|
|
644
650
|
and do not snapshot-the-cache / write-a-prediction / restore-on-error. A cache
|
|
645
651
|
library is not the productionizing step — the sync mutation already was the
|
|
646
652
|
prediction. (Router `query` or `@tanstack/solid-query` remain valid when the
|
|
647
|
-
project already uses them; they are not required to "make it real".
|
|
653
|
+
project already uses them; they are not required to "make it real". Solid 2's
|
|
654
|
+
TanStack Query is `@tanstack/solid-query@rc` (6.x) — npm `latest` is the Solid 1 5.x line.)
|
|
648
655
|
Optimistic rows stay interactive — a not-yet-acked todo can still
|
|
649
656
|
be toggled. Do not disable the control until confirmation, and do not mutex
|
|
650
657
|
rapid clicks; `action` is the transaction (no half-mutation, no interleaved
|
|
@@ -660,7 +667,7 @@ handler or the `action`.
|
|
|
660
667
|
`createOptimistic(value)`) is a pure overlay: writes inside an action vanish when it
|
|
661
668
|
settles, and writes outside an action do not stick. Copying the reference example
|
|
662
669
|
`createOptimisticStore<Todo[]>([])` and writing the saved row after the `yield`
|
|
663
|
-
leaves an empty list once the action settles, with no warning. Durable data comes
|
|
670
|
+
leaves an empty list once the action settles, with no warning. <!-- upstream:optimistic-value-overlay --> Durable data comes
|
|
664
671
|
from the function form — `createOptimisticStore(() => api.list(), [])` refreshed
|
|
665
672
|
after the `yield`, or `createOptimisticStore(() => base.todos, [])` over a separate
|
|
666
673
|
`createStore` that the action writes the confirmed value into. Keep the value form for
|
|
@@ -1010,7 +1017,8 @@ writes from owned scopes, and async reads outside `<Loading>`. Fix them; don't s
|
|
|
1010
1017
|
A read at component-body top level reports `[STRICT_READ_UNTRACKED]` naming the
|
|
1011
1018
|
component — move the read into JSX, a memo, or an effect compute.
|
|
1012
1019
|
|
|
1013
|
-
Component tests use `@solidjs/testing-library
|
|
1020
|
+
Component tests use `@solidjs/testing-library@next` (1.x; npm `latest` is the Solid 1
|
|
1021
|
+
0.8 line). Pass a **function** to `render` so
|
|
1014
1022
|
the tree has an owner, and `flush()` after interactions that stage writes:
|
|
1015
1023
|
|
|
1016
1024
|
```tsx
|
|
@@ -1029,6 +1037,14 @@ test('increments on click', () => {
|
|
|
1029
1037
|
});
|
|
1030
1038
|
```
|
|
1031
1039
|
|
|
1040
|
+
Primitive tests write **outside** the root callback — a write inside `createRoot((dispose)
|
|
1041
|
+
=> { …; setA(2); })` is `[REACTIVE_WRITE_IN_OWNED_SCOPE]` in dev. Return the handles, then
|
|
1042
|
+
write and `flush()`: `const { setA, b, dispose } = createRoot((dispose) => { …; return
|
|
1043
|
+
{ setA, b, dispose }; }); setA(2); flush();`. A hook that reads context runs under
|
|
1044
|
+
`renderHook(useCart, { wrapper: CartProvider })`. A routed component gets a router built in
|
|
1045
|
+
the test — `createRouter({ routes, history: memoryHistory("/orders/42") })` — because
|
|
1046
|
+
Testing Library's `render(…, { location })` is typed but not implemented (1.0.0-beta.3). <!-- upstream:testing-library-location -->
|
|
1047
|
+
|
|
1032
1048
|
`jsxImportSource` for tests and app code is `"@solidjs/web"`, not `"solid-js"`.
|
|
1033
1049
|
Vite plugin is `@solidjs/vite-plugin`, not `vite-plugin-solid`.
|
|
1034
1050
|
|
|
@@ -1054,14 +1070,14 @@ in observe and dev) and **checks** (dev-only guidance printed by the console rep
|
|
|
1054
1070
|
Always-on dev checks include misplaced reads
|
|
1055
1071
|
(`[STRICT_READ_UNTRACKED]`, `[PENDING_ASYNC_UNTRACKED_READ]`), misplaced writes
|
|
1056
1072
|
(`[REACTIVE_WRITE_IN_OWNED_SCOPE]` — since rc.9 also for store setters called in a
|
|
1057
|
-
component or root body
|
|
1058
|
-
a promise
|
|
1073
|
+
component or root body <!-- upstream:store-write-owned-scope -->, `[ASYNC_STORE_SETTER]` for a store setter callback that returns
|
|
1074
|
+
a promise <!-- upstream:async-store-setter-throws -->, `[FLUSH_IN_ACTION]`, `[SERVER_WRITE]`), and
|
|
1059
1075
|
`[ASYNC_OUTSIDE_LOADING_BOUNDARY]` when a tracked read has no boundary above it.
|
|
1060
1076
|
Each entry carries a stable `[CODE]`, what the runtime observed, and an owner chain
|
|
1061
1077
|
(`in <App> › <Cart> › <LineItem>`); never silence a code before understanding it.
|
|
1062
1078
|
Since rc.9 the server render reports on the same channel with the same owner chains:
|
|
1063
1079
|
`[SSR_RENDER_ERROR_CONTAINED]` (a boundary routed a render error; `ownerPath` is where
|
|
1064
|
-
it was thrown, `boundaryPath` where it was met)
|
|
1080
|
+
it was thrown, `boundaryPath` where it was met) <!-- upstream:ssr-render-error-contained -->, `[SSR_ERROR_SANITIZED]` (the original
|
|
1065
1081
|
behind a production-sanitized error), `[SSR_CLIENT_CONTENT_MASKED]` (client-only content
|
|
1066
1082
|
that surfaced only after a real server wait — the wait was wasted), server-side
|
|
1067
1083
|
`[ASYNC_WATERFALL]`, and `[REVEAL_IN_RENDER_TO_STRING]`.
|
|
@@ -1072,7 +1088,7 @@ code maps to what the runtime observed and what to change.
|
|
|
1072
1088
|
Cost and responsiveness findings need the opt-in attribution engine, which records
|
|
1073
1089
|
every scope that re-ran, what changed to cause it, and how long it took. Since
|
|
1074
1090
|
`solid-js` 2.0.0-rc.8 the engine lives behind its own subpath — `DEV.attribution`
|
|
1075
|
-
no longer exists (it is a type error; `DEV` holds only devtools hooks, graph
|
|
1091
|
+
no longer exists <!-- upstream:dev-attribution-removed --> (it is a type error; `DEV` holds only devtools hooks, graph
|
|
1076
1092
|
traversal, and console reporting). `OBSERVE` is the public observability surface on
|
|
1077
1093
|
the observe and dev builds — `OBSERVE.records`, `OBSERVE.diagnostics`,
|
|
1078
1094
|
`OBSERVE.exclude`, `OBSERVE.server.trace` are for app and tool code; only the
|
|
@@ -1080,7 +1096,7 @@ the observe and dev builds — `OBSERVE.records`, `OBSERVE.diagnostics`,
|
|
|
1080
1096
|
to engines, routers, and devtools. Since 2.0.0-rc.9 the folds, queries, and
|
|
1081
1097
|
formatters are **named exports** of that subpath, not methods of `attribution`
|
|
1082
1098
|
(`attribution.why` / `.costs` / `.feedback` / `.subscriptions` / `.format` are type
|
|
1083
|
-
errors)
|
|
1099
|
+
errors) <!-- upstream:attribution-named-exports -->; `attribution` keeps `enable`, `disable`, `subscribe`, `markFlight`, and the
|
|
1084
1100
|
record ring buffers (`history`, `holds`, `waterfalls`, `navigations`, `interactions`):
|
|
1085
1101
|
|
|
1086
1102
|
```ts
|
|
@@ -1287,22 +1303,28 @@ Solid Router 0.x/1.x stand-ins.** Patterns below; fetch the matching pages from
|
|
|
1287
1303
|
```
|
|
1288
1304
|
|
|
1289
1305
|
`solid-js` does **not** export a `JSX` namespace — `import type { JSX } from "solid-js"`
|
|
1290
|
-
is a type error (TS2305)
|
|
1291
|
-
|
|
1306
|
+
is a type error (TS2305) <!-- upstream:jsx-namespace-absent -->. Markup types are `JSX.Element` from `@solidjs/web` (the
|
|
1307
|
+
docs' choice; it also admits DOM nodes) or the renderer-neutral `Element` from `solid-js`
|
|
1292
1308
|
(`children?: Element`), alongside `Component` / `ParentProps` / `Accessor` / `Setter`.
|
|
1293
|
-
|
|
1294
|
-
|
|
1309
|
+
Import the latter as `import type { Element as SolidElement } from "solid-js"` in any file
|
|
1310
|
+
that also uses DOM elements — unaliased, it shadows the DOM `Element`
|
|
1311
|
+
(`el.getBoundingClientRect()` becomes TS2339). DOM-specific `JSX` types
|
|
1312
|
+
(`JSX.IntrinsicElements`, `JSX.CSSProperties`) and `ComponentProps` come from
|
|
1313
|
+
`@solidjs/web`. Vite: `import solid from "@solidjs/vite-plugin"`
|
|
1295
1314
|
with `start: true` / `ssr: true` / `serverFunctions: true` only when the
|
|
1296
1315
|
project already uses start mode. Tiers are `bare` → `basic` (router) →
|
|
1297
1316
|
`fullstack` (SSR + server functions); do not jump a tier without cause.
|
|
1298
|
-
Under `start: true` the plugin owns entries
|
|
1299
|
-
|
|
1300
|
-
or a hand-written `index.html`.
|
|
1317
|
+
Under `start: true` the plugin owns entries and the build — write `src/App.tsx`, not
|
|
1318
|
+
SolidStart-style `entry-client.tsx` / `entry-server.tsx` (authored ones replace the
|
|
1319
|
+
generated entries) or a hand-written `index.html`. The document shell (`<html lang>`,
|
|
1320
|
+
favicon, fallback `<title>`) is an optional `src/Document.tsx` default export that renders
|
|
1321
|
+
`<HydrationScript />` in `<head>` and `<body>{props.children}</body>`; `App` never renders
|
|
1322
|
+
`<html>`.
|
|
1301
1323
|
|
|
1302
1324
|
### Solid Router 2 — `createRouter`, not JSX `<Route>`
|
|
1303
1325
|
|
|
1304
1326
|
```tsx
|
|
1305
|
-
import { lazy } from 'solid-js';
|
|
1327
|
+
import { Loading, lazy } from 'solid-js';
|
|
1306
1328
|
import { createRouter } from '@solidjs/router';
|
|
1307
1329
|
|
|
1308
1330
|
export const Router = createRouter({
|
|
@@ -1320,7 +1342,9 @@ export default function App() {
|
|
|
1320
1342
|
{(props) => (
|
|
1321
1343
|
<>
|
|
1322
1344
|
<a href={Router.paths}>Home</a>
|
|
1323
|
-
<
|
|
1345
|
+
<Loading fallback={<p>Loading…</p>}>
|
|
1346
|
+
<main>{props.children}</main>
|
|
1347
|
+
</Loading>
|
|
1324
1348
|
</>
|
|
1325
1349
|
)}
|
|
1326
1350
|
</Router>
|
|
@@ -1328,7 +1352,9 @@ export default function App() {
|
|
|
1328
1352
|
}
|
|
1329
1353
|
```
|
|
1330
1354
|
|
|
1331
|
-
|
|
1355
|
+
The `<Loading>` covers the first load of the lazy pages (and lets SSR stream the shell);
|
|
1356
|
+
later navigations hold the current page while the next one loads (links get
|
|
1357
|
+
`data-pending`). Create the instance at **module scope**. Nested layouts are `children` arrays
|
|
1332
1358
|
on the route objects, not nested `<Route>` / nested routers. Solid Router does
|
|
1333
1359
|
**not** support nested `<Router>` instances — compose one route tree (or a lazy
|
|
1334
1360
|
`children` thunk). Navigate with
|
|
@@ -1410,7 +1436,9 @@ setSearch] = useSearchParams()` — the setter merges and navigates without scro
|
|
|
1410
1436
|
`""` / `undefined` / `null` removes a key. For typed values, put a synchronous
|
|
1411
1437
|
Standard Schema on the route's `search` field and pass the path node:
|
|
1412
1438
|
`useSearchParams(Router.paths.search)` (async schemas throw; a failing schema leaves
|
|
1413
|
-
the raw strings rather than throwing).
|
|
1439
|
+
the raw strings rather than throwing). Query values are strings, so coerce in the schema —
|
|
1440
|
+
`page: v.optional(v.pipe(v.unknown(), v.transform(Number)), 1)`; a plain `v.number()`
|
|
1441
|
+
rejects every value and hands back the string, still typed `number`.
|
|
1414
1442
|
|
|
1415
1443
|
**Two different `action`s.** Core `action` from `solid-js` is a generator transaction
|
|
1416
1444
|
(optimistic writes that span an async gap). Router `action` from `@solidjs/router` is
|
|
@@ -1435,6 +1463,14 @@ const save = action(async (form: FormData) => {
|
|
|
1435
1463
|
Only POST forms are accepted. Bind extra args with `.with(id)` (they go in the
|
|
1436
1464
|
action URL). `onSubmit={(e) => { e.preventDefault(); fetch(...) }}` *runs* and
|
|
1437
1465
|
drops the no-JS form fallback. `useAction` is JS-only for the same reason.
|
|
1466
|
+
Optimistic rows for a router form keep the `<form>` and add no core `action`: read the
|
|
1467
|
+
list `query` with `createOptimisticStore(() => getList(), [])` and register
|
|
1468
|
+
`save.onSubmit((form) => setList((d) => { d.push({ …, pending: true }); }))` in the
|
|
1469
|
+
component — the hook runs inside the action's transaction, so the row is an overlay,
|
|
1470
|
+
replaced on settle by the revalidated `query` (a plain server-function read is not
|
|
1471
|
+
revalidated, so the row just disappears).
|
|
1472
|
+
`useSubmissions` keeps only completions with a result or error; observe void and
|
|
1473
|
+
redirect completions with `save.onSettled(hook)`.
|
|
1438
1474
|
|
|
1439
1475
|
Validated forms come in three passes sharing one HTML shape. Pass 1 (above) posts
|
|
1440
1476
|
uncontrolled inputs with browser `required` / `pattern` — it works with no JavaScript.
|
|
@@ -1534,7 +1570,15 @@ A function-level server function cannot close over component locals — pass
|
|
|
1534
1570
|
values as arguments. Treat every argument as untrusted — TypeScript does not
|
|
1535
1571
|
cross the HTTP boundary, so do not invent tRPC / RPC type-gen;
|
|
1536
1572
|
validate inside the function (a parse in the function body is that
|
|
1537
|
-
validation, not an RPC schema across the wire).
|
|
1573
|
+
validation, not an RPC schema across the wire). Checks belong in the body: HTTP dispatch
|
|
1574
|
+
calls a function-level `"use server"` body directly, so a HOF around it (`withAuth(fn)`,
|
|
1575
|
+
`validated(schema, fn)`) runs for in-process calls and is skipped for `curl` — an auth
|
|
1576
|
+
bypass. A wrapper inside a file-level `"use server"` module (`export const x =
|
|
1577
|
+
withAccount(async …)`) is what gets registered and does run. Arguments are capped at 1 MiB
|
|
1578
|
+
by default (413 beyond; the host's `bodySizeLimit`) — upload large files to a signed URL.
|
|
1579
|
+
Never hand a server function point-free to a library callback (`mutationFn: renameUser`,
|
|
1580
|
+
`onClick={deleteTodo}`): every argument the callback receives (a context object, the
|
|
1581
|
+
event) is sent over the wire — wrap it in an arrow that passes only the data. These functions *are* the RPC — do not add an
|
|
1538
1582
|
API-route file (or a Next `route.ts`) just to wrap `db.todos.insert`. API
|
|
1539
1583
|
routes are for real HTTP endpoints (webhooks, third-party POST), not for the
|
|
1540
1584
|
app's own RPC. Read trusted identity
|
|
@@ -1588,12 +1632,19 @@ response) close that loop. For 404 during SSR: `httpStatus(404)` from `@solidjs/
|
|
|
1588
1632
|
on the server-function wire **and**, since 2.0.0-rc.9, on every SSR road a failure
|
|
1589
1633
|
takes (an `<Errored>` fallback rendered on the server, a rejected async source
|
|
1590
1634
|
serialized into the stream, a `<Loading>` fragment's rejection), so `err().message`
|
|
1591
|
-
in a fallback reads the generic message in a production server render. Use
|
|
1592
|
-
`markSafeError` or `respond(..., { status })` for intentional
|
|
1593
|
-
failures
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1635
|
+
in a fallback reads the generic message in a production server render. <!-- upstream:ssr-error-sanitized --> Use
|
|
1636
|
+
`throw markSafeError(...)` or `throw respond(..., { status })` for intentional
|
|
1637
|
+
client-facing failures — a *returned* `respond()` resolves the call whatever its
|
|
1638
|
+
status, so `return respond({ error }, { status: 400 })` reads as success to the UI,
|
|
1639
|
+
`useSubmissions`, and the optimistic overlay. Do not `return Response.json(...)` from
|
|
1640
|
+
`"use server"` — that is HTTP-handler control flow; `return respond(value, { status: 201 })`
|
|
1641
|
+
is success metadata a scripted caller unwraps. Arguments are JSON: send `Date` / `Map` /
|
|
1642
|
+
`Set` as ISO strings / arrays (they throw otherwise; one `File` / `Blob` / `FormData`
|
|
1643
|
+
argument travels natively). `enableRichArguments()` from
|
|
1644
|
+
`@solidjs/web/server-functions/rich-args` (at `src/App.tsx` module scope) lifts that, but
|
|
1645
|
+
in rc.9 importing it fails `vite build` (`"./client" is not exported` — the dev server
|
|
1646
|
+
works, so the break shows only at build); `resolve: { dedupe: ["@solidjs/web"] }` in the
|
|
1647
|
+
Vite config works around it. <!-- upstream:rich-args-vite-build https://github.com/solidjs/solid/issues/3627 -->
|
|
1597
1648
|
`GET()` is only for idempotent reads (URLs leak into logs/history);
|
|
1598
1649
|
import it from `@solidjs/web/server-functions`, never `@solidjs/start`.
|
|
1599
1650
|
Reads are function calls too: `GET(async (id) => { "use server"; ... })` is invoked
|
|
@@ -1686,9 +1737,11 @@ When the app has a server bundle (`ssr` or server functions), production is
|
|
|
1686
1737
|
`handleRequest(request)` from the built `dist/server/server.js` (or that
|
|
1687
1738
|
module's Fetchable default `fetch`) — not an import from `solid-js`. Cloudflare /
|
|
1688
1739
|
Netlify / Nitro Vite plugins adopt that handler — do not write a Solid adapter,
|
|
1689
|
-
a custom Worker, or a Netlify Function. For Node,
|
|
1690
|
-
`server.js` (
|
|
1691
|
-
Express/
|
|
1740
|
+
a custom Worker, or a Netlify Function. For Node, set `start: { node: true }` and run
|
|
1741
|
+
`node dist/server/node.js` (serves `dist/client`, reads `PORT` / `HOST`); inside an
|
|
1742
|
+
existing Express / Fastify app, mount the `listener` that file exports — do not
|
|
1743
|
+
hand-write a Node↔web bridge (`node dist/server/server.js` starts nothing: it is the
|
|
1744
|
+
handler module). Client-only `start: true` is still static `dist/client`
|
|
1692
1745
|
— there is no handler to wrap. Request middleware is the Vite option
|
|
1693
1746
|
`start: { middleware: "./src/middleware.ts" }` (web `Request` + `next`), not
|
|
1694
1747
|
Express `app.use`. Server functions that return components
|
|
@@ -1807,7 +1860,7 @@ how to reuse. Prefer the form on the right.
|
|
|
1807
1860
|
| `createOptimistic` for a local edit draft | writable derivation / plain store. Optimistic is for an in-flight mutation |
|
|
1808
1861
|
| `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`) |
|
|
1809
1862
|
| `setSubmitted(true)` + an effect that watches it | do the work in the handler or an `action` |
|
|
1810
|
-
| `findUser(userId)` with the id from the client as identity | `getRequestEvent()
|
|
1863
|
+
| `findUser(userId)` with the id from the client as identity | `const userId = getRequestEvent()?.locals.userId; if (!userId) throw respond(null, { status: 401 });` (`throw redirect("/sign-in")` under a router `action` / `query`) |
|
|
1811
1864
|
| `action={"/_server/" + id}` / `action={createTodo.url}` on a POST form | a router action: `const save = action(createTodo, "create-todo")` + `<form action={save} method="post">` over a `(form: FormData)` server function (other signatures are TS2322) — `.url` on a bare `"use server"` function is TS2339; it is typed only on `GET()` / `live()` references (GET search forms) |
|
|
1812
1865
|
| `<form method="get" action={update.url}>` for a mutation | GET forms only for idempotent search; mutations are POST |
|
|
1813
1866
|
| `window.location.href = ...` / `history.pushState` | `useNavigate()` or `<a href={Router.paths...}>` |
|
|
@@ -1839,20 +1892,20 @@ how to reuse. Prefer the form on the right.
|
|
|
1839
1892
|
| `submission.clear()` from an effect | on dismiss/resubmit only — from an effect, errors flash and vanish |
|
|
1840
1893
|
| `revalidate(liveSource)` | `live()` updates through the open stream; do not revalidate it |
|
|
1841
1894
|
| `refresh(user)` without `affects` when the reload should look pending | pair `affects(user)` with `refresh(user)` — a bare `refresh` re-asks quietly |
|
|
1842
|
-
| `<Dynamic component={tab() ? A : B} />` | `const View = dynamic(() => (tab() ? A : B))` — `<Dynamic>` is deprecated (rc.9); `dynamic()` hoists the factory and keeps identity stable |
|
|
1843
|
-
| `setStore(async (draft) => { draft.x = await load(); })` | `const x = await load(); setStore((draft) => { draft.x = x; })` — the setter is a synchronous transaction; dev throws `[ASYNC_STORE_SETTER]` |
|
|
1844
|
-
| `setStore((d) => { d.items = seed; })` in a component body | seed through `createStore(seed)` / `createStore(() => props.items, fallback)` — a body-level store write is `[REACTIVE_WRITE_IN_OWNED_SCOPE]` |
|
|
1895
|
+
| `<Dynamic component={tab() ? A : B} />` | `const View = dynamic(() => (tab() ? A : B))` — `<Dynamic>` is deprecated (rc.9); `dynamic()` hoists the factory and keeps identity stable <!-- upstream:dynamic-deprecated --> |
|
|
1896
|
+
| `setStore(async (draft) => { draft.x = await load(); })` | `const x = await load(); setStore((draft) => { draft.x = x; })` — the setter is a synchronous transaction; dev throws `[ASYNC_STORE_SETTER]` <!-- upstream:async-store-setter-throws --> |
|
|
1897
|
+
| `setStore((d) => { d.items = seed; })` in a component body | seed through `createStore(seed)` / `createStore(() => props.items, fallback)` — a body-level store write is `[REACTIVE_WRITE_IN_OWNED_SCOPE]` <!-- upstream:store-write-owned-scope --> |
|
|
1845
1898
|
| `const saved = await api.save(); yield until(() => ...)` | `await api.save(); yield; yield until(() => ...)` — a bare `yield` re-enters the transaction before any reader is created, not only before writes |
|
|
1846
|
-
| `fallback={(err) => { captureException(err()); return <Fallback />; }}` | `configureClientErrors({ onError })` from `solid-js` (or `render(
|
|
1847
|
-
| `latest(user) ?? placeholder` in a handler as a null-safe read | `latest()` throws `NotReadyError` before the first value in every scope; read settled values under `<Loading>`, or `await until(() => user())` |
|
|
1848
|
-
| `attribution.why(total)` / `attribution.format(e)` / `attribution.costs()` | named imports from `solid-js/attribution`: `why(total)`, `formatRerun(e)`, `costs()`, `feedback()`, `subscriptions(scope)` (rc.9) |
|
|
1899
|
+
| `fallback={(err) => { captureException(err()); return <Fallback />; }}` | `configureClientErrors({ onError })` from `solid-js` (or per root `render(fn, root, undefined, { onError })`), `configureServerErrors({ onError })` from `@solidjs/web` — once per error, with `ownerPath` / `boundaryPath` |
|
|
1900
|
+
| `latest(user) ?? placeholder` in a handler as a null-safe read | `latest()` throws `NotReadyError` before the first value in every scope; read settled values under `<Loading>`, or `await until(() => user())` <!-- upstream:latest-throws-unsettled --> |
|
|
1901
|
+
| `attribution.why(total)` / `attribution.format(e)` / `attribution.costs()` | named imports from `solid-js/attribution`: `why(total)`, `formatRerun(e)`, `costs()`, `feedback()`, `subscriptions(scope)` (rc.9) <!-- upstream:attribution-named-exports --> |
|
|
1849
1902
|
| `attribution.enable()` unguarded in app code, or `enable()` with the default `log: true` left on | `if (isDev) attribution.enable({ log: false })` — on the observe build an unguarded `enable()` records in production; prefer the `/__solid/diagnostics` session (`begin` enables, `end` disables) so app code carries nothing |
|
|
1850
1903
|
| `solid({ observe: true })` added "to see diagnostics in dev" | `vite dev` is always the dev build; `observe: true` is the production-observability opt-in (observe condition + `componentNames` for `vite build`/preview) |
|
|
1851
1904
|
| `configureServerErrors({ onError: (e) => { report(e); return e; } })` | return nothing or a reference (`new Error(\`ref ${id}\`)`) — the return value replaces the sanitized wire value; the error itself leaks message/stack/secrets |
|
|
1852
1905
|
| `import "./instrument"` at the top of the server entry for a monitoring SDK | `start: { instrument: "./src/instrument.ts" }` — ESM hoists static imports in dependency order, so the entry's own deps load first |
|
|
1853
1906
|
| Express/middleware injecting `traceparent` / trace headers into responses | the runtime carries a sampled or provided trace on `Server-Timing` + `<meta>`; forward downstream with `getTraceContext()?.entries`; SDKs use `OBSERVE?.server.trace.provide` |
|
|
1854
1907
|
| `OBSERVE.records.subscribe("call", (e) => setStats(...))` | listeners run synchronously inside the runtime and must not write signals; collect into a plain buffer and drain from outside |
|
|
1855
|
-
| `import { storePath } from "solid-js"` | gone from `solid-js` (rc.9); write draft setters `setState((d) => { d.user.name = "Grace"; })` |
|
|
1908
|
+
| `import { storePath } from "solid-js"` | gone from `solid-js` (rc.9); write draft setters `setState((d) => { d.user.name = "Grace"; })` <!-- upstream:store-path-removed --> |
|
|
1856
1909
|
| `<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 |
|
|
1857
1910
|
| `useHead({ tag: "meta", ... })` for ordinary page tags | `<Title>` / `<Meta>` / `<Script>` (JSON-LD). `useHead` is the lower-level registry |
|
|
1858
1911
|
| two `<Meta property="og:image">` meant as one replacement set | wrap them in `<Head>` — a later group replaces the earlier set as one unit |
|
|
@@ -1866,19 +1919,19 @@ how to reuse. Prefer the form on the right.
|
|
|
1866
1919
|
| `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 |
|
|
1867
1920
|
| `<input type="checkbox" value={on()} />` | `checked={on()}` for the toggle; `value="string"` only when grouping radios or listing submitted values |
|
|
1868
1921
|
| `createMemo(() => { void save(); return x(); })` / an effect that calls an action | invoke actions from handlers, not from memos or effects |
|
|
1869
|
-
| `createSignal(props.value)` / `createStore(props.items)` | `createSignal(() => props.value)` / `createStore(() => props.items, fallback)` — a bare `props.x` at setup is a snapshot |
|
|
1870
|
-
| `createRenderEffect` / `createTrackedEffect` as the default effect | two-phase `createEffect(compute, apply)` |
|
|
1922
|
+
| `createSignal(props.value)` / `createStore(props.items)` | `createSignal(() => props.value)` / `createStore(() => props.items, fallback)` — a bare `props.x` at setup is a snapshot. That form resets on every prop change; an intentional one-time read (an `initialCount` prop) is `createSignal(untrack(() => props.initialCount))` |
|
|
1923
|
+
| `createRenderEffect` / `createTrackedEffect` as the default effect | two-phase `createEffect(compute, apply)`; `createTrackedEffect` is `@deprecated` (rc.9) <!-- upstream:tracked-effect-deprecated --> |
|
|
1871
1924
|
| `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 |
|
|
1872
1925
|
| 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 |
|
|
1873
1926
|
| rewrite `App.tsx` with loading/error branches, or snapshot/restore, when the list moves to the server | same `setTodos`; wrap mutations in `action`; swap in function-form `createOptimisticStore(() => api.list(), [])` + a server-functions file. The overlay is discarded; the confirmed store is the truth |
|
|
1874
|
-
| `createOptimisticStore<Todo[]>([])` holding the list, saved rows written after `yield` | `createOptimisticStore(() => api.list(), [])` + `refresh(todos)` (or a function form over a separate `createStore`) — the value form is a pure overlay: action writes vanish on settle |
|
|
1927
|
+
| `createOptimisticStore<Todo[]>([])` holding the list, saved rows written after `yield` | `createOptimisticStore(() => api.list(), [])` + `refresh(todos)` (or a function form over a separate `createStore`) — the value form is a pure overlay: action writes vanish on settle <!-- upstream:optimistic-value-overlay --> |
|
|
1875
1928
|
| disable an optimistic row until ack, or mutex rapid clicks / freeze unrelated writes | keep the control enabled; `action` is the transaction. An in-flight action does not freeze other writes. Failed-action replay is optional |
|
|
1876
1929
|
| tRPC / RPC type-gen around `"use server"` | TypeScript does not cross HTTP — validate inside the function. Locals only the body reads (db, secrets) stay off the client |
|
|
1877
1930
|
| `src/routes/api/todos.ts` (or a Next `route.ts`) wrapping `db.todos.insert` | `"use server"` *is* the RPC. API routes are for real HTTP endpoints (webhooks), not the app's own mutations |
|
|
1878
1931
|
| router/server mutation, then `fetch` / `onSettled` / `hydrate` to refresh the page | `reload` / `revalidate` / single-flight (the mutation response can seed destination preloads). In-flight Promises serialize as Promises; `live()` continues from HTML |
|
|
1879
1932
|
| delay `renderToStream` so HTML arrives in visual order | stream as soon as ready; `<Reveal collapsed>` controls when content *appears* |
|
|
1880
1933
|
| subscribe/unsubscribe in the component around `live()` | `createMemo(() => stockPrice(props.symbol))` — hoist that memo to share one connection. `onstatus` is on the iterable the `live()` call returns, not on the memo. Until the first yield, `<Loading>`; later yields are updates |
|
|
1881
|
-
| a Solid adapter / custom Worker / Express bridge, or `entry-client.tsx` under `start: true` | With a server bundle: `handleRequest` from `dist/server/server.js` / Fetchable `fetch`; platform plugins adopt it. Node:
|
|
1934
|
+
| a Solid adapter / custom Worker / Express bridge, or `entry-client.tsx` under `start: true` | With a server bundle: `handleRequest` from `dist/server/server.js` / Fetchable `fetch`; platform plugins adopt it. Node: `start: { node: true }` → `node dist/server/node.js` (its `listener` for an existing Express / Fastify app). Client-only start: static `dist/client`. Middleware is `start: { middleware: "..." }`, not Express `app.use` |
|
|
1882
1935
|
| `createMemo(async () => (await props.story).author)` | `createMemo(() => props.story.author)` — a derivation over an async value becomes async; no await, no Promise type |
|
|
1883
1936
|
| return a component from `"use server"` / flip `serverFunctions.components` | experimental preview — do not enable unless the project already has that flag |
|
|
1884
1937
|
|
|
@@ -2017,7 +2070,8 @@ always-applied rules installed alongside this skill.
|
|
|
2017
2070
|
bind `latest(query)`; failures throw to `<Errored>`, never `{ success: false }`
|
|
2018
2071
|
result objects checked at every read.
|
|
2019
2072
|
- [ ] Lists via `<For>` (server/refetched rows keyed by stable id), conditionals via
|
|
2020
|
-
ternary/`<Show>`; no `{list().map(...)}` in JSX; no `key` props
|
|
2073
|
+
ternary/`<Show>`; no `{list().map(...)}` in JSX; no `key` props (except
|
|
2074
|
+
`@solidjs/meta` tags); no `value()!`.
|
|
2021
2075
|
- [ ] Effects are two-phase and only at imperative boundaries; apply does not read stores.
|
|
2022
2076
|
- [ ] Async reads sit under `<Loading>` (the data slot, not chrome); errors under `<Errored>`.
|
|
2023
2077
|
Reactive inputs of an async memo are read before the first `await`. No hand-rolled
|
|
@@ -2028,7 +2082,7 @@ always-applied rules installed alongside this skill.
|
|
|
2028
2082
|
failures never reach `<Errored>` (an escaping error reverts the overlay and
|
|
2029
2083
|
rejects the call): catch them in the action/projection, and `.catch` the call
|
|
2030
2084
|
in the handler. Error monitoring goes through `configureClientErrors` /
|
|
2031
|
-
`configureServerErrors` (or `render(
|
|
2085
|
+
`configureServerErrors` (or `render(fn, root, undefined, { onError })`), never a side effect in
|
|
2032
2086
|
the fallback. Core `refresh(source)`, router `revalidate(key)`, and `return reload(...)` are
|
|
2033
2087
|
different APIs — do not mix them. Client → server is additive (same
|
|
2034
2088
|
setters + `action` + server functions): do not rewrite App with
|
|
@@ -2046,8 +2100,8 @@ always-applied rules installed alongside this skill.
|
|
|
2046
2100
|
- [ ] Rest props via `omit`, not `{ ...props }`. Conditional `class` via objects, not string concat.
|
|
2047
2101
|
- [ ] Input filters write back to the DOM on reject (inputs do not rewind).
|
|
2048
2102
|
- [ ] No Solid 1.x imports or APIs (`solid-js/store`, `createResource`, `onMount`, `Suspense`, ...).
|
|
2049
|
-
No `JSX` imported from `solid-js` —
|
|
2050
|
-
`solid-js
|
|
2103
|
+
No `JSX` imported from `solid-js` — markup types are `JSX.Element` from
|
|
2104
|
+
`@solidjs/web` or `Element` from `solid-js` (aliased next to DOM code).
|
|
2051
2105
|
- [ ] Inspect children with `children()`; code-split with `lazy` + `<Loading>`; reactive
|
|
2052
2106
|
component choice with `dynamic()`. No `React.lazy`, no effects inside ref callbacks.
|
|
2053
2107
|
- [ ] Browser-only code uses `isServer` / `clientOnly` (components) / `ssrSource`
|
|
@@ -2058,7 +2112,8 @@ always-applied rules installed alongside this skill.
|
|
|
2058
2112
|
cleanup or nothing.
|
|
2059
2113
|
- [ ] Dev console shows no diagnostics: reads in tracking scopes (no
|
|
2060
2114
|
`[STRICT_READ_UNTRACKED]`), store setters synchronous and never called at
|
|
2061
|
-
body level (no `[ASYNC_STORE_SETTER]`
|
|
2115
|
+
body level (no `[ASYNC_STORE_SETTER]` <!-- upstream:async-store-setter-throws --> /
|
|
2116
|
+
`[REACTIVE_WRITE_IN_OWNED_SCOPE]` <!-- upstream:store-write-owned-scope -->), held
|
|
2062
2117
|
writes paired with `isPending` / `latest` / optimistic values / `affects()`
|
|
2063
2118
|
(no `[SILENT_HOLD]`). Attribution-only costs
|
|
2064
2119
|
(`[IMMUTABLE_UPDATE_IN_STORE]`, `[UNSTABLE_LIST_IDENTITY]`, `[EFFECT_*]`)
|
|
@@ -14,6 +14,32 @@ exist when a project has routing, server functions, or document head tags.
|
|
|
14
14
|
It is not every Advanced reference page. When an API is missing here, look it up
|
|
15
15
|
in `llms.txt` rather than guessing from Solid 1.x or React.
|
|
16
16
|
|
|
17
|
+
## Read before using
|
|
18
|
+
|
|
19
|
+
The skill does not cover these; fetch the pages when the task needs them.
|
|
20
|
+
|
|
21
|
+
- Filesystem routing (`fileRoutes({ types: true })` from `filesystem-routing/vite`, `[id]` / `[[p]]` / `[...rest]` / `(group)/` names, `defineFileRoute`): https://v2-rebuild--solid-docs-v2.netlify.app/reference/filesystem-routing.md, https://v2-rebuild--solid-docs-v2.netlify.app/reference/filesystem-routing/vite.md, https://v2-rebuild--solid-docs-v2.netlify.app/reference/filesystem-routing/conventions.md, https://v2-rebuild--solid-docs-v2.netlify.app/reference/filesystem-routing/manifest.md, https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-router/filesystem.md
|
|
22
|
+
- A route array in its own file (`defineRoutes`, `RouteProps<typeof Router.paths.x>`, `matchFilters`): https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-router/router-factory.md, https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-router/routes-and-paths.md
|
|
23
|
+
- Layout routes, pathless routes, where router hooks may run: https://v2-rebuild--solid-docs-v2.netlify.app/routing/solid-router/nested-routes.md
|
|
24
|
+
- Auth guards and redirect-after-sign-in (`next` validation): https://v2-rebuild--solid-docs-v2.netlify.app/guides/protected-routes.md
|
|
25
|
+
- Unsaved-changes guard (`useBeforeLeave`): https://v2-rebuild--solid-docs-v2.netlify.app/routing/solid-router/navigation.md, https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-router/navigation.md
|
|
26
|
+
- Row selection without `createSelector` (id-keyed projection): https://v2-rebuild--solid-docs-v2.netlify.app/guides/lists.md, https://v2-rebuild--solid-docs-v2.netlify.app/guides/performance.md
|
|
27
|
+
- Infinite scroll, polling, sharing one request: https://v2-rebuild--solid-docs-v2.netlify.app/guides/data-fetching-patterns.md
|
|
28
|
+
- Writing a custom primitive (called under an owner, `MaybeAccessor`, `getOwner` / `runWithOwner` / `isDisposed` after async): https://v2-rebuild--solid-docs-v2.netlify.app/guides/custom-primitives.md, https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/advanced/owner-introspection/get-owner.md, https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/advanced/owner-introspection/run-with-owner.md, https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/advanced/owner-introspection/is-disposed.md
|
|
29
|
+
- Wrapping a non-Solid library, push feeds with a reactive argument, web components (`prop:`, event names): https://v2-rebuild--solid-docs-v2.netlify.app/guides/integrate-non-solid-code.md
|
|
30
|
+
- Typed env (`env.ts` schema, `VITE_` client keys), `import "server-only"`, what compiles `"use server"`: https://v2-rebuild--solid-docs-v2.netlify.app/building-apps/environment.md, https://v2-rebuild--solid-docs-v2.netlify.app/reference/vite-plugin-solid/modules.md, https://v2-rebuild--solid-docs-v2.netlify.app/reference/vite-plugin-solid/server-functions.md
|
|
31
|
+
- Sessions and cookies (`parseCookieHeader` / `serializeCookie`, `RequestEventLocals` typing, when a cookie write commits): https://v2-rebuild--solid-docs-v2.netlify.app/building-apps/sessions-and-auth.md, https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-web/request-response/cookies.md
|
|
32
|
+
- Middleware chains and API routes / webhooks (`createAPIHandler`, `httpMethods: true`): https://v2-rebuild--solid-docs-v2.netlify.app/building-apps/middleware-and-api-routes.md, https://v2-rebuild--solid-docs-v2.netlify.app/reference/filesystem-routing/api.md
|
|
33
|
+
- Auth headers for every call, per-call `signal` / `keepalive` (`configureServerFunctionsClient`, `invoke`): https://v2-rebuild--solid-docs-v2.netlify.app/building-apps/server-functions/metadata-and-transport.md, https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-web/server-functions/configure-client.md, https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-web/server-functions/invoke.md
|
|
34
|
+
- Server-function limits (`bodySizeLimit`, `maxArguments`): https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-web/server-functions/host-configuration.md
|
|
35
|
+
- Static prerendering (`prerender-crawler/vite`, `prerendered()`), choosing client / SSR / prerender: https://v2-rebuild--solid-docs-v2.netlify.app/guides/choose-a-rendering-mode.md
|
|
36
|
+
- SSR-safe code (what runs on the server, hydration mismatches): https://v2-rebuild--solid-docs-v2.netlify.app/guides/ssr-safe-code.md
|
|
37
|
+
- Type helpers (`VoidProps`, `FlowProps`, `ComponentProps<"button">`, `JSX.EventHandler`, generic `<T,>` components): https://v2-rebuild--solid-docs-v2.netlify.app/guides/typescript.md, https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/types/component-types.md, https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/types/reactive-types.md, https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/types/jsx-types.md
|
|
38
|
+
- Head tag identity (OG `property`, `key`, layout `<Head>` groups): https://v2-rebuild--solid-docs-v2.netlify.app/building-apps/head-and-metadata.md
|
|
39
|
+
- Server-side tests (`createRequestEvent`, `provideRequestEvent` from `@solidjs/web/storage`, a Node Vitest project): https://v2-rebuild--solid-docs-v2.netlify.app/guides/testing.md, https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-web/request-response/provide-request-event.md
|
|
40
|
+
- Migrating 1.x data fetching (`createResource`, `resource.latest`): https://v2-rebuild--solid-docs-v2.netlify.app/migration/data-fetching-from-solid-1.md
|
|
41
|
+
- Terms used across the docs: https://v2-rebuild--solid-docs-v2.netlify.app/glossary.md; the mental model: https://v2-rebuild--solid-docs-v2.netlify.app/guides/thinking-in-solid.md
|
|
42
|
+
|
|
17
43
|
## Getting started
|
|
18
44
|
|
|
19
45
|
- https://v2-rebuild--solid-docs-v2.netlify.app/getting-started/quick-start.md
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "solid2-agent-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
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",
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
],
|
|
17
17
|
"scripts": {
|
|
18
18
|
"check:drift": "node scripts/check-docs-drift.mjs",
|
|
19
|
-
"
|
|
19
|
+
"check:upstream": "node scripts/check-upstream.mjs",
|
|
20
|
+
"test": "node tests/check.test.mjs && node tests/hook.test.mjs && node tests/doctor.test.mjs && node tests/init.test.mjs && node tests/upstream.test.mjs"
|
|
20
21
|
},
|
|
21
22
|
"engines": {
|
|
22
23
|
"node": ">=18"
|