solid2-agent-kit 0.16.0 → 0.18.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 +2 -1
- package/bin/baseline.mjs +41 -0
- package/bin/solid2-kit.mjs +52 -15
- package/files/commands/solid2-review.md +4 -3
- package/files/shared/agents-section.md +1 -1
- package/files/shared/rules-body.md +13 -13
- package/files/skills/solid-2/SKILL.md +34 -29
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ Supported agents: **Cursor** and **Claude Code**.
|
|
|
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` / `@tanstack/solid-router` (declared, or installed from the `latest` tag — Solid 2 releases are on `next`; TanStack Router'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) |
|
|
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`), a Solid 2 prerelease older than the kit's baseline in `bin/baseline.mjs` (`@solidjs/web` / `@solidjs/signals` / `@solidjs/diagnostics` `latest` is an older rc than `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) |
|
|
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
|
|
@@ -169,6 +169,7 @@ markdown with an `.md` suffix).
|
|
|
169
169
|
|
|
170
170
|
```
|
|
171
171
|
bin/solid2-kit.mjs CLI: init / sync / check / doctor / hook
|
|
172
|
+
bin/baseline.mjs Solid 2 versions the kit is verified against (doctor + check:upstream)
|
|
172
173
|
files/shared/rules-body.md hard rules (rendered into .mdc and CLAUDE.md)
|
|
173
174
|
files/shared/agents-section.md AGENTS.md managed-block content
|
|
174
175
|
files/skills/solid-2/ SKILL.md + references/official-docs.md
|
package/bin/baseline.mjs
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// The Solid 2 releases the kit's guidance is verified against, shared by
|
|
2
|
+
// `solid2-kit doctor` (a project on an older release is flagged) and
|
|
3
|
+
// check:upstream (scripts/upstream-probes/registry.mjs: a newer release is
|
|
4
|
+
// flagged). `line` is the major the kit targets: check:upstream installs the
|
|
5
|
+
// newest dist-tagged version on it. `baseline` is the version every upstream
|
|
6
|
+
// probe was verified against — bump it (and fix any marked claim that changed)
|
|
7
|
+
// after re-verifying.
|
|
8
|
+
//
|
|
9
|
+
// npm dist-tags lag each other on these prereleases (@solidjs/web, @solidjs/signals
|
|
10
|
+
// and @solidjs/diagnostics `latest` is an older 2.0 rc than `next`;
|
|
11
|
+
// @solidjs/vite-plugin's `next` is behind `latest`), so a bare install can
|
|
12
|
+
// resolve a release older than the baseline.
|
|
13
|
+
export const BASELINE = {
|
|
14
|
+
'solid-js': { line: 2, baseline: '2.0.0-rc.9' },
|
|
15
|
+
'@solidjs/web': { line: 2, baseline: '2.0.0-rc.9' },
|
|
16
|
+
'@solidjs/signals': { line: 2, baseline: '2.0.0-rc.9' },
|
|
17
|
+
'@solidjs/diagnostics': { line: 2, baseline: '2.0.0-rc.9' },
|
|
18
|
+
'@solidjs/router': { line: 2, baseline: '2.0.0-next.27' },
|
|
19
|
+
'@solidjs/vite-plugin': { line: 3, baseline: '3.0.0-next.44' },
|
|
20
|
+
'@solidjs/testing-library': { line: 1, baseline: '1.0.0-beta.3' },
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function parseVersion(version) {
|
|
24
|
+
const [core, pre] = version.split('+')[0].split(/-(.*)/s);
|
|
25
|
+
return { core: core.split('.').map(Number), pre: pre ? pre.split('.') : [] };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Semver precedence, prerelease-aware (2.0.0-rc.10 > 2.0.0-rc.9 < 2.0.0).
|
|
29
|
+
export function compareVersions(a, b) {
|
|
30
|
+
const x = parseVersion(a);
|
|
31
|
+
const y = parseVersion(b);
|
|
32
|
+
for (let i = 0; i < 3; i++) if (x.core[i] !== y.core[i]) return x.core[i] - y.core[i];
|
|
33
|
+
if (!x.pre.length || !y.pre.length) return y.pre.length - x.pre.length;
|
|
34
|
+
for (let i = 0; i < Math.max(x.pre.length, y.pre.length); i++) {
|
|
35
|
+
const [p, q] = [x.pre[i], y.pre[i]];
|
|
36
|
+
if (p === undefined || q === undefined) return p === undefined ? -1 : 1;
|
|
37
|
+
const numeric = /^\d+$/.test(p) && /^\d+$/.test(q);
|
|
38
|
+
if (p !== q) return numeric ? Number(p) - Number(q) : /^\d+$/.test(p) ? -1 : /^\d+$/.test(q) ? 1 : p < q ? -1 : 1;
|
|
39
|
+
}
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
package/bin/solid2-kit.mjs
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
} from 'node:fs';
|
|
25
25
|
import { dirname, join, relative, resolve } from 'node:path';
|
|
26
26
|
import { fileURLToPath } from 'node:url';
|
|
27
|
+
import { BASELINE, compareVersions } from './baseline.mjs';
|
|
27
28
|
|
|
28
29
|
const KIT_ROOT = fileURLToPath(new URL('..', import.meta.url));
|
|
29
30
|
const VERSION = JSON.parse(readFileSync(join(KIT_ROOT, 'package.json'), 'utf8')).version;
|
|
@@ -273,7 +274,7 @@ function init() {
|
|
|
273
274
|
if (hooksNote) console.log(` note: ${hooksNote}`);
|
|
274
275
|
if (!hasDiagnosticsDependency(target)) {
|
|
275
276
|
console.log(
|
|
276
|
-
|
|
277
|
+
` note: the solid-2 skill's development loop drives the dev server's /__solid/diagnostics endpoint; add "@solidjs/diagnostics" to devDependencies (install @solidjs/diagnostics@^${BASELINE['@solidjs/diagnostics'].baseline} — npm \`latest\` can be an older rc) so @solidjs/vite-plugin serves it (auto-on when declared). Without it the fallback is an isDev-guarded attribution.enable({ log: false }).`,
|
|
277
278
|
);
|
|
278
279
|
}
|
|
279
280
|
}
|
|
@@ -1221,9 +1222,10 @@ const BANNED_DEPS = {
|
|
|
1221
1222
|
};
|
|
1222
1223
|
|
|
1223
1224
|
// Packages whose npm `latest` dist-tag is still the Solid 1.x line (solid-js
|
|
1224
|
-
// 1.9, @solidjs/router 1.0, @solidjs/meta 0.29, @
|
|
1225
|
-
//
|
|
1226
|
-
//
|
|
1225
|
+
// 1.9, @solidjs/router 1.0, @solidjs/meta 0.29, @solidjs/testing-library 0.8,
|
|
1226
|
+
// @tanstack/solid-router 1.x, @tanstack/solid-query 5.x): a bare
|
|
1227
|
+
// `pnpm add <name>` installs it. The Solid 2 releases are published under
|
|
1228
|
+
// `next` (TanStack: `rc` — solid-router 2.0.0-rc.x, solid-query 6.0.0-rc.x).
|
|
1227
1229
|
const SOLID2_LINE_PACKAGES = [
|
|
1228
1230
|
{ name: 'solid-js', id: 'solid-js-version', solid1: /^[\s^~=v]*[01](?:\.|$)/, fix: 'Install solid-js@next (^2).' },
|
|
1229
1231
|
{
|
|
@@ -1244,8 +1246,45 @@ const SOLID2_LINE_PACKAGES = [
|
|
|
1244
1246
|
solid1: /^[\s^~=v]*[01](?:\.|$)/,
|
|
1245
1247
|
fix: 'TanStack Router 1.x has a solid-js ^1.9 peer; install @tanstack/solid-router@rc (2.x, solid-js 2 peer).',
|
|
1246
1248
|
},
|
|
1249
|
+
{
|
|
1250
|
+
name: '@tanstack/solid-query',
|
|
1251
|
+
id: 'tanstack-query-version',
|
|
1252
|
+
solid1: /^[\s^~=v]*[0-5](?:\.|$)/,
|
|
1253
|
+
fix: 'TanStack Query 5.x has a solid-js ^1.6 peer; install @tanstack/solid-query@rc (6.x, solid-js 2 peer).',
|
|
1254
|
+
},
|
|
1255
|
+
{
|
|
1256
|
+
name: '@solidjs/testing-library',
|
|
1257
|
+
id: 'testing-library-version',
|
|
1258
|
+
solid1: /^[\s^~=v]*0(?:\.|$)/,
|
|
1259
|
+
fix: 'Testing Library 0.x renders through solid-js/web (Solid 1); install @solidjs/testing-library@next (1.x, solid-js 2 peer).',
|
|
1260
|
+
},
|
|
1247
1261
|
];
|
|
1248
1262
|
|
|
1263
|
+
// Packages doctor checks the version of: the Solid 1.x lines above, then any
|
|
1264
|
+
// release older than the kit's baseline (bin/baseline.mjs) — a bare
|
|
1265
|
+
// `npm i @solidjs/web` installs `latest`, 2.0.0-rc.0, which the current
|
|
1266
|
+
// @solidjs/vite-plugin's `@solidjs/web ^2.0.0-rc.9` peer rejects.
|
|
1267
|
+
const VERSION_CHECKED = [...new Set([...SOLID2_LINE_PACKAGES.map((p) => p.name), ...Object.keys(BASELINE)])];
|
|
1268
|
+
|
|
1269
|
+
// The lower bound of a single-comparator range ("^2.0.0-rc.0", "~2.0.0-rc.8",
|
|
1270
|
+
// ">=2.0.0-rc.0", "2.0.0-rc.0") or an installed version. Dist-tags, "*",
|
|
1271
|
+
// unions and protocol specs have no bound to read; their install is checked.
|
|
1272
|
+
const LOWER_BOUND = /^\s*(?:[\^~]|>=|=)?\s*v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)\s*$/;
|
|
1273
|
+
|
|
1274
|
+
// `declared` ranges are judged by their floor: npm installs the `latest` tag
|
|
1275
|
+
// whenever it satisfies the range, so "^2.0.0-rc.0" resolves to rc.0 today.
|
|
1276
|
+
function versionProblem(name, version, declared) {
|
|
1277
|
+
const line = SOLID2_LINE_PACKAGES.find((p) => p.name === name);
|
|
1278
|
+
if (line?.solid1.test(version)) return `the Solid 1.x line; this kit teaches Solid 2.x. ${line.fix}`;
|
|
1279
|
+
const baseline = BASELINE[name]?.baseline;
|
|
1280
|
+
const lowest = version.match(LOWER_BOUND)?.[1];
|
|
1281
|
+
if (baseline && lowest && compareVersions(lowest, baseline) < 0) {
|
|
1282
|
+
const older = declared ? 'allows releases older than' : 'older than';
|
|
1283
|
+
return `${older} ${baseline}, the release this kit's guidance is verified against (npm dist-tags lag on the Solid 2 prereleases, so a bare install can resolve an older one). Install ${name}@^${baseline}.`;
|
|
1284
|
+
}
|
|
1285
|
+
return null;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1249
1288
|
function installedVersion(target, name) {
|
|
1250
1289
|
try {
|
|
1251
1290
|
return JSON.parse(readFileSync(join(target, 'node_modules', name, 'package.json'), 'utf8')).version;
|
|
@@ -1270,21 +1309,19 @@ function doctorFindings(target) {
|
|
|
1270
1309
|
for (const [name, message] of Object.entries(BANNED_DEPS)) {
|
|
1271
1310
|
if (name in deps) report(`dep-${name.replace(/[@/]/g, '')}`, `package.json depends on "${name}". ${message}`);
|
|
1272
1311
|
}
|
|
1273
|
-
for (const
|
|
1312
|
+
for (const name of VERSION_CHECKED) {
|
|
1313
|
+
const id = SOLID2_LINE_PACKAGES.find((p) => p.name === name)?.id ?? `${name.replace('@solidjs/', '')}-version`;
|
|
1274
1314
|
const range = deps[name];
|
|
1275
|
-
if (typeof range
|
|
1276
|
-
|
|
1315
|
+
if (typeof range !== 'string') continue;
|
|
1316
|
+
const declared = versionProblem(name, range, true);
|
|
1317
|
+
if (declared) {
|
|
1318
|
+
report(id, `package.json pins ${name} "${range}" — ${declared}`);
|
|
1277
1319
|
continue;
|
|
1278
1320
|
}
|
|
1279
|
-
// A range such as "latest" or "*" only shows its
|
|
1280
|
-
if (range === undefined) continue;
|
|
1321
|
+
// A range such as "latest" or "*" only shows its version once resolved.
|
|
1281
1322
|
const installed = installedVersion(target, name);
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
id,
|
|
1285
|
-
`node_modules has ${name} ${installed} (package.json: "${range}") — the Solid 1.x line; this kit teaches Solid 2.x. ${fix}`,
|
|
1286
|
-
);
|
|
1287
|
-
}
|
|
1323
|
+
const resolved = installed && versionProblem(name, installed, false);
|
|
1324
|
+
if (resolved) report(id, `node_modules has ${name} ${installed} (package.json: "${range}") — ${resolved}`);
|
|
1288
1325
|
}
|
|
1289
1326
|
|
|
1290
1327
|
for (const entry of readdirSync(target, { withFileTypes: true })) {
|
|
@@ -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
|
|
@@ -50,8 +50,9 @@ Steps:
|
|
|
50
50
|
`configureClientErrors` / `configureServerErrors` / `render(fn, root, undefined, { onError })`
|
|
51
51
|
(options are `render`'s 4th argument — a 3rd-argument object is `init`);
|
|
52
52
|
- store setter callbacks that `await` (the draft closes when the callback returns —
|
|
53
|
-
`[ASYNC_STORE_SETTER]`), store setters called at
|
|
54
|
-
`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 -->;
|
|
55
56
|
- nested fetches assumed to waterfall, or `<Loading>` lifted along with a lifted fetch;
|
|
56
57
|
- mutations on a value read through `live()` that settle on `yield save()` alone or
|
|
57
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 — markup types are `JSX.Element` from `@solidjs/web` or `Element` from `solid-js` (alias it `SolidElement` next to DOM code)
|
|
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.
|
|
@@ -87,7 +87,7 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
87
87
|
from `apply` — `onCleanup` there never runs (`[NO_OWNER_CLEANUP]`).
|
|
88
88
|
Single-argument `createEffect(fn)` is an error in Solid 2. Do not substitute
|
|
89
89
|
`createTrackedEffect` for that — it is `@deprecated` since 2.0.0-rc.9 (kept only for
|
|
90
|
-
1.x migration)
|
|
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.
|
|
91
91
|
9. **Stores update by mutating a draft**: `setStore(draft => { draft.user.name = "Ada" })`.
|
|
92
92
|
Never rebuild with spreads — that destroys property-level subscriptions. The return
|
|
93
93
|
form is for shapes where mutation is awkward — most commonly removal
|
|
@@ -107,10 +107,10 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
107
107
|
array), or assign a new instance in the setter (`draft.tags = new Set([...draft.tags, t])`).
|
|
108
108
|
The setter callback is a **synchronous** transaction: `setStore(async (draft) => …)`
|
|
109
109
|
throws `[ASYNC_STORE_SETTER]` in dev (writes after the first `await` were silently
|
|
110
|
-
lost before)
|
|
110
|
+
lost before) <!-- upstream:async-store-setter-throws -->. `await` first, then call the setter (inside an `action`, `yield` first).
|
|
111
111
|
Mechanically enforced by `solid2-kit check`. A store setter called at
|
|
112
112
|
component-body top level is a write in an owned scope (`[REACTIVE_WRITE_IN_OWNED_SCOPE]`,
|
|
113
|
-
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.
|
|
114
114
|
10. **Async data is an async computation**: `createMemo(async () => ...)` (or
|
|
115
115
|
`createMemo(() => fetchUser(id()))`) read under `<Loading>` / `<Errored>`.
|
|
116
116
|
**Fetch high, block low** — creating/reading are like sync; consuming (await)
|
|
@@ -137,7 +137,7 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
137
137
|
use `loadingValue` / `seedLoadingValue` as the default first-flight UI — those skip
|
|
138
138
|
`<Loading>`. `{latest(() => x())}` is a preview, not the visible answer, and not a
|
|
139
139
|
null-safe probe: `latest(user)` before the first value throws `NotReadyError` in every
|
|
140
|
-
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 -->
|
|
141
141
|
Default navigation holds: keep `selectedId()` so highlight and content stay
|
|
142
142
|
consistent; `latest(selectedId)` only when the highlight should move first.
|
|
143
143
|
Pair `class={{ pending: isPending(selectedId) }}` with a short CSS
|
|
@@ -290,7 +290,7 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
290
290
|
or one that reads a separate `createStore`); the value form
|
|
291
291
|
`createOptimisticStore([])` / `createOptimistic(false)` is a pure overlay with no
|
|
292
292
|
durable layer — action writes vanish on settle (a saved row disappears) and writes
|
|
293
|
-
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
|
|
294
294
|
body as a generator (`function*` / `async function*`) suspending on `yield`, never a
|
|
295
295
|
plain `async` function (`solid2-kit check` flags that). After a bare `await`, put a
|
|
296
296
|
bare `yield` before the next write **and** before anything that creates a reader
|
|
@@ -360,7 +360,7 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
360
360
|
because the named export is not a call-site literal. Select a component from reactive
|
|
361
361
|
state with `dynamic(() => ...)` from `@solidjs/web`
|
|
362
362
|
(stable identity); `<Dynamic component={...}>` is deprecated (`@deprecated` in the
|
|
363
|
-
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 })`
|
|
364
364
|
at module scope — not JSX `<Route>` / `<A>` / `<HashRouter>`. That package's `action` /
|
|
365
365
|
`query` are URL-addressable POST forms and a read cache — not core `action` /
|
|
366
366
|
`refresh` from `solid-js`. Router mutations: `<form action={save} method="post">`
|
|
@@ -402,7 +402,7 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
402
402
|
call whatever its status, so a failure is `throw respond(body, { status: 400 })`. Production sanitizes every thrown value that
|
|
403
403
|
crosses the wire — server-function throws **and** SSR render errors (an `<Errored>`
|
|
404
404
|
fallback printing `err().message` shows `"Internal Server Error"` in a production
|
|
405
|
-
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
|
|
406
406
|
`throw respond(body, { status })`. Reads are function calls too (`await getUser(id)`
|
|
407
407
|
on a `GET()`-declared read; the method mirrors the operation). `redirect()` /
|
|
408
408
|
`reload()` are signals for an integration: Solid Router's `action` / `query` and the
|
|
@@ -438,7 +438,7 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
438
438
|
send `Date` / `Map` / `Set` as ISO strings / arrays (they throw otherwise; one `File` /
|
|
439
439
|
`Blob` / `FormData` argument travels natively). `enableRichArguments()` from
|
|
440
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`). Declare live reads as `live(GET(fn))` —
|
|
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))` —
|
|
442
442
|
`live()` outermost. A plain `async function*` server function is an event stream on
|
|
443
443
|
one connection (no reconnect when it drops); `live()` is one value that changes over
|
|
444
444
|
time and reconnects. `live()` connection state is
|
|
@@ -477,9 +477,9 @@ directory (default `src/`).
|
|
|
477
477
|
| Never write (Solid 1.x) | Write instead (Solid 2) |
|
|
478
478
|
|---|---|
|
|
479
479
|
| `import ... from "solid-js/store"` or `"solid-js/web"` | stores/`merge`/`omit` from `"solid-js"`; `render`/`hydrate`/`Portal`/`Dynamic` from `"@solidjs/web"` |
|
|
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 |
|
|
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 --> |
|
|
481
481
|
| `createResource` | async `createMemo` + `<Loading>`/`<Errored>`; `refresh()`, `latest()`, `isPending()` |
|
|
482
|
-
| `createEffect(fn)` (one arg), `on(...)`, `createTrackedEffect` (deprecated in 2.0) | `createEffect(compute, apply)`; deps belong in `compute`; one-time DOM work is `onSettled` |
|
|
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` |
|
|
483
483
|
| `onMount` | `onSettled(() => { ...; return cleanup })` — return the cleanup; `onCleanup`, memo/effect creation, and `flush()` throw inside the callback |
|
|
484
484
|
| `batch(...)` | delete it — writes auto-batch; `flush()` only to observe synchronously |
|
|
485
485
|
| `<Suspense>`, `<ErrorBoundary>`, `<SuspenseList>` | `<Loading>`, `<Errored>` (fallback gets an error *accessor*), `<Reveal>` |
|
|
@@ -493,7 +493,7 @@ directory (default `src/`).
|
|
|
493
493
|
| `startTransition`, `useTransition` | automatic held updates + `isPending` |
|
|
494
494
|
| `onError` / `catchError` | `<Errored>` or effect bundle `{ effect, error }` |
|
|
495
495
|
| `from(...)` / `observable(...)` (the solid-js helpers) | inbound: async iterable from a memo; outbound: split effect |
|
|
496
|
-
| `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` |
|
|
497
497
|
| `renderToStringAsync(...)` | `await renderToStream(() => <App />)` (one consumer: `pipe` / `pipeTo` / `readable`) |
|
|
498
498
|
| `clearDelegatedEvents` | delete — delegated listeners are scoped to each render root |
|
|
499
499
|
| `vite-plugin-solid` / `jsxImportSource: "solid-js"` | `@solidjs/vite-plugin` / `"jsxImportSource": "@solidjs/web"` |
|
|
@@ -513,4 +513,4 @@ Solid 1.x or React memory.
|
|
|
513
513
|
`createRenderEffect` exists in Solid 2 but is not a default. `storePath`
|
|
514
514
|
(the 1.x path-setter migration helper) is no longer exported from `solid-js` as of
|
|
515
515
|
2.0.0-rc.9 — convert path setters to draft setters instead of importing it from
|
|
516
|
-
`@solidjs/signals`.
|
|
516
|
+
`@solidjs/signals`. <!-- upstream:store-path-removed -->
|
|
@@ -69,7 +69,7 @@ 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)`) |
|
|
@@ -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
|
|
@@ -261,7 +261,7 @@ variable in JSX (`const View = tab() ? A : B` freezes the choice at setup).
|
|
|
261
261
|
`<Dynamic component={...}>` is the same primitive with a worse shape (the tag travels
|
|
262
262
|
in the props bag, so every instance merges and omits it and rebuilds a factory) and
|
|
263
263
|
is **deprecated** since 2.0.0-rc.9 — `@deprecated` in the types, no runtime warning,
|
|
264
|
-
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
|
|
265
265
|
constant tag) instead.
|
|
266
266
|
|
|
267
267
|
### Two-phase effect (imperative boundary only)
|
|
@@ -435,7 +435,7 @@ return <StoryDetail story={story} />;
|
|
|
435
435
|
`isPending(() => results())`). Freshest in-flight value for a preview:
|
|
436
436
|
`latest(results)`. `latest` is not a null-safe probe: before the source's first value
|
|
437
437
|
it throws `NotReadyError` in every scope (since 2.0.0-rc.9 also in event handlers and
|
|
438
|
-
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()` /
|
|
439
439
|
`isPending()` only from the flush that carries it — `flush()` first when a test reads
|
|
440
440
|
its own write through them. Do not start `fetch` at component-body top level.
|
|
441
441
|
- Coordinate sibling `<Loading>` reveal with `<Reveal>` (`order="sequential"` default,
|
|
@@ -511,7 +511,7 @@ Uncaught errors (`REACTIVITY_HALTED`) go to the platform's `reportError`, which
|
|
|
511
511
|
monitor already listens on — do not report them twice. `renderToStream` /
|
|
512
512
|
`renderToString`'s `onError` option *is* the per-render server hook: it now hears every
|
|
513
513
|
handled failure, so filter on `context.handling === "failed"` when only request-failing
|
|
514
|
-
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 -->
|
|
515
515
|
|
|
516
516
|
### Streams and subscriptions (websockets, reactive clients)
|
|
517
517
|
|
|
@@ -650,7 +650,8 @@ file of server functions. Do not rewrite `App.tsx` with loading/error branches,
|
|
|
650
650
|
and do not snapshot-the-cache / write-a-prediction / restore-on-error. A cache
|
|
651
651
|
library is not the productionizing step — the sync mutation already was the
|
|
652
652
|
prediction. (Router `query` or `@tanstack/solid-query` remain valid when the
|
|
653
|
-
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.)
|
|
654
655
|
Optimistic rows stay interactive — a not-yet-acked todo can still
|
|
655
656
|
be toggled. Do not disable the control until confirmation, and do not mutex
|
|
656
657
|
rapid clicks; `action` is the transaction (no half-mutation, no interleaved
|
|
@@ -666,7 +667,7 @@ handler or the `action`.
|
|
|
666
667
|
`createOptimistic(value)`) is a pure overlay: writes inside an action vanish when it
|
|
667
668
|
settles, and writes outside an action do not stick. Copying the reference example
|
|
668
669
|
`createOptimisticStore<Todo[]>([])` and writing the saved row after the `yield`
|
|
669
|
-
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
|
|
670
671
|
from the function form — `createOptimisticStore(() => api.list(), [])` refreshed
|
|
671
672
|
after the `yield`, or `createOptimisticStore(() => base.todos, [])` over a separate
|
|
672
673
|
`createStore` that the action writes the confirmed value into. Keep the value form for
|
|
@@ -1016,7 +1017,8 @@ writes from owned scopes, and async reads outside `<Loading>`. Fix them; don't s
|
|
|
1016
1017
|
A read at component-body top level reports `[STRICT_READ_UNTRACKED]` naming the
|
|
1017
1018
|
component — move the read into JSX, a memo, or an effect compute.
|
|
1018
1019
|
|
|
1019
|
-
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
|
|
1020
1022
|
the tree has an owner, and `flush()` after interactions that stage writes:
|
|
1021
1023
|
|
|
1022
1024
|
```tsx
|
|
@@ -1041,7 +1043,7 @@ write and `flush()`: `const { setA, b, dispose } = createRoot((dispose) => { …
|
|
|
1041
1043
|
{ setA, b, dispose }; }); setA(2); flush();`. A hook that reads context runs under
|
|
1042
1044
|
`renderHook(useCart, { wrapper: CartProvider })`. A routed component gets a router built in
|
|
1043
1045
|
the test — `createRouter({ routes, history: memoryHistory("/orders/42") })` — because
|
|
1044
|
-
Testing Library's `render(…, { location })` is typed but not implemented (1.0.0-beta.3).
|
|
1046
|
+
Testing Library's `render(…, { location })` is typed but not implemented (1.0.0-beta.3). <!-- upstream:testing-library-location -->
|
|
1045
1047
|
|
|
1046
1048
|
`jsxImportSource` for tests and app code is `"@solidjs/web"`, not `"solid-js"`.
|
|
1047
1049
|
Vite plugin is `@solidjs/vite-plugin`, not `vite-plugin-solid`.
|
|
@@ -1068,14 +1070,14 @@ in observe and dev) and **checks** (dev-only guidance printed by the console rep
|
|
|
1068
1070
|
Always-on dev checks include misplaced reads
|
|
1069
1071
|
(`[STRICT_READ_UNTRACKED]`, `[PENDING_ASYNC_UNTRACKED_READ]`), misplaced writes
|
|
1070
1072
|
(`[REACTIVE_WRITE_IN_OWNED_SCOPE]` — since rc.9 also for store setters called in a
|
|
1071
|
-
component or root body
|
|
1072
|
-
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
|
|
1073
1075
|
`[ASYNC_OUTSIDE_LOADING_BOUNDARY]` when a tracked read has no boundary above it.
|
|
1074
1076
|
Each entry carries a stable `[CODE]`, what the runtime observed, and an owner chain
|
|
1075
1077
|
(`in <App> › <Cart> › <LineItem>`); never silence a code before understanding it.
|
|
1076
1078
|
Since rc.9 the server render reports on the same channel with the same owner chains:
|
|
1077
1079
|
`[SSR_RENDER_ERROR_CONTAINED]` (a boundary routed a render error; `ownerPath` is where
|
|
1078
|
-
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
|
|
1079
1081
|
behind a production-sanitized error), `[SSR_CLIENT_CONTENT_MASKED]` (client-only content
|
|
1080
1082
|
that surfaced only after a real server wait — the wait was wasted), server-side
|
|
1081
1083
|
`[ASYNC_WATERFALL]`, and `[REVEAL_IN_RENDER_TO_STRING]`.
|
|
@@ -1086,7 +1088,7 @@ code maps to what the runtime observed and what to change.
|
|
|
1086
1088
|
Cost and responsiveness findings need the opt-in attribution engine, which records
|
|
1087
1089
|
every scope that re-ran, what changed to cause it, and how long it took. Since
|
|
1088
1090
|
`solid-js` 2.0.0-rc.8 the engine lives behind its own subpath — `DEV.attribution`
|
|
1089
|
-
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
|
|
1090
1092
|
traversal, and console reporting). `OBSERVE` is the public observability surface on
|
|
1091
1093
|
the observe and dev builds — `OBSERVE.records`, `OBSERVE.diagnostics`,
|
|
1092
1094
|
`OBSERVE.exclude`, `OBSERVE.server.trace` are for app and tool code; only the
|
|
@@ -1094,7 +1096,7 @@ the observe and dev builds — `OBSERVE.records`, `OBSERVE.diagnostics`,
|
|
|
1094
1096
|
to engines, routers, and devtools. Since 2.0.0-rc.9 the folds, queries, and
|
|
1095
1097
|
formatters are **named exports** of that subpath, not methods of `attribution`
|
|
1096
1098
|
(`attribution.why` / `.costs` / `.feedback` / `.subscriptions` / `.format` are type
|
|
1097
|
-
errors)
|
|
1099
|
+
errors) <!-- upstream:attribution-named-exports -->; `attribution` keeps `enable`, `disable`, `subscribe`, `markFlight`, and the
|
|
1098
1100
|
record ring buffers (`history`, `holds`, `waterfalls`, `navigations`, `interactions`):
|
|
1099
1101
|
|
|
1100
1102
|
```ts
|
|
@@ -1177,7 +1179,8 @@ effects** — a pure markup or styling change does not need it.
|
|
|
1177
1179
|
2. **Name the scopes you changed** (`createMemo(fn, { name: "total" })`,
|
|
1178
1180
|
`createStore(v, { name: "cart" })`) so chains and tables refer to them by name.
|
|
1179
1181
|
3. **Record the interaction and ask.** First choice — no app code — is the diagnostics
|
|
1180
|
-
endpoint: with `@solidjs/diagnostics` in `devDependencies
|
|
1182
|
+
endpoint: with `@solidjs/diagnostics` in `devDependencies` at the same version as
|
|
1183
|
+
`solid-js` (npm `latest` can lag; `solid2-kit doctor` flags an older one), the Vite plugin's
|
|
1181
1184
|
`diagnostics` option (auto-on when the package is declared; dev server only, never
|
|
1182
1185
|
under vitest, `vite build`, or preview) injects the capture bridge and serves
|
|
1183
1186
|
`/__solid/diagnostics`. `begin` enables attribution with `log: false` for the
|
|
@@ -1301,7 +1304,7 @@ Solid Router 0.x/1.x stand-ins.** Patterns below; fetch the matching pages from
|
|
|
1301
1304
|
```
|
|
1302
1305
|
|
|
1303
1306
|
`solid-js` does **not** export a `JSX` namespace — `import type { JSX } from "solid-js"`
|
|
1304
|
-
is a type error (TS2305)
|
|
1307
|
+
is a type error (TS2305) <!-- upstream:jsx-namespace-absent -->. Markup types are `JSX.Element` from `@solidjs/web` (the
|
|
1305
1308
|
docs' choice; it also admits DOM nodes) or the renderer-neutral `Element` from `solid-js`
|
|
1306
1309
|
(`children?: Element`), alongside `Component` / `ParentProps` / `Accessor` / `Setter`.
|
|
1307
1310
|
Import the latter as `import type { Element as SolidElement } from "solid-js"` in any file
|
|
@@ -1630,7 +1633,7 @@ response) close that loop. For 404 during SSR: `httpStatus(404)` from `@solidjs/
|
|
|
1630
1633
|
on the server-function wire **and**, since 2.0.0-rc.9, on every SSR road a failure
|
|
1631
1634
|
takes (an `<Errored>` fallback rendered on the server, a rejected async source
|
|
1632
1635
|
serialized into the stream, a `<Loading>` fragment's rejection), so `err().message`
|
|
1633
|
-
in a fallback reads the generic message in a production server render. Use
|
|
1636
|
+
in a fallback reads the generic message in a production server render. <!-- upstream:ssr-error-sanitized --> Use
|
|
1634
1637
|
`throw markSafeError(...)` or `throw respond(..., { status })` for intentional
|
|
1635
1638
|
client-facing failures — a *returned* `respond()` resolves the call whatever its
|
|
1636
1639
|
status, so `return respond({ error }, { status: 400 })` reads as success to the UI,
|
|
@@ -1641,7 +1644,8 @@ is success metadata a scripted caller unwraps. Arguments are JSON: send `Date` /
|
|
|
1641
1644
|
argument travels natively). `enableRichArguments()` from
|
|
1642
1645
|
`@solidjs/web/server-functions/rich-args` (at `src/App.tsx` module scope) lifts that, but
|
|
1643
1646
|
in rc.9 importing it fails `vite build` (`"./client" is not exported` — the dev server
|
|
1644
|
-
works, so the break shows only at build)
|
|
1647
|
+
works, so the break shows only at build); `resolve: { dedupe: ["@solidjs/web"] }` in the
|
|
1648
|
+
Vite config works around it. <!-- upstream:rich-args-vite-build https://github.com/solidjs/solid/issues/3627 -->
|
|
1645
1649
|
`GET()` is only for idempotent reads (URLs leak into logs/history);
|
|
1646
1650
|
import it from `@solidjs/web/server-functions`, never `@solidjs/start`.
|
|
1647
1651
|
Reads are function calls too: `GET(async (id) => { "use server"; ... })` is invoked
|
|
@@ -1889,20 +1893,20 @@ how to reuse. Prefer the form on the right.
|
|
|
1889
1893
|
| `submission.clear()` from an effect | on dismiss/resubmit only — from an effect, errors flash and vanish |
|
|
1890
1894
|
| `revalidate(liveSource)` | `live()` updates through the open stream; do not revalidate it |
|
|
1891
1895
|
| `refresh(user)` without `affects` when the reload should look pending | pair `affects(user)` with `refresh(user)` — a bare `refresh` re-asks quietly |
|
|
1892
|
-
| `<Dynamic component={tab() ? A : B} />` | `const View = dynamic(() => (tab() ? A : B))` — `<Dynamic>` is deprecated (rc.9); `dynamic()` hoists the factory and keeps identity stable |
|
|
1893
|
-
| `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]` |
|
|
1894
|
-
| `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]` |
|
|
1896
|
+
| `<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 --> |
|
|
1897
|
+
| `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 --> |
|
|
1898
|
+
| `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 --> |
|
|
1895
1899
|
| `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 |
|
|
1896
1900
|
| `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` |
|
|
1897
|
-
| `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())` |
|
|
1898
|
-
| `attribution.why(total)` / `attribution.format(e)` / `attribution.costs()` | named imports from `solid-js/attribution`: `why(total)`, `formatRerun(e)`, `costs()`, `feedback()`, `subscriptions(scope)` (rc.9) |
|
|
1901
|
+
| `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 --> |
|
|
1902
|
+
| `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 --> |
|
|
1899
1903
|
| `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 |
|
|
1900
1904
|
| `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) |
|
|
1901
1905
|
| `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 |
|
|
1902
1906
|
| `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 |
|
|
1903
1907
|
| 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` |
|
|
1904
1908
|
| `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 |
|
|
1905
|
-
| `import { storePath } from "solid-js"` | gone from `solid-js` (rc.9); write draft setters `setState((d) => { d.user.name = "Grace"; })` |
|
|
1909
|
+
| `import { storePath } from "solid-js"` | gone from `solid-js` (rc.9); write draft setters `setState((d) => { d.user.name = "Grace"; })` <!-- upstream:store-path-removed --> |
|
|
1906
1910
|
| `<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 |
|
|
1907
1911
|
| `useHead({ tag: "meta", ... })` for ordinary page tags | `<Title>` / `<Meta>` / `<Script>` (JSON-LD). `useHead` is the lower-level registry |
|
|
1908
1912
|
| two `<Meta property="og:image">` meant as one replacement set | wrap them in `<Head>` — a later group replaces the earlier set as one unit |
|
|
@@ -1917,11 +1921,11 @@ how to reuse. Prefer the form on the right.
|
|
|
1917
1921
|
| `<input type="checkbox" value={on()} />` | `checked={on()}` for the toggle; `value="string"` only when grouping radios or listing submitted values |
|
|
1918
1922
|
| `createMemo(() => { void save(); return x(); })` / an effect that calls an action | invoke actions from handlers, not from memos or effects |
|
|
1919
1923
|
| `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))` |
|
|
1920
|
-
| `createRenderEffect` / `createTrackedEffect` as the default effect | two-phase `createEffect(compute, apply)`; `createTrackedEffect` is `@deprecated` (rc.9) |
|
|
1924
|
+
| `createRenderEffect` / `createTrackedEffect` as the default effect | two-phase `createEffect(compute, apply)`; `createTrackedEffect` is `@deprecated` (rc.9) <!-- upstream:tracked-effect-deprecated --> |
|
|
1921
1925
|
| `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 |
|
|
1922
1926
|
| 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 |
|
|
1923
1927
|
| 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 |
|
|
1924
|
-
| `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 |
|
|
1928
|
+
| `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 --> |
|
|
1925
1929
|
| 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 |
|
|
1926
1930
|
| 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 |
|
|
1927
1931
|
| `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 |
|
|
@@ -2109,7 +2113,8 @@ always-applied rules installed alongside this skill.
|
|
|
2109
2113
|
cleanup or nothing.
|
|
2110
2114
|
- [ ] Dev console shows no diagnostics: reads in tracking scopes (no
|
|
2111
2115
|
`[STRICT_READ_UNTRACKED]`), store setters synchronous and never called at
|
|
2112
|
-
body level (no `[ASYNC_STORE_SETTER]`
|
|
2116
|
+
body level (no `[ASYNC_STORE_SETTER]` <!-- upstream:async-store-setter-throws --> /
|
|
2117
|
+
`[REACTIVE_WRITE_IN_OWNED_SCOPE]` <!-- upstream:store-write-owned-scope -->), held
|
|
2113
2118
|
writes paired with `isPending` / `latest` / optimistic values / `affects()`
|
|
2114
2119
|
(no `[SILENT_HOLD]`). Attribution-only costs
|
|
2115
2120
|
(`[IMMUTABLE_UPDATE_IN_STORE]`, `[UNSTABLE_LIST_IDENTITY]`, `[EFFECT_*]`)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "solid2-agent-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.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"
|