solid2-agent-kit 0.17.0 → 0.19.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 +66 -12
- package/files/commands/solid2-review.md +6 -3
- package/files/shared/agents-section.md +1 -1
- package/files/shared/rules-body.md +21 -13
- package/files/skills/solid-2/SKILL.md +98 -51
- package/files/skills/solid-2/references/official-docs.md +2 -0
- package/package.json +1 -1
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` / `@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) |
|
|
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.13' },
|
|
15
|
+
'@solidjs/web': { line: 2, baseline: '2.0.0-rc.13' },
|
|
16
|
+
'@solidjs/signals': { line: 2, baseline: '2.0.0-rc.13' },
|
|
17
|
+
'@solidjs/diagnostics': { line: 2, baseline: '2.0.0-rc.13' },
|
|
18
|
+
'@solidjs/router': { line: 2, baseline: '2.0.0-next.34' },
|
|
19
|
+
'@solidjs/vite-plugin': { line: 3, baseline: '3.0.0-next.47' },
|
|
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
|
}
|
|
@@ -479,6 +480,29 @@ function reactKeyPropFindings(content) {
|
|
|
479
480
|
// reads it from package.json / node_modules.
|
|
480
481
|
const TANSTACK_ROUTER_MODULE = /^@tanstack\/(?:solid-router|router-core)$/;
|
|
481
482
|
|
|
483
|
+
// `<Loading on={id}>` hands the boundary an accessor: `on` is a tracked
|
|
484
|
+
// expression whose value is irrelevant, so a bare accessor is never read and
|
|
485
|
+
// the boundary never re-arms. `on={id()}`, `on={props.id}` and
|
|
486
|
+
// `on={[a(), b()]}` do not match. The opening tag is scanned to its own `>`
|
|
487
|
+
// (braces balanced) so a `fallback={<p>…</p>}` before `on` does not end it.
|
|
488
|
+
function loadingOnAccessorFindings(content) {
|
|
489
|
+
const matches = [];
|
|
490
|
+
for (const tag of content.matchAll(/<Loading\b/g)) {
|
|
491
|
+
let index = tag.index + tag[0].length;
|
|
492
|
+
let depth = 0;
|
|
493
|
+
while (index < content.length) {
|
|
494
|
+
const ch = content[index];
|
|
495
|
+
if (ch === '{') depth += 1;
|
|
496
|
+
else if (ch === '}') depth -= 1;
|
|
497
|
+
else if (ch === '>' && depth === 0) break;
|
|
498
|
+
index += 1;
|
|
499
|
+
}
|
|
500
|
+
const bare = content.slice(tag.index, index).match(/\son=\{\s*[A-Za-z_$][\w$]*\s*\}/);
|
|
501
|
+
if (bare) matches.push({ index: tag.index + bare.index });
|
|
502
|
+
}
|
|
503
|
+
return matches;
|
|
504
|
+
}
|
|
505
|
+
|
|
482
506
|
// Local names bound by `import [Default,] { a, b as c } from "<module>"`.
|
|
483
507
|
function importedNames(content, moduleName) {
|
|
484
508
|
const names = new Set();
|
|
@@ -748,6 +772,13 @@ const CHECKS = [
|
|
|
748
772
|
message:
|
|
749
773
|
'Pass the accessor to isPending/latest, not the call: isPending(user), not isPending(user()). Calling it first evaluates the read before the helper runs.',
|
|
750
774
|
},
|
|
775
|
+
{
|
|
776
|
+
// `<Loading on={id}>` — the accessor is never read, so `on` never re-arms.
|
|
777
|
+
id: 'loading-on-accessor',
|
|
778
|
+
find: loadingOnAccessorFindings,
|
|
779
|
+
message:
|
|
780
|
+
'`on` is a tracked dependency list, not a key: a bare accessor is never read, so the boundary never re-arms. Read it — on={id()} — or list several inputs: on={[a(), b()]}.',
|
|
781
|
+
},
|
|
751
782
|
{
|
|
752
783
|
id: 'dynamic-jsx',
|
|
753
784
|
pattern: /<Dynamic[\s/>]/g,
|
|
@@ -1259,6 +1290,31 @@ const SOLID2_LINE_PACKAGES = [
|
|
|
1259
1290
|
},
|
|
1260
1291
|
];
|
|
1261
1292
|
|
|
1293
|
+
// Packages doctor checks the version of: the Solid 1.x lines above, then any
|
|
1294
|
+
// release older than the kit's baseline (bin/baseline.mjs) — a bare
|
|
1295
|
+
// `npm i @solidjs/web` installs `latest`, 2.0.0-rc.0, which the current
|
|
1296
|
+
// @solidjs/vite-plugin's `@solidjs/web ^2.0.0-rc.13` peer rejects.
|
|
1297
|
+
const VERSION_CHECKED = [...new Set([...SOLID2_LINE_PACKAGES.map((p) => p.name), ...Object.keys(BASELINE)])];
|
|
1298
|
+
|
|
1299
|
+
// The lower bound of a single-comparator range ("^2.0.0-rc.0", "~2.0.0-rc.8",
|
|
1300
|
+
// ">=2.0.0-rc.0", "2.0.0-rc.0") or an installed version. Dist-tags, "*",
|
|
1301
|
+
// unions and protocol specs have no bound to read; their install is checked.
|
|
1302
|
+
const LOWER_BOUND = /^\s*(?:[\^~]|>=|=)?\s*v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)\s*$/;
|
|
1303
|
+
|
|
1304
|
+
// `declared` ranges are judged by their floor: npm installs the `latest` tag
|
|
1305
|
+
// whenever it satisfies the range, so "^2.0.0-rc.0" resolves to rc.0 today.
|
|
1306
|
+
function versionProblem(name, version, declared) {
|
|
1307
|
+
const line = SOLID2_LINE_PACKAGES.find((p) => p.name === name);
|
|
1308
|
+
if (line?.solid1.test(version)) return `the Solid 1.x line; this kit teaches Solid 2.x. ${line.fix}`;
|
|
1309
|
+
const baseline = BASELINE[name]?.baseline;
|
|
1310
|
+
const lowest = version.match(LOWER_BOUND)?.[1];
|
|
1311
|
+
if (baseline && lowest && compareVersions(lowest, baseline) < 0) {
|
|
1312
|
+
const older = declared ? 'allows releases older than' : 'older than';
|
|
1313
|
+
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}.`;
|
|
1314
|
+
}
|
|
1315
|
+
return null;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1262
1318
|
function installedVersion(target, name) {
|
|
1263
1319
|
try {
|
|
1264
1320
|
return JSON.parse(readFileSync(join(target, 'node_modules', name, 'package.json'), 'utf8')).version;
|
|
@@ -1283,21 +1339,19 @@ function doctorFindings(target) {
|
|
|
1283
1339
|
for (const [name, message] of Object.entries(BANNED_DEPS)) {
|
|
1284
1340
|
if (name in deps) report(`dep-${name.replace(/[@/]/g, '')}`, `package.json depends on "${name}". ${message}`);
|
|
1285
1341
|
}
|
|
1286
|
-
for (const
|
|
1342
|
+
for (const name of VERSION_CHECKED) {
|
|
1343
|
+
const id = SOLID2_LINE_PACKAGES.find((p) => p.name === name)?.id ?? `${name.replace('@solidjs/', '')}-version`;
|
|
1287
1344
|
const range = deps[name];
|
|
1288
|
-
if (typeof range
|
|
1289
|
-
|
|
1345
|
+
if (typeof range !== 'string') continue;
|
|
1346
|
+
const declared = versionProblem(name, range, true);
|
|
1347
|
+
if (declared) {
|
|
1348
|
+
report(id, `package.json pins ${name} "${range}" — ${declared}`);
|
|
1290
1349
|
continue;
|
|
1291
1350
|
}
|
|
1292
|
-
// A range such as "latest" or "*" only shows its
|
|
1293
|
-
if (range === undefined) continue;
|
|
1351
|
+
// A range such as "latest" or "*" only shows its version once resolved.
|
|
1294
1352
|
const installed = installedVersion(target, name);
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
id,
|
|
1298
|
-
`node_modules has ${name} ${installed} (package.json: "${range}") — the Solid 1.x line; this kit teaches Solid 2.x. ${fix}`,
|
|
1299
|
-
);
|
|
1300
|
-
}
|
|
1353
|
+
const resolved = installed && versionProblem(name, installed, false);
|
|
1354
|
+
if (resolved) report(id, `node_modules has ${name} ${installed} (package.json: "${range}") — ${resolved}`);
|
|
1301
1355
|
}
|
|
1302
1356
|
|
|
1303
1357
|
for (const entry of readdirSync(target, { withFileTypes: true })) {
|
|
@@ -14,7 +14,8 @@ Steps:
|
|
|
14
14
|
4. Review every changed `.tsx`/`.jsx`/`.ts` file against the checklist, prioritizing what
|
|
15
15
|
regexes cannot see:
|
|
16
16
|
- reactive reads at component-body top level, or extracted into plain `const`s;
|
|
17
|
-
- async computations reading reactive inputs **after** the first `await
|
|
17
|
+
- async computations reading reactive inputs **after** the first `await` (dev reports
|
|
18
|
+
`[UNTRACKED_READ_AFTER_AWAIT]` on V8);
|
|
18
19
|
- effects that copy state instead of deriving it, or reading stores in the apply phase;
|
|
19
20
|
- `<For>` over server/refetched rows without a stable-id key function;
|
|
20
21
|
- core `action` bodies that are not generators (plain `async` functions) or that write
|
|
@@ -23,7 +24,8 @@ Steps:
|
|
|
23
24
|
- `<Loading>`/`<Errored>` wrapping page chrome instead of the data slot, or
|
|
24
25
|
`const u = user()` extracted then passed (a real parent-side read). Passing
|
|
25
26
|
`user={user()}` is the colorless form — do not "fix" it into accessors or
|
|
26
|
-
`Promise<User>` props;
|
|
27
|
+
`Promise<User>` props; `<Loading on={id}>` handed the accessor instead of a read
|
|
28
|
+
(`on={id()}`), or the boundary's data also read outside it (`[LOADING_ON_OUTSIDE_HOLD]`);
|
|
27
29
|
- `latest(selectedId)` as the default highlight (hold is the default);
|
|
28
30
|
`isPending` treated as a global spinner instead of a per-expression question;
|
|
29
31
|
- treating `<Errored>` as a terminal ErrorBoundary, or expecting it to catch
|
|
@@ -74,7 +76,8 @@ Steps:
|
|
|
74
76
|
thrown failures, or router forms without server-side validation (`throw respond(...)`)
|
|
75
77
|
and `useSubmissions` inline errors;
|
|
76
78
|
- browser-only *values* handled with a `clientOnly` component split or an `isServer`
|
|
77
|
-
branch instead of `ssrSource: "client"`
|
|
79
|
+
branch instead of `ssrSource: "client"` on the memo/signal/derived store (`"hybrid"`
|
|
80
|
+
only continues an async-iterable source after hydration <!-- upstream:ssr-source-hybrid-stream -->);
|
|
78
81
|
- held writes with no feedback (`isPending` / `latest` / optimistic value / `affects()`)
|
|
79
82
|
— the `[SILENT_HOLD]` shape — and dev-console diagnostics (`[STRICT_READ_UNTRACKED]`,
|
|
80
83
|
attribution-only store/list/effect costs) left unaddressed;
|
|
@@ -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) <!-- 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
|
|
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: "client"` on the memo/signal/derived store, 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.
|
|
@@ -125,14 +125,18 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
125
125
|
must `refresh()` that source — type that prop `SourceAccessor<User>` (`Accessor<User>`
|
|
126
126
|
is TS2345 at `refresh`) — `isPending(() => props.user)` works on the
|
|
127
127
|
value. Read every reactive input **before the first
|
|
128
|
-
`await`** — post-`await` reads do not subscribe
|
|
128
|
+
`await`** — post-`await` reads do not subscribe (dev reports `[UNTRACKED_READ_AFTER_AWAIT]`
|
|
129
|
+
on V8 engines), and in production the computation can
|
|
129
130
|
sit pending with no retry. No `useEffect` + `setState` fetching, no `createResource`.
|
|
130
131
|
`<Loading>` wraps the data slot, not page chrome. After first paint it keeps content
|
|
131
|
-
during refetch (`isPending` for the indicator).
|
|
132
|
-
accessor) only when that
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
132
|
+
during refetch (`isPending` for the indicator). Add `on={id()}` (a tracked *read*, not
|
|
133
|
+
the accessor `on={id}`) only when that change of subject should show the fallback again.
|
|
134
|
+
`on` is a dependency list, not a compared key: its value is irrelevant, a change to
|
|
135
|
+
anything it reads re-arms the boundary (several inputs: `on={[a(), b()]}`), and the
|
|
136
|
+
fallback lands with the frame of the change that caused it. <!-- upstream:loading-on-dependency-list -->
|
|
137
|
+
It can never show while the same data is also read outside the boundary (dev reports
|
|
138
|
+
`[LOADING_ON_OUTSIDE_HOLD]`) — move that read under the boundary so one hold owns the
|
|
139
|
+
data; that is how one slow panel stops holding the page. Do not start `fetch` (or any request) at component-body top
|
|
136
140
|
level — that runs once at mount and is not a reactive source. Do not `try/catch` `NotReadyError` around a read, and do not
|
|
137
141
|
use `loadingValue` / `seedLoadingValue` as the default first-flight UI — those skip
|
|
138
142
|
`<Loading>`. `{latest(() => x())}` is a preview, not the visible answer, and not a
|
|
@@ -323,9 +327,10 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
323
327
|
render. `NoHydration` / `Hydration` split hydration *ownership*; they do not choose
|
|
324
328
|
visible content — `clientOnly` is for components that must never run on the server.
|
|
325
329
|
A single browser-only *value* takes `ssrSource` on the memo/signal/derived store
|
|
326
|
-
(`"client"` skips the server value and computes after hydration, e.g. `localStorage
|
|
327
|
-
`"hybrid"`
|
|
328
|
-
|
|
330
|
+
(`"client"` skips the server value and computes after hydration, e.g. `localStorage`,
|
|
331
|
+
or server data mixed with the window size; `"hybrid"` adopts the serialized value and
|
|
332
|
+
then continues an async-iterable source on the client — for a sync or promise compute
|
|
333
|
+
it is identical to `"server"`, no re-run <!-- upstream:ssr-source-hybrid-stream -->) — do not split a component via `clientOnly` for one value. An
|
|
329
334
|
`onSettled` fill-in remains a valid alternative.
|
|
330
335
|
Pass a **function** to `render` / `hydrate` / `renderToString` / `renderToStream`
|
|
331
336
|
(`render(() => <App />, root)`), never `render(<App />, root)`. `hydrate` when the
|
|
@@ -348,7 +353,8 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
348
353
|
link previews) takes `{ deferStream: true }` on its memo; keep the boundary. `pipe` / `pipeTo` / `readable` each consume
|
|
349
354
|
a stream render — use exactly one. `createRoot` is for tests, libraries, and
|
|
350
355
|
non-render entry points; inside a component let `render` / the component owner
|
|
351
|
-
own the scope
|
|
356
|
+
own the scope (a root created under an owner is disposed with it — detach on purpose
|
|
357
|
+
with `runWithOwner(null, () => createRoot(...))`). Async reads inside `<Portal>` start on the client — hoist the
|
|
352
358
|
read above the portal. Navigation-shaped updates (a setter, then async computeds holding previous values)
|
|
353
359
|
do not need core `action` — reach for it only when writes happen *after* async work.
|
|
354
360
|
Invoke actions from handlers, not from memos or effects.
|
|
@@ -437,9 +443,11 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
437
443
|
experimental `serverFunctions.components` flag. Server-function arguments are JSON:
|
|
438
444
|
send `Date` / `Map` / `Set` as ISO strings / arrays (they throw otherwise; one `File` /
|
|
439
445
|
`Blob` / `FormData` argument travels natively). `enableRichArguments()` from
|
|
440
|
-
`@solidjs/web/server-functions/rich-args` (at `src/App.tsx` module scope) lifts that
|
|
441
|
-
|
|
442
|
-
|
|
446
|
+
`@solidjs/web/server-functions/rich-args` (at `src/App.tsx` module scope) lifts that
|
|
447
|
+
(its import builds cleanly since 2.0.0-rc.10 — a `resolve.dedupe: ["@solidjs/web"]`
|
|
448
|
+
workaround left from rc.9 is no longer needed) <!-- upstream:rich-args-vite-build https://github.com/solidjs/solid/issues/3627 -->. Declare live reads as `live(GET(fn))` —
|
|
449
|
+
`live()` outermost; an undeclared async iterable still producing while a document
|
|
450
|
+
renders is `[SSR_UNDECLARED_LIVE_SOURCE]` in dev. A plain `async function*` server function is an event stream on
|
|
443
451
|
one connection (no reconnect when it drops); `live()` is one value that changes over
|
|
444
452
|
time and reconnects. `live()` connection state is
|
|
445
453
|
`source.onstatus` (`"connected"` / `"reconnecting"` / `"closed"`), never a
|
|
@@ -74,7 +74,7 @@ Most React `useEffect` code should NOT become `createEffect`:
|
|
|
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
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
|
-
| Browser-only *value* (`localStorage`, viewport size) | `createMemo(..., { ssrSource: "client" })` — per-value policy at the data source, no component split (`"hybrid"`
|
|
77
|
+
| Browser-only *value* (`localStorage`, viewport size) | `createMemo(..., { ssrSource: "client" })` — per-value policy at the data source, no component split (`"hybrid"` only to keep consuming an async-iterable source after hydration <!-- upstream:ssr-source-hybrid-stream -->) |
|
|
78
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
|
|
|
@@ -256,7 +256,11 @@ const Result = dynamic(() => (detailed() ? Detailed : Compact));
|
|
|
256
256
|
```
|
|
257
257
|
|
|
258
258
|
`dynamic()` returns a **stable** component whose source can be a component, an
|
|
259
|
-
intrinsic tag name, a promise, or empty.
|
|
259
|
+
intrinsic tag name, a promise, or empty. An async source (a promise, or a `live()`
|
|
260
|
+
server-component iterable) may resolve to a server component or a tag name; a promise
|
|
261
|
+
of a *client* component function is a dev error on the server
|
|
262
|
+
(`[DYNAMIC_ASYNC_COMPONENT]`) — resolve the async upstream, or `lazy()` for code.
|
|
263
|
+
Prefer it over swapping a component
|
|
260
264
|
variable in JSX (`const View = tab() ? A : B` freezes the choice at setup).
|
|
261
265
|
`<Dynamic component={...}>` is the same primitive with a worse shape (the tag travels
|
|
262
266
|
in the props bag, so every instance merges and omits it and rebuilds a factory) and
|
|
@@ -399,8 +403,12 @@ return <StoryDetail story={story} />;
|
|
|
399
403
|
Lifting the fetch does **not** mean lifting the boundary — that is consume glued
|
|
400
404
|
back onto block. After first paint the
|
|
401
405
|
boundary keeps settled content during refetch; `isPending(results)` (pass the accessor,
|
|
402
|
-
not `isPending(results())`) is the indicator.
|
|
403
|
-
accessor `on={query}`) only when that
|
|
406
|
+
not `isPending(results())`) is the indicator. Add `on={query()}` (a tracked **read** —
|
|
407
|
+
the accessor `on={query}` reads nothing) only when that change of subject should put the
|
|
408
|
+
fallback back on screen. `on` is a dependency list, not a compared key: its value is
|
|
409
|
+
irrelevant, several inputs go in as `on={[query(), page()]}`, and a change to anything it
|
|
410
|
+
reads re-arms the boundary. <!-- upstream:loading-on-dependency-list --> `[LOADING_ON_OUTSIDE_HOLD]` in dev means the same data is
|
|
411
|
+
also read outside the boundary, so the fallback can never show — move that read under it.
|
|
404
412
|
Do not use `isPending` as the first-load spinner.
|
|
405
413
|
- **Nesting is not a waterfall.** `Comments` above fetches in parallel with `story`
|
|
406
414
|
because it reads `props.storyId` (already known), not `props.story`. Passing
|
|
@@ -426,7 +434,8 @@ return <StoryDetail story={story} />;
|
|
|
426
434
|
the same rule: bind `value={latest(query)}` so a controlled search box reflects
|
|
427
435
|
keystrokes while its write is held (an uncontrolled input needs nothing).
|
|
428
436
|
- Read every reactive input **before the first `await`** in an async computation. A read
|
|
429
|
-
after `await` does not subscribe
|
|
437
|
+
after `await` does not subscribe (dev reports `[UNTRACKED_READ_AFTER_AWAIT]` on V8 —
|
|
438
|
+
Chromium, Node, Deno, Bun); production can sit pending with no retry.
|
|
430
439
|
- `<Errored>` function fallbacks receive an error **accessor** and a `reset` callback:
|
|
431
440
|
`fallback={(error, reset) => ...}`. The boundary **heals** when the source succeeds
|
|
432
441
|
again (refresh, live reconnect, input change) — it is graph status, not a terminal
|
|
@@ -988,8 +997,8 @@ function DraftPreview(props: { documentId: string }) {
|
|
|
988
997
|
| Policy | Meaning | Use when |
|
|
989
998
|
|---|---|---|
|
|
990
999
|
| `"server"` (default) | Client adopts the serialized server value; compute does not re-run | Compute is deterministic from server-available inputs |
|
|
991
|
-
| `"hybrid"` | Client adopts the serialized value, then
|
|
992
|
-
| `"client"` | Server value skipped; compute runs after hydration as if first-mounted | Serialization is meaningless (`localStorage`, viewport) |
|
|
1000
|
+
| `"hybrid"` | Client adopts the serialized value, then — for an async-iterable source — continues the stream on the client; for a sync or promise compute it is identical to `"server"` (no re-run) <!-- upstream:ssr-source-hybrid-stream --> | A stream (`live()`, a subscription) the client should keep consuming after hydration |
|
|
1001
|
+
| `"client"` | Server value skipped; compute runs after hydration as if first-mounted | Serialization is meaningless (`localStorage`, viewport), or server data mixes with a browser-only input |
|
|
993
1002
|
|
|
994
1003
|
With `"client"`, the bare form suspends on the server and the nearest `<Loading>`
|
|
995
1004
|
fallback renders into the HTML; a declared first value (`loadingValue` on
|
|
@@ -1068,19 +1077,24 @@ see [Production observability](#production-observability-the-observe-build) —
|
|
|
1068
1077
|
dev setting. Diagnostics come in two layers: **findings** (facts about a render, present
|
|
1069
1078
|
in observe and dev) and **checks** (dev-only guidance printed by the console reporter).
|
|
1070
1079
|
Always-on dev checks include misplaced reads
|
|
1071
|
-
(`[STRICT_READ_UNTRACKED]`, `[PENDING_ASYNC_UNTRACKED_READ]`
|
|
1080
|
+
(`[STRICT_READ_UNTRACKED]`, `[PENDING_ASYNC_UNTRACKED_READ]`, `[UNTRACKED_READ_AFTER_AWAIT]`
|
|
1081
|
+
for a first read made after an `await` in an async compute — V8 engines only), misplaced writes
|
|
1072
1082
|
(`[REACTIVE_WRITE_IN_OWNED_SCOPE]` — since rc.9 also for store setters called in a
|
|
1073
1083
|
component or root body <!-- upstream:store-write-owned-scope -->, `[ASYNC_STORE_SETTER]` for a store setter callback that returns
|
|
1074
1084
|
a promise <!-- upstream:async-store-setter-throws -->, `[FLUSH_IN_ACTION]`, `[SERVER_WRITE]`), and
|
|
1075
|
-
`[ASYNC_OUTSIDE_LOADING_BOUNDARY]` when a tracked read has no boundary above it
|
|
1085
|
+
`[ASYNC_OUTSIDE_LOADING_BOUNDARY]` when a tracked read has no boundary above it, and
|
|
1086
|
+
`[LOADING_ON_OUTSIDE_HOLD]` when a `<Loading on>` boundary's data is also read outside it.
|
|
1076
1087
|
Each entry carries a stable `[CODE]`, what the runtime observed, and an owner chain
|
|
1077
1088
|
(`in <App> › <Cart> › <LineItem>`); never silence a code before understanding it.
|
|
1078
1089
|
Since rc.9 the server render reports on the same channel with the same owner chains:
|
|
1079
1090
|
`[SSR_RENDER_ERROR_CONTAINED]` (a boundary routed a render error; `ownerPath` is where
|
|
1080
|
-
it was thrown, `boundaryPath` where it was met) <!-- upstream:ssr-render-error-contained -->, `[
|
|
1081
|
-
behind a production-sanitized error
|
|
1082
|
-
that surfaced only after a real
|
|
1083
|
-
`[
|
|
1091
|
+
it was thrown, `boundaryPath` where it was met) <!-- upstream:ssr-render-error-contained -->, `[SERVER_ERROR_SANITIZED]` (the original
|
|
1092
|
+
behind a production-sanitized error, from a server function or an SSR render),
|
|
1093
|
+
`[SSR_CLIENT_CONTENT_MASKED]` (client-only content that surfaced only after a real
|
|
1094
|
+
server wait — the wait was wasted), `[SSR_BOUNDARY_WATERFALL]` (sequential waits inside
|
|
1095
|
+
one server `<Loading>` boundary), `[SSR_UNDECLARED_LIVE_SOURCE]` (an async iterable
|
|
1096
|
+
still pumping into a document render — declare it `live()`), and
|
|
1097
|
+
`[REVEAL_IN_RENDER_TO_STRING]`.
|
|
1084
1098
|
For the repair behind any code, read the `reactivity-diagnostics` skill Solid ships
|
|
1085
1099
|
in the repo (`node_modules/solid-js/skills/reactivity-diagnostics/SKILL.md`) — every
|
|
1086
1100
|
code maps to what the runtime observed and what to change.
|
|
@@ -1091,13 +1105,18 @@ every scope that re-ran, what changed to cause it, and how long it took. Since
|
|
|
1091
1105
|
no longer exists <!-- upstream:dev-attribution-removed --> (it is a type error; `DEV` holds only devtools hooks, graph
|
|
1092
1106
|
traversal, and console reporting). `OBSERVE` is the public observability surface on
|
|
1093
1107
|
the observe and dev builds — `OBSERVE.records`, `OBSERVE.diagnostics`,
|
|
1094
|
-
`OBSERVE.exclude`, `OBSERVE.server.trace` are
|
|
1095
|
-
`OBSERVE.attribution`
|
|
1096
|
-
to engines, routers, and devtools. Since 2.0.0-rc.9 the folds,
|
|
1097
|
-
formatters are **named exports** of that subpath, not methods of
|
|
1098
|
-
(`attribution.why` / `.costs` / `.feedback` / `.subscriptions` / `.format`
|
|
1099
|
-
errors) <!-- upstream:attribution-named-exports -->; `attribution` keeps `enable
|
|
1100
|
-
|
|
1108
|
+
`OBSERVE.exclude` / `OBSERVE.include`, `OBSERVE.ownerPath`, `OBSERVE.server.trace` are
|
|
1109
|
+
for app and tool code; only the `OBSERVE.attribution` slot (`withInteraction` /
|
|
1110
|
+
`withOrigin`) belongs to engines, routers, and devtools. Since 2.0.0-rc.9 the folds,
|
|
1111
|
+
queries, and formatters are **named exports** of that subpath, not methods of
|
|
1112
|
+
`attribution` (`attribution.why` / `.costs` / `.feedback` / `.subscriptions` / `.format`
|
|
1113
|
+
are type errors) <!-- upstream:attribution-named-exports -->; `attribution` keeps `enable` (a hold on the one shared engine — it
|
|
1114
|
+
returns the release), `disable` (full teardown), `markFlight`, and one ring-buffer reader,
|
|
1115
|
+
`attribution.history(type)` (`"rerun"` / `"hold"` / `"waterfall"` / `"navigation"` /
|
|
1116
|
+
`"interaction"`). Live records — the engine's included — arrive on
|
|
1117
|
+
`OBSERVE.records.subscribe(type, (event, live) => …)`; `attribution.subscribe` and the
|
|
1118
|
+
`holds()` / `waterfalls()` / `navigations()` / `interactions()` methods are gone since
|
|
1119
|
+
2.0.0-rc.10. <!-- upstream:attribution-history-records -->
|
|
1101
1120
|
|
|
1102
1121
|
```ts
|
|
1103
1122
|
import { attribution, formatRerun, why } from 'solid-js/attribution';
|
|
@@ -1116,13 +1135,15 @@ to query the tables yourself. Name the scopes you intend to interrogate
|
|
|
1116
1135
|
(`createMemo(..., { name: "total" })`) — chains refer to nodes by name, and
|
|
1117
1136
|
anonymous nodes print as `computed` / `effect` / `signal`; component labels
|
|
1118
1137
|
(`<Checkout>`) are always present under `vite dev` and survive minification only with
|
|
1119
|
-
the compiler's `
|
|
1138
|
+
the compiler's `sourceNames` option (components, binding effects, primitives — the Vite
|
|
1139
|
+
plugin turns it on under `vite dev` and `observe: true`; `componentNames` was its rc.9 name
|
|
1140
|
+
and is rejected now) <!-- upstream:source-names-option -->. On the
|
|
1120
1141
|
**prod build** the import resolves to an inert same-surface twin whose `enable()` is a
|
|
1121
1142
|
no-op, so the import can stay in the code. On the **observe build** it is the real
|
|
1122
1143
|
engine and an unguarded `enable()` records in production — keep the `isDev` guard
|
|
1123
1144
|
unless recording in production is the intent. The split is pay-for-use: importing
|
|
1124
1145
|
`costs` or `feedback` is what turns their tables on, so a records-only consumer
|
|
1125
|
-
(`
|
|
1146
|
+
(`OBSERVE.records.subscribe("rerun", …)`) ships neither.
|
|
1126
1147
|
|
|
1127
1148
|
While attribution is enabled, Solid also warns on its own about over-subscription,
|
|
1128
1149
|
chained async waterfalls (`[ASYNC_WATERFALL]`), memos whose fresh-but-equal output
|
|
@@ -1136,10 +1157,18 @@ ran while held; info from 100ms, warning from 200ms, tunable via
|
|
|
1136
1157
|
`enable({ holds: { infoMs, warnMs } })`). A hold that *was* acknowledged but still ran
|
|
1137
1158
|
long is `[LONG_HOLD]` (info from 500ms, warning from 1000ms;
|
|
1138
1159
|
`enable({ longHolds: { infoMs, warnMs } })`) — the suggestion is a `<Loading on={...}>`
|
|
1139
|
-
boundary, never removing the hold.
|
|
1140
|
-
`
|
|
1141
|
-
`
|
|
1142
|
-
|
|
1160
|
+
boundary, never removing the hold. Beside them: `[UNTRACKED_ASYNC_HANDLER]` (an `async`
|
|
1161
|
+
handler that awaits with no write before the `await` and no `action` — no hold exists, so
|
|
1162
|
+
the click looks dead; make it an `action`, or write a `createOptimistic(false)` flag
|
|
1163
|
+
first), `[ABANDONED_FLIGHTS]` (a source re-asked on every keystroke, answers thrown away
|
|
1164
|
+
— debounce at the handler), `[STACKED_HOLDS]` (repeated clicks piling into one hold),
|
|
1165
|
+
`[FALLBACK_FLASH]` (a fallback shown under 150ms), `[WASTED_RECOMPUTE]` (a scope whose
|
|
1166
|
+
runs mostly compared equal — narrow its reads upstream), `[GRAPH_GROWTH]` (the graph grows
|
|
1167
|
+
on every visit to a route — an undisposed root or subscription), and `[OPTIMISTIC_REVERTED]`
|
|
1168
|
+
(info: a guess the screen showed was replaced). `costs()` and `feedback()` (named imports)
|
|
1169
|
+
rank the session worst-first; `attribution.history("hold")` / `("waterfall")` /
|
|
1170
|
+
`("navigation")` / `("interaction")` hold the fact tables behind the verdicts (each hold
|
|
1171
|
+
carries its `silent` / `long` verdict). <!-- upstream:attribution-history-records -->
|
|
1143
1172
|
`subscriptions(scope)` (named import) lists a scope's current dependencies to compare
|
|
1144
1173
|
against what it uses.
|
|
1145
1174
|
|
|
@@ -1179,7 +1208,8 @@ effects** — a pure markup or styling change does not need it.
|
|
|
1179
1208
|
2. **Name the scopes you changed** (`createMemo(fn, { name: "total" })`,
|
|
1180
1209
|
`createStore(v, { name: "cart" })`) so chains and tables refer to them by name.
|
|
1181
1210
|
3. **Record the interaction and ask.** First choice — no app code — is the diagnostics
|
|
1182
|
-
endpoint: with `@solidjs/diagnostics` in `devDependencies
|
|
1211
|
+
endpoint: with `@solidjs/diagnostics` in `devDependencies` at the same version as
|
|
1212
|
+
`solid-js` (npm `latest` can lag; `solid2-kit doctor` flags an older one), the Vite plugin's
|
|
1183
1213
|
`diagnostics` option (auto-on when the package is declared; dev server only, never
|
|
1184
1214
|
under vitest, `vite build`, or preview) injects the capture bridge and serves
|
|
1185
1215
|
`/__solid/diagnostics`. `begin` enables attribution with `log: false` for the
|
|
@@ -1201,11 +1231,14 @@ effects** — a pure markup or styling change does not need it.
|
|
|
1201
1231
|
`attribution.enable({ log: false })` in the client entry, then `why(scope)` /
|
|
1202
1232
|
`costs()` / `feedback()` from the console or a scratch effect — never `log: true`
|
|
1203
1233
|
left on (one collapsed `[why-run]` group per re-run) and never unguarded (on the
|
|
1204
|
-
observe build it records in production).
|
|
1234
|
+
observe build it records in production). Under `vite dev` the plugin also paints the
|
|
1235
|
+
same records as tracks in the Chrome Performance panel (`performanceTracks`, on by
|
|
1236
|
+
default; `false` opts out) — record there when a timeline reads better than tables.
|
|
1205
1237
|
4. **Read the verdicts, then the tables.** Fix every coded warning the session raised
|
|
1206
1238
|
(`[SILENT_HOLD]`, `[IMMUTABLE_UPDATE_IN_STORE]`, `[UNSTABLE_LIST_IDENTITY]`,
|
|
1207
1239
|
`[EFFECT_RELAY_TEAR]`, `[EFFECT_WRITES_OWN_SOURCE]`, `[ASYNC_WATERFALL]`,
|
|
1208
|
-
`[HOT_SCOPE_RERUNS]
|
|
1240
|
+
`[HOT_SCOPE_RERUNS]`, `[WASTED_RECOMPUTE]`, `[ABANDONED_FLIGHTS]`,
|
|
1241
|
+
`[UNTRACKED_ASYNC_HANDLER]`). Then `costs().scopes` (self time, `wastedMs` for runs whose
|
|
1209
1242
|
result did not change), `costs().writes` (root writes by downstream work),
|
|
1210
1243
|
`feedback().sources` / `.interactions` / `.flights` / `.fallbacks` (silent holds,
|
|
1211
1244
|
held time per interaction, abandoned requests, fallback flashes). A scope that
|
|
@@ -1232,14 +1265,16 @@ export default { plugins: [solid({ ssr: true, observe: true })] };
|
|
|
1232
1265
|
```
|
|
1233
1266
|
|
|
1234
1267
|
`observe: true` adds the `observe` condition to every environment (client and server,
|
|
1235
|
-
`vite build` and preview) and turns on the compiler's `
|
|
1236
|
-
`ownerPath` / `boundaryPath` name components instead
|
|
1237
|
-
set `resolve.conditions` (or `node --conditions=observe`
|
|
1238
|
-
`
|
|
1268
|
+
`vite build` and preview) and turns on the compiler's `sourceNames` option (components,
|
|
1269
|
+
binding effects, and primitives) so `ownerPath` / `boundaryPath` name components instead
|
|
1270
|
+
of `computed` <!-- upstream:source-names-option -->. Without the plugin, set `resolve.conditions` (or `node --conditions=observe`
|
|
1271
|
+
for an unbundled server) and `sourceNames` yourself. Cost in Solid's size suite: ~16.8 KB against ~15.3 KB brotli
|
|
1239
1272
|
for a small client app; importing the attribution engine on top raises the cap to
|
|
1240
1273
|
~27 KB (a separate entry — an observe build that never imports it never ships it). The
|
|
1241
1274
|
two error hooks are **not** part of this: they fire on every build, prod included, only
|
|
1242
|
-
the paths are `undefined` there.
|
|
1275
|
+
the paths are `undefined` there. `enablePerformanceTracks()` from
|
|
1276
|
+
`@solidjs/web/performance-tracks` paints the records on the Chrome Performance panel in
|
|
1277
|
+
production too (observe build; a no-op on the prod build).
|
|
1243
1278
|
|
|
1244
1279
|
- **Instrumentation order.** A monitoring SDK's `init()` (and `configureServerErrors`)
|
|
1245
1280
|
must run before the modules it patches load. `import "./instrument"` at the top of an
|
|
@@ -1256,22 +1291,31 @@ the paths are `undefined` there.
|
|
|
1256
1291
|
`OBSERVE?.server.trace.provide((request) => ({ traceId, spanId, entries }))`, once per
|
|
1257
1292
|
request. Do not write middleware to inject trace headers.
|
|
1258
1293
|
- **Records.** `OBSERVE?.records.subscribe(type, (event, live) => …)` delivers one plain
|
|
1259
|
-
record per `"
|
|
1260
|
-
|
|
1261
|
-
`
|
|
1294
|
+
record per `"render"` (a server render: `mode`, `shellMs`, `durationMs`, `boundaries`,
|
|
1295
|
+
`outcome`, the matched `route`), `"boundary"` (a `<Loading>` that waited on the server),
|
|
1296
|
+
`"invocation"` (server-function execution), `"request"` / `"call"` (a server-function
|
|
1297
|
+
call from the page, at send and at settle — `call` shares `id` with its invocation),
|
|
1298
|
+
`"frame"` (produced / applied), and `"recovery"` (a boundary the client re-rendered
|
|
1299
|
+
after a server failure); the attribution engine's own records (`"rerun"`, `"hold"`,
|
|
1300
|
+
`"interaction"`, `"navigation"`, …) ride the same channel while it is enabled. Records are
|
|
1262
1301
|
serializable data; live handles (request, args, the error as thrown) ride in the
|
|
1263
1302
|
second argument. Listeners run synchronously inside the runtime, **must not write
|
|
1264
1303
|
signals**, and should subscribe from a module that loads before the app.
|
|
1265
|
-
- **Interactions.** `attribution.enable()` then
|
|
1266
|
-
`
|
|
1304
|
+
- **Interactions.** `attribution.enable()` (keep the release it returns) then
|
|
1305
|
+
`OBSERVE?.records.subscribe("interaction", (event) => …)` <!-- upstream:attribution-history-records --> says what each click waited on
|
|
1267
1306
|
(`event.settledMs`, `event.holds[].blockers`, `event.holds[].holdMs`); a hold nothing acknowledged
|
|
1268
1307
|
is the dead click. The fold tables stay separate imports.
|
|
1269
1308
|
- **What leaves the process.** Beyond names, four record fields carry page data —
|
|
1270
1309
|
`target` (element as `tag#id "text"`, 30 chars), `prev` / `value` previews (40 chars),
|
|
1271
|
-
navigation `to` / `from` / `params`, and `data.error` on server render findings
|
|
1272
|
-
|
|
1310
|
+
navigation `to` / `from` / `params`, and `data.error` on server render findings. The
|
|
1311
|
+
engine's `values` option governs the first three at the source — `"full"` (the dev
|
|
1312
|
+
default), `"labels"` (no previews; element text only on buttons and links), `"none"`
|
|
1313
|
+
(the observe-build default) — so an exporter on the observe build passes its level
|
|
1314
|
+
explicitly (`attribution.enable({ values: "labels" })`) and decides what to do with the
|
|
1315
|
+
rest. A tool that renders inside the app it watches
|
|
1273
1316
|
(devtools panel) marks its root with `OBSERVE?.exclude(getOwner()!)` inside its
|
|
1274
|
-
`createRoot` so its own effects and stores never become findings
|
|
1317
|
+
`createRoot` so its own effects and stores never become findings (`OBSERVE?.include(owner)`
|
|
1318
|
+
re-admits the app's root under an excluded shell); do not route its
|
|
1275
1319
|
writes through `runWithOwner` (that is a write in an owned scope).
|
|
1276
1320
|
|
|
1277
1321
|
## Client mode: no server HTML, no hydration reflexes
|
|
@@ -1641,10 +1685,10 @@ status, so `return respond({ error }, { status: 400 })` reads as success to the
|
|
|
1641
1685
|
is success metadata a scripted caller unwraps. Arguments are JSON: send `Date` / `Map` /
|
|
1642
1686
|
`Set` as ISO strings / arrays (they throw otherwise; one `File` / `Blob` / `FormData`
|
|
1643
1687
|
argument travels natively). `enableRichArguments()` from
|
|
1644
|
-
`@solidjs/web/server-functions/rich-args` (at `src/App.tsx` module scope) lifts that
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1688
|
+
`@solidjs/web/server-functions/rich-args` (at `src/App.tsx` module scope) lifts that. Its
|
|
1689
|
+
import failed `vite build` in rc.9 (`"./client" is not exported`); that is fixed since
|
|
1690
|
+
2.0.0-rc.10, so a `resolve: { dedupe: ["@solidjs/web"] }` workaround left in a Vite config
|
|
1691
|
+
is no longer needed (the plugin dedupes the runtime packages itself under `vite dev`). <!-- upstream:rich-args-vite-build https://github.com/solidjs/solid/issues/3627 -->
|
|
1648
1692
|
`GET()` is only for idempotent reads (URLs leak into logs/history);
|
|
1649
1693
|
import it from `@solidjs/web/server-functions`, never `@solidjs/start`.
|
|
1650
1694
|
Reads are function calls too: `GET(async (id) => { "use server"; ... })` is invoked
|
|
@@ -1672,7 +1716,10 @@ export const fetchStory = GET(async (id: number) => {
|
|
|
1672
1716
|
A value that changes over time is `live(GET(fn))` — **`live()` outermost**: `GET()`
|
|
1673
1717
|
chooses the read transport and `live()` wraps the call behavior around it. `live()`
|
|
1674
1718
|
does not imply `GET()`; without it the source streams over POST, which is fine when a
|
|
1675
|
-
cacheable URL is of no use.
|
|
1719
|
+
cacheable URL is of no use. A server component rendered into a document that reads an
|
|
1720
|
+
*undeclared* async iterable still producing after 5s is `[SSR_UNDECLARED_LIVE_SOURCE]` in
|
|
1721
|
+
dev — declare it `live()` (the document takes the first value and closes it; the client
|
|
1722
|
+
connects after hydration) or bound the source.
|
|
1676
1723
|
|
|
1677
1724
|
```ts
|
|
1678
1725
|
// src/data/auction.ts
|
|
@@ -1805,7 +1852,7 @@ how to reuse. Prefer the form on the right.
|
|
|
1805
1852
|
| `createEffect(() => user, (u) => { log(u.name); })` | `createEffect(() => user.name, (name) => { log(name); })` |
|
|
1806
1853
|
| `createEffect(() => title(), (t) => (document.title = t))` / `(v) => setX(v)` | `(t) => { document.title = t; }` — apply returns a cleanup or nothing; any other return value halts reactivity. An apply that only calls a local setter is rule 4 even with braces (`solid2-kit check` flags it): use a (writable) derivation |
|
|
1807
1854
|
| `<Loading fallback={<PageSkeleton />}>{/* header + data */}</Loading>` | wrap only the data slot; chrome stays outside |
|
|
1808
|
-
| `<Loading on={id} fallback={...}>` (the accessor) | `on={id()}` — a
|
|
1855
|
+
| `<Loading on={id} fallback={...}>` (the accessor), or `` on={`${a()}/${b()}`} `` to key several inputs | `on={id()}` / `on={[a(), b()]}` — `on` is a tracked dependency list, not a compared key: a bare accessor is never read (`solid2-kit check` flags it), and an array is the several-inputs form <!-- upstream:loading-on-dependency-list --> |
|
|
1809
1856
|
| `setCount(count() + 1)` when writes can batch | `setCount((c) => c + 1)` |
|
|
1810
1857
|
| `setHandler(fn)` to store a function | `setHandler(() => fn)` (otherwise `fn` is an updater) |
|
|
1811
1858
|
| `{user() ? <P user={user()!} /> : <SignIn />}` | `<Show when={user()}>{(u) => <P user={u()} />}</Show>` |
|
|
@@ -1837,7 +1884,7 @@ how to reuse. Prefer the form on the right.
|
|
|
1837
1884
|
| `renderToString(() => <App />)` for an async or lazy-route tree | `await renderToStream(() => <App />)` (one consumer: `pipe` / `pipeTo` / `readable`). String render emits `<Loading>` fallbacks |
|
|
1838
1885
|
| `clientOnly(() => import("./Map"))` for a rarely shown widget | same + `{ lazy: true }` so the import waits until first render (default starts at declaration) |
|
|
1839
1886
|
| `<NoHydration><Chart /></NoHydration>` to skip SSR of a widget | `clientOnly(() => import("./Chart"))` — `NoHydration`/`Hydration` split ownership, they do not choose visible content |
|
|
1840
|
-
| `clientOnly` split (or `isServer` branch) for one browser-only *value* | `createMemo(() => localStorage.getItem(k), { ssrSource: "client" })` — `"hybrid"`
|
|
1887
|
+
| `clientOnly` split (or `isServer` branch) for one browser-only *value* | `createMemo(() => localStorage.getItem(k), { ssrSource: "client" })` — `"hybrid"` only to keep consuming an async-iterable source after hydration <!-- upstream:ssr-source-hybrid-stream --> |
|
|
1841
1888
|
| `await flush()` / `flush()` to wait on a pending memo | `await resolve(() => value())` (or Testing Library async queries); `flush()` only drains staged *sync* writes |
|
|
1842
1889
|
| `action(function* () { setX(v); })` around a navigation-shaped setter | plain setter; async computeds hold previous values. Core `action` only when writes happen *after* async work |
|
|
1843
1890
|
| `export const [cart, setCart] = createStore(...)` for app-wide state | `createCart()` called by a `CartProvider` at the root of `App`, read with `useCart()` over `createContext<T>()` — module state has no owner and, under SSR, one instance serves every request (`[SERVER_WRITE]`). Module scope is for constants and `createContext` |
|
|
@@ -1900,7 +1947,7 @@ how to reuse. Prefer the form on the right.
|
|
|
1900
1947
|
| `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
1948
|
| `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 --> |
|
|
1902
1949
|
| `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 |
|
|
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 + `
|
|
1950
|
+
| `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 + `sourceNames` for `vite build`/preview <!-- upstream:source-names-option -->) |
|
|
1904
1951
|
| `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 |
|
|
1905
1952
|
| `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 |
|
|
1906
1953
|
| 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` |
|
|
@@ -2111,7 +2158,7 @@ always-applied rules installed alongside this skill.
|
|
|
2111
2158
|
memo/effect. Effect `apply` functions have block bodies and return only a
|
|
2112
2159
|
cleanup or nothing.
|
|
2113
2160
|
- [ ] Dev console shows no diagnostics: reads in tracking scopes (no
|
|
2114
|
-
`[STRICT_READ_UNTRACKED]`), store setters synchronous and never called at
|
|
2161
|
+
`[STRICT_READ_UNTRACKED]`, no `[UNTRACKED_READ_AFTER_AWAIT]`), store setters synchronous and never called at
|
|
2115
2162
|
body level (no `[ASYNC_STORE_SETTER]` <!-- upstream:async-store-setter-throws --> /
|
|
2116
2163
|
`[REACTIVE_WRITE_IN_OWNED_SCOPE]` <!-- upstream:store-write-owned-scope -->), held
|
|
2117
2164
|
writes paired with `isPending` / `latest` / optimistic values / `affects()`
|
|
@@ -26,6 +26,7 @@ The skill does not cover these; fetch the pages when the task needs them.
|
|
|
26
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
27
|
- Infinite scroll, polling, sharing one request: https://v2-rebuild--solid-docs-v2.netlify.app/guides/data-fetching-patterns.md
|
|
28
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
|
+
- Library code that must know whether it is hydrating, or write a keyed server-to-client value (`isHydrating`, `isHydratable`, `getHydrationWriter` / `takeHydrationValue`): https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/advanced/manual-hydration/is-hydrating.md, https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/advanced/manual-hydration/is-hydratable.md, https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-web/rendering-ssr/get-hydration-writer.md, https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-web/rendering-ssr/take-hydration-value.md
|
|
29
30
|
- 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
31
|
- 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
32
|
- 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
|
|
@@ -158,6 +159,7 @@ the error hooks fire on every build.
|
|
|
158
159
|
- https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/advanced/diagnostics-dev-hooks/configure-client-errors.md
|
|
159
160
|
- https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-web/request-response/configure-server-errors.md
|
|
160
161
|
- https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-web/request-response/get-trace-context.md
|
|
162
|
+
- https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-web/performance-tracks/enable-performance-tracks.md
|
|
161
163
|
|
|
162
164
|
## Reference: components and JSX (solid-js)
|
|
163
165
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "solid2-agent-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.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",
|