solid2-agent-kit 0.18.0 → 0.20.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/bin/baseline.mjs +15 -6
- package/bin/solid2-kit.mjs +68 -3
- package/files/commands/solid2-review.md +9 -5
- package/files/shared/agents-section.md +1 -1
- package/files/shared/rules-body.md +54 -23
- package/files/skills/solid-2/SKILL.md +156 -65
- package/files/skills/solid-2/references/official-docs.md +2 -0
- package/package.json +1 -1
package/bin/baseline.mjs
CHANGED
|
@@ -10,13 +10,22 @@
|
|
|
10
10
|
// and @solidjs/diagnostics `latest` is an older 2.0 rc than `next`;
|
|
11
11
|
// @solidjs/vite-plugin's `next` is behind `latest`), so a bare install can
|
|
12
12
|
// resolve a release older than the baseline.
|
|
13
|
+
//
|
|
14
|
+
// The JSX compilers (@solidjs/compiler, native and the default;
|
|
15
|
+
// @solidjs/babel-plugin under `compiler: "babel"`) are listed because compiled
|
|
16
|
+
// output carries behavior the guidance states (lowercase `on*` is a plain
|
|
17
|
+
// attribute since rc.14). Projects rarely declare them — @solidjs/vite-plugin
|
|
18
|
+
// pulls them in — so doctor also checks an undeclared install of any package
|
|
19
|
+
// here, which is where a lockfile holds an older compiler.
|
|
13
20
|
export const BASELINE = {
|
|
14
|
-
'solid-js': { line: 2, baseline: '2.0.0-rc.
|
|
15
|
-
'@solidjs/web': { line: 2, baseline: '2.0.0-rc.
|
|
16
|
-
'@solidjs/signals': { line: 2, baseline: '2.0.0-rc.
|
|
17
|
-
'@solidjs/diagnostics': { line: 2, baseline: '2.0.0-rc.
|
|
18
|
-
'@solidjs/
|
|
19
|
-
'@solidjs/
|
|
21
|
+
'solid-js': { line: 2, baseline: '2.0.0-rc.14' },
|
|
22
|
+
'@solidjs/web': { line: 2, baseline: '2.0.0-rc.14' },
|
|
23
|
+
'@solidjs/signals': { line: 2, baseline: '2.0.0-rc.14' },
|
|
24
|
+
'@solidjs/diagnostics': { line: 2, baseline: '2.0.0-rc.14' },
|
|
25
|
+
'@solidjs/compiler': { line: 2, baseline: '2.0.0-rc.14' },
|
|
26
|
+
'@solidjs/babel-plugin': { line: 2, baseline: '2.0.0-rc.14' },
|
|
27
|
+
'@solidjs/router': { line: 2, baseline: '2.0.0-next.37' },
|
|
28
|
+
'@solidjs/vite-plugin': { line: 3, baseline: '3.0.0-next.47' },
|
|
20
29
|
'@solidjs/testing-library': { line: 1, baseline: '1.0.0-beta.3' },
|
|
21
30
|
};
|
|
22
31
|
|
package/bin/solid2-kit.mjs
CHANGED
|
@@ -480,6 +480,29 @@ function reactKeyPropFindings(content) {
|
|
|
480
480
|
// reads it from package.json / node_modules.
|
|
481
481
|
const TANSTACK_ROUTER_MODULE = /^@tanstack\/(?:solid-router|router-core)$/;
|
|
482
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
|
+
|
|
483
506
|
// Local names bound by `import [Default,] { a, b as c } from "<module>"`.
|
|
484
507
|
function importedNames(content, moduleName) {
|
|
485
508
|
const names = new Set();
|
|
@@ -672,7 +695,7 @@ const CHECKS = [
|
|
|
672
695
|
pattern: /<(?:HashRouter|MemoryRouter|Route|Navigate|A|FileRoutes|StartClient|StartServer)\b/g,
|
|
673
696
|
exemptImportsFrom: TANSTACK_ROUTER_MODULE,
|
|
674
697
|
message:
|
|
675
|
-
'Solid Router 0.x/1.x or SolidStart JSX. Define routes with createRouter({ routes }) / fileRoutes(pageRoutes) and plain <a href={Router.paths...}> (link state: automatic aria-current/data-active
|
|
698
|
+
'Solid Router 0.x/1.x or SolidStart JSX. Define routes with createRouter({ routes }) / fileRoutes(pageRoutes) and plain <a href={Router.paths...}> (link state: automatic aria-current/data-active + CSS, data-pending via createRouter({ links: pendingLinks }), or useLinkState).',
|
|
676
699
|
},
|
|
677
700
|
{
|
|
678
701
|
id: 'meta-provider',
|
|
@@ -727,6 +750,23 @@ const CHECKS = [
|
|
|
727
750
|
message:
|
|
728
751
|
'React DOM prop. Native dblclick is onDblClick; keypress is onKeyDown; Solid has no suppressHydrationWarning.',
|
|
729
752
|
},
|
|
753
|
+
{
|
|
754
|
+
// Only `on` + uppercase binds an event: since rc.14 a lowercase `on*`
|
|
755
|
+
// attribute is a plain attribute (dev warns [LOWERCASE_EVENT_ATTRIBUTE]),
|
|
756
|
+
// and a 1.x `on:click` is a plain namespaced attribute — neither runs the
|
|
757
|
+
// handler. A real event name in lowercase is always a leftover. Anchored
|
|
758
|
+
// on `={` (a JSX expression attribute): `const onmessage = ...`,
|
|
759
|
+
// `ws.onerror = fn`, and HTML strings (`onerror="…"`) are not matches,
|
|
760
|
+
// and neither is a string expression (`onclick={"track()"}`) — a string
|
|
761
|
+
// inline-handler attribute is the one legitimate lowercase use. The names
|
|
762
|
+
// are the GlobalEventHandlers / WindowEventHandlers set, so a custom
|
|
763
|
+
// attribute that merely starts with `on` (`one={…}`) is never flagged.
|
|
764
|
+
id: 'solid1-lowercase-event',
|
|
765
|
+
pattern:
|
|
766
|
+
/(?<=\s)on(?:abort|afterprint|animation(?:cancel|end|iteration|start)|auxclick|beforeinput|beforematch|beforeprint|beforetoggle|beforeunload|blur|cancel|canplay|canplaythrough|change|click|close|command|contextlost|contextmenu|contextrestored|copy|cuechange|cut|dblclick|drag|dragend|dragenter|dragleave|dragover|dragstart|drop|durationchange|emptied|ended|error|focus|focusin|focusout|formdata|fullscreenchange|fullscreenerror|gotpointercapture|hashchange|input|invalid|keydown|keypress|keyup|languagechange|load|loadeddata|loadedmetadata|loadstart|lostpointercapture|message|messageerror|mousedown|mouseenter|mouseleave|mousemove|mouseout|mouseover|mouseup|offline|online|pagehide|pagereveal|pageshow|pageswap|paste|pause|play|playing|pointercancel|pointerdown|pointerenter|pointerleave|pointermove|pointerout|pointerover|pointerrawupdate|pointerup|popstate|progress|ratechange|rejectionhandled|reset|resize|scroll|scrollend|search|securitypolicyviolation|seeked|seeking|select|selectionchange|selectstart|slotchange|stalled|storage|submit|suspend|timeupdate|toggle|touchcancel|touchend|touchmove|touchstart|transitioncancel|transitionend|transitionrun|transitionstart|unhandledrejection|unload|volumechange|waiting|wheel)\s*=\s*\{(?!\s*["'`])/g,
|
|
767
|
+
message:
|
|
768
|
+
'Lowercase `on*` is a plain attribute in Solid 2 (dev warns [LOWERCASE_EVENT_ATTRIBUTE]), not an event binding. Use the camelCase prop (onClick).',
|
|
769
|
+
},
|
|
730
770
|
{
|
|
731
771
|
id: 'history-nav',
|
|
732
772
|
pattern: /\b(?:history\.pushState|history\.replaceState|window\.location\.href\s*=)/g,
|
|
@@ -749,6 +789,13 @@ const CHECKS = [
|
|
|
749
789
|
message:
|
|
750
790
|
'Pass the accessor to isPending/latest, not the call: isPending(user), not isPending(user()). Calling it first evaluates the read before the helper runs.',
|
|
751
791
|
},
|
|
792
|
+
{
|
|
793
|
+
// `<Loading on={id}>` — the accessor is never read, so `on` never re-arms.
|
|
794
|
+
id: 'loading-on-accessor',
|
|
795
|
+
find: loadingOnAccessorFindings,
|
|
796
|
+
message:
|
|
797
|
+
'`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()]}.',
|
|
798
|
+
},
|
|
752
799
|
{
|
|
753
800
|
id: 'dynamic-jsx',
|
|
754
801
|
pattern: /<Dynamic[\s/>]/g,
|
|
@@ -1263,7 +1310,7 @@ const SOLID2_LINE_PACKAGES = [
|
|
|
1263
1310
|
// Packages doctor checks the version of: the Solid 1.x lines above, then any
|
|
1264
1311
|
// release older than the kit's baseline (bin/baseline.mjs) — a bare
|
|
1265
1312
|
// `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.
|
|
1313
|
+
// @solidjs/vite-plugin's `@solidjs/web ^2.0.0-rc.13` peer rejects.
|
|
1267
1314
|
const VERSION_CHECKED = [...new Set([...SOLID2_LINE_PACKAGES.map((p) => p.name), ...Object.keys(BASELINE)])];
|
|
1268
1315
|
|
|
1269
1316
|
// The lower bound of a single-comparator range ("^2.0.0-rc.0", "~2.0.0-rc.8",
|
|
@@ -1285,6 +1332,19 @@ function versionProblem(name, version, declared) {
|
|
|
1285
1332
|
return null;
|
|
1286
1333
|
}
|
|
1287
1334
|
|
|
1335
|
+
// An undeclared package on the baseline's major that a dependency pulled in
|
|
1336
|
+
// (the JSX compilers under @solidjs/vite-plugin): only the floor applies — a
|
|
1337
|
+
// lockfile keeps the version it first resolved. Another major is someone
|
|
1338
|
+
// else's copy, not the one the kit's guidance describes.
|
|
1339
|
+
function transitiveProblem(name, installed) {
|
|
1340
|
+
const { line, baseline } = BASELINE[name];
|
|
1341
|
+
if (!LOWER_BOUND.test(installed) || compareVersions(installed, `${line}.0.0-0`) < 0 || compareVersions(installed, `${line + 1}.0.0-0`) >= 0) {
|
|
1342
|
+
return null;
|
|
1343
|
+
}
|
|
1344
|
+
if (compareVersions(installed, baseline) >= 0) return null;
|
|
1345
|
+
return `older than ${baseline}, the release this kit's guidance is verified against. It is not in package.json, so a dependency installed it and the lockfile holds the old version: update it (\`npm update ${name}\`, \`pnpm update ${name}\`, or reinstall without the lockfile entry).`;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1288
1348
|
function installedVersion(target, name) {
|
|
1289
1349
|
try {
|
|
1290
1350
|
return JSON.parse(readFileSync(join(target, 'node_modules', name, 'package.json'), 'utf8')).version;
|
|
@@ -1312,7 +1372,12 @@ function doctorFindings(target) {
|
|
|
1312
1372
|
for (const name of VERSION_CHECKED) {
|
|
1313
1373
|
const id = SOLID2_LINE_PACKAGES.find((p) => p.name === name)?.id ?? `${name.replace('@solidjs/', '')}-version`;
|
|
1314
1374
|
const range = deps[name];
|
|
1315
|
-
if (typeof range !== 'string')
|
|
1375
|
+
if (typeof range !== 'string') {
|
|
1376
|
+
const installed = BASELINE[name] && installedVersion(target, name);
|
|
1377
|
+
const transitive = installed && transitiveProblem(name, installed);
|
|
1378
|
+
if (transitive) report(id, `node_modules has ${name} ${installed} (not in package.json) — ${transitive}`);
|
|
1379
|
+
continue;
|
|
1380
|
+
}
|
|
1316
1381
|
const declared = versionProblem(name, range, true);
|
|
1317
1382
|
if (declared) {
|
|
1318
1383
|
report(id, `package.json pins ${name} "${range}" — ${declared}`);
|
|
@@ -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
|
|
@@ -66,15 +68,17 @@ Steps:
|
|
|
66
68
|
(`filter` for removal is fine — survivors keep identity; only fresh-tree wholesale
|
|
67
69
|
replacement needs `reconcile` / the derived form);
|
|
68
70
|
- hand-rolled link-active comparisons (`location.pathname === ...`) instead of the
|
|
69
|
-
automatic `aria-current` / `data-active`
|
|
71
|
+
automatic `aria-current` / `data-active` attributes (+ CSS) — `data-pending` is
|
|
72
|
+
opt-in via `createRouter({ links: pendingLinks })` — or
|
|
70
73
|
`useLinkState` / `useIsRouting`; hand-rolled query parsing instead of
|
|
71
|
-
`useSearchParams`;
|
|
74
|
+
`useSearchParams`; <!-- upstream:pending-links-opt-in -->
|
|
72
75
|
- search inputs bound to a held value without `latest`, effect-based debounce, fully
|
|
73
76
|
controlled forms without `name`s, `{ success: false }` result objects instead of
|
|
74
77
|
thrown failures, or router forms without server-side validation (`throw respond(...)`)
|
|
75
78
|
and `useSubmissions` inline errors;
|
|
76
79
|
- browser-only *values* handled with a `clientOnly` component split or an `isServer`
|
|
77
|
-
branch instead of `ssrSource: "client"`
|
|
80
|
+
branch instead of `ssrSource: "client"` on the memo/signal/derived store (`"hybrid"`
|
|
81
|
+
only continues an async-iterable source after hydration <!-- upstream:ssr-source-hybrid-stream -->);
|
|
78
82
|
- held writes with no feedback (`isPending` / `latest` / optimistic value / `affects()`)
|
|
79
83
|
— the `[SILENT_HOLD]` shape — and dev-console diagnostics (`[STRICT_READ_UNTRACKED]`,
|
|
80
84
|
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. Creating a memo/effect/root inside an effect's `apply` callback throws in dev too (`[PRIMITIVE_IN_EFFECT_CALLBACK]`) — create it in the body or compute phase. <!-- upstream:primitive-in-effect-callback --> 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 `aria-current`/`data-active` + CSS (`data-pending` is opt-in via `createRouter({ links: pendingLinks })`; `useLinkState`/`useIsRouting` in JSX) <!-- upstream:pending-links-opt-in -->; 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.
|
|
@@ -32,7 +32,11 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
32
32
|
Never write "a signal plus an effect that syncs it". That includes **reset**: to clear
|
|
33
33
|
a signal when another reactive value changes, use a writable derivation —
|
|
34
34
|
`createSignal(() => { source(); return initial; })` (re-runs when `source()` changes;
|
|
35
|
-
the setter still works) — never a `createEffect` whose apply calls the setter.
|
|
35
|
+
the setter still works) — never a `createEffect` whose apply calls the setter. A write
|
|
36
|
+
lands at once; on the next source change the function re-runs receiving the write as
|
|
37
|
+
`prev` (a store receives it as the draft), so a function that ignores `prev` resets on
|
|
38
|
+
every source change — including a write staged in the same update — while one that
|
|
39
|
+
honors `prev` can keep the local value. <!-- upstream:derived-write-prev --> The
|
|
36
40
|
derivation also moves the `source()` read to the read site, under its boundaries.
|
|
37
41
|
(React needs `useState(props.x)` plus a `previousX` comparison reset for the same
|
|
38
42
|
derive-override-reset behavior.)
|
|
@@ -59,6 +63,9 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
59
63
|
`event.currentTarget` (the element that owns the handler), not `event.target`
|
|
60
64
|
(a nested child). Never pass a setter as the handler (`onClick={setCount}`) —
|
|
61
65
|
that writes the event object; wrap it: `onClick={() => setCount((c) => c + 1)}`.
|
|
66
|
+
Only `on` + uppercase binds an event: lowercase `onclick` is a plain attribute
|
|
67
|
+
(dev warns `[LOWERCASE_EVENT_ATTRIBUTE]`), not a handler.
|
|
68
|
+
<!-- upstream:lowercase-event-attribute -->
|
|
62
69
|
Debounce at the handler (`onInput={debounce(...)}`), never as an effect copying
|
|
63
70
|
one signal into another.
|
|
64
71
|
7. **Lists use `<For>`**, never `{list().map(...)}` in reactive JSX and never `key` props (only `@solidjs/meta` tags take `key`, as the tag's identity).
|
|
@@ -125,14 +132,18 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
125
132
|
must `refresh()` that source — type that prop `SourceAccessor<User>` (`Accessor<User>`
|
|
126
133
|
is TS2345 at `refresh`) — `isPending(() => props.user)` works on the
|
|
127
134
|
value. Read every reactive input **before the first
|
|
128
|
-
`await`** — post-`await` reads do not subscribe
|
|
135
|
+
`await`** — post-`await` reads do not subscribe (dev reports `[UNTRACKED_READ_AFTER_AWAIT]`
|
|
136
|
+
on V8 engines), and in production the computation can
|
|
129
137
|
sit pending with no retry. No `useEffect` + `setState` fetching, no `createResource`.
|
|
130
138
|
`<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
|
-
|
|
139
|
+
during refetch (`isPending` for the indicator). Add `on={id()}` (a tracked *read*, not
|
|
140
|
+
the accessor `on={id}`) only when that change of subject should show the fallback again.
|
|
141
|
+
`on` is a dependency list, not a compared key: its value is irrelevant, a change to
|
|
142
|
+
anything it reads re-arms the boundary (several inputs: `on={[a(), b()]}`), and the
|
|
143
|
+
fallback lands with the frame of the change that caused it. <!-- upstream:loading-on-dependency-list -->
|
|
144
|
+
It can never show while the same data is also read outside the boundary (dev reports
|
|
145
|
+
`[LOADING_ON_OUTSIDE_HOLD]`) — move that read under the boundary so one hold owns the
|
|
146
|
+
data; that is how one slow panel stops holding the page. Do not start `fetch` (or any request) at component-body top
|
|
136
147
|
level — that runs once at mount and is not a reactive source. Do not `try/catch` `NotReadyError` around a read, and do not
|
|
137
148
|
use `loadingValue` / `seedLoadingValue` as the default first-flight UI — those skip
|
|
138
149
|
`<Loading>`. `{latest(() => x())}` is a preview, not the visible answer, and not a
|
|
@@ -196,7 +207,11 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
196
207
|
updated *synchronous* state or DOM. Waiting on an async memo is
|
|
197
208
|
`await resolve(() => value())` (or Testing Library async queries), not `flush()`.
|
|
198
209
|
Never call `flush()` inside an `action`, an `onSettled` callback (throws in dev), or an
|
|
199
|
-
effect callback (a no-op, `[FLUSH_IN_EFFECT_CALLBACK]`).
|
|
210
|
+
effect callback (a no-op, `[FLUSH_IN_EFFECT_CALLBACK]`). Never create a memo, effect,
|
|
211
|
+
or root inside an effect callback either — it throws in dev
|
|
212
|
+
(`[PRIMITIVE_IN_EFFECT_CALLBACK]`, `runWithOwner(null, …)` included); create it in the
|
|
213
|
+
body or compute phase, or under `runWithOwner(owner, …)` with an owner captured in
|
|
214
|
+
the body. <!-- upstream:primitive-in-effect-callback -->
|
|
200
215
|
14. Do not port these React tools — they have no Solid equivalent because the problems they
|
|
201
216
|
solve don't exist: `useCallback`, `React.memo`, `forwardRef`, `useSyncExternalStore`,
|
|
202
217
|
dependency arrays, `startTransition`/`useTransition` (updates are held and coordinated
|
|
@@ -240,7 +255,9 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
240
255
|
from `@solidjs/web` — do not call `captureException` inside `<Errored fallback>`.
|
|
241
256
|
The server hook's return value replaces the sanitized error on the wire: return
|
|
242
257
|
nothing or a reference id, never the error itself (message/stack/secrets would
|
|
243
|
-
reach the browser).
|
|
258
|
+
reach the browser). Failures outside render and server functions (middleware,
|
|
259
|
+
request setup) reach it via `reportRequestFailure(error, event)` as
|
|
260
|
+
`kind: "request"`. <!-- upstream:report-request-failure -->
|
|
244
261
|
Making a client store "real" is additive: same setters, wrap mutations in
|
|
245
262
|
`action`, swap in the **function form** `createOptimisticStore(() => api.list(), [])`
|
|
246
263
|
(with `refresh` after the `yield`) and a file of server functions — do
|
|
@@ -323,9 +340,10 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
323
340
|
render. `NoHydration` / `Hydration` split hydration *ownership*; they do not choose
|
|
324
341
|
visible content — `clientOnly` is for components that must never run on the server.
|
|
325
342
|
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
|
-
|
|
343
|
+
(`"client"` skips the server value and computes after hydration, e.g. `localStorage`,
|
|
344
|
+
or server data mixed with the window size; `"hybrid"` adopts the serialized value and
|
|
345
|
+
then continues an async-iterable source on the client — for a sync or promise compute
|
|
346
|
+
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
347
|
`onSettled` fill-in remains a valid alternative.
|
|
330
348
|
Pass a **function** to `render` / `hydrate` / `renderToString` / `renderToStream`
|
|
331
349
|
(`render(() => <App />, root)`), never `render(<App />, root)`. `hydrate` when the
|
|
@@ -348,7 +366,8 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
348
366
|
link previews) takes `{ deferStream: true }` on its memo; keep the boundary. `pipe` / `pipeTo` / `readable` each consume
|
|
349
367
|
a stream render — use exactly one. `createRoot` is for tests, libraries, and
|
|
350
368
|
non-render entry points; inside a component let `render` / the component owner
|
|
351
|
-
own the scope
|
|
369
|
+
own the scope (a root created under an owner is disposed with it — detach on purpose
|
|
370
|
+
with `runWithOwner(null, () => createRoot(...))`). Async reads inside `<Portal>` start on the client — hoist the
|
|
352
371
|
read above the portal. Navigation-shaped updates (a setter, then async computeds holding previous values)
|
|
353
372
|
do not need core `action` — reach for it only when writes happen *after* async work.
|
|
354
373
|
Invoke actions from handlers, not from memos or effects.
|
|
@@ -381,9 +400,14 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
381
400
|
the route is matched. One router instance —
|
|
382
401
|
Solid Router does not support nested `<Router>` / nested `createRouter`.
|
|
383
402
|
In-app navigation is `useNavigate` or `<a href={Router.paths...}>`, not
|
|
384
|
-
`window.location` / `history.pushState`. Link state
|
|
385
|
-
|
|
386
|
-
|
|
403
|
+
`window.location` / `history.pushState`. Link state needs no component code:
|
|
404
|
+
`aria-current` (same path and same query) and `data-active` (path prefix, the
|
|
405
|
+
root exact-only) are automatic; `data-pending` is opt-in via
|
|
406
|
+
`createRouter({ links: pendingLinks })`. Style them in CSS — nav highlighting on
|
|
407
|
+
`data-active` (a nav link to `/products` loses `aria-current` on
|
|
408
|
+
`/products?page=2`); `useLinkState` /
|
|
409
|
+
`useIsRouting` in JSX — never hand-rolled `location.pathname` comparisons.
|
|
410
|
+
<!-- upstream:pending-links-opt-in --> Trusted identity is
|
|
387
411
|
`getRequestEvent()`, never a caller-supplied user id. Unscripted (no-JS) POST
|
|
388
412
|
forms use a router `action` over a `(form: FormData)` server function (the post
|
|
389
413
|
arrives as one `FormData`; other signatures make `action={save}` TS2322) —
|
|
@@ -437,9 +461,11 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
437
461
|
experimental `serverFunctions.components` flag. Server-function arguments are JSON:
|
|
438
462
|
send `Date` / `Map` / `Set` as ISO strings / arrays (they throw otherwise; one `File` /
|
|
439
463
|
`Blob` / `FormData` argument travels natively). `enableRichArguments()` from
|
|
440
|
-
`@solidjs/web/server-functions/rich-args` (at `src/App.tsx` module scope) lifts that
|
|
441
|
-
|
|
442
|
-
|
|
464
|
+
`@solidjs/web/server-functions/rich-args` (at `src/App.tsx` module scope) lifts that
|
|
465
|
+
(its import builds cleanly since 2.0.0-rc.10 — a `resolve.dedupe: ["@solidjs/web"]`
|
|
466
|
+
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))` —
|
|
467
|
+
`live()` outermost; an undeclared async iterable still producing while a document
|
|
468
|
+
renders is `[SSR_UNDECLARED_LIVE_SOURCE]` in dev. A plain `async function*` server function is an event stream on
|
|
443
469
|
one connection (no reconnect when it drops); `live()` is one value that changes over
|
|
444
470
|
time and reconnects. `live()` connection state is
|
|
445
471
|
`source.onstatus` (`"connected"` / `"reconnecting"` / `"closed"`), never a
|
|
@@ -493,22 +519,27 @@ directory (default `src/`).
|
|
|
493
519
|
| `startTransition`, `useTransition` | automatic held updates + `isPending` |
|
|
494
520
|
| `onError` / `catchError` | `<Errored>` or effect bundle `{ effect, error }` |
|
|
495
521
|
| `from(...)` / `observable(...)` (the solid-js helpers) | inbound: async iterable from a memo; outbound: split effect |
|
|
496
|
-
| `createDynamic(...)`, `<Dynamic component={...}>` (deprecated in 2.0) <!-- upstream:dynamic-deprecated --> | `dynamic(source)` from `@solidjs/web` |
|
|
522
|
+
| `createDynamic(...)`, `<Dynamic component={...}>` (deprecated in 2.0) <!-- upstream:dynamic-deprecated --> | `dynamic(source)` from `@solidjs/web` — `dynamicComponent(source)` when the source only answers with a component <!-- upstream:dynamic-component-export --> |
|
|
497
523
|
| `renderToStringAsync(...)` | `await renderToStream(() => <App />)` (one consumer: `pipe` / `pipeTo` / `readable`) |
|
|
498
524
|
| `clearDelegatedEvents` | delete — delegated listeners are scoped to each render root |
|
|
499
525
|
| `vite-plugin-solid` / `jsxImportSource: "solid-js"` | `@solidjs/vite-plugin` / `"jsxImportSource": "@solidjs/web"` |
|
|
500
|
-
| JSX `<Route>` / `<A>` / `<HashRouter>` / `<Navigate>` / `<FileRoutes>` | `createRouter({ routes })`, `fileRoutes(pageRoutes)` (`fileRoutes` from `@solidjs/router/fs`, `pageRoutes` from `virtual:file-routes`), plain `<a href={Router.paths...}>`, `hashHistory()`; link state via automatic `aria-current` / `data-active`
|
|
526
|
+
| JSX `<Route>` / `<A>` / `<HashRouter>` / `<Navigate>` / `<FileRoutes>` | `createRouter({ routes })`, `fileRoutes(pageRoutes)` (`fileRoutes` from `@solidjs/router/fs`, `pageRoutes` from `virtual:file-routes`), plain `<a href={Router.paths...}>`, `hashHistory()`; link state via automatic `aria-current` / `data-active` + CSS and opt-in `data-pending` (`links: pendingLinks`), or `useLinkState` in JSX <!-- upstream:pending-links-opt-in --> |
|
|
501
527
|
| `useCurrentMatches`, `MemoryRouter` / `createMemoryHistory`, `redirect` / `reload` from `@solidjs/router`, `indexArray` | `useRouteMatches`, `createRouter({ routes, history: memoryHistory("/x") })`, `redirect` / `reload` from `@solidjs/web`, `mapArray(list, fn, { keyed: false })` |
|
|
502
528
|
| `createAsync` / `createAsyncStore` / `useSubmission` / router `json()` / `cache()` | `createMemo(() => getUser(id()))`, `useSubmissions`, `respond()` from `@solidjs/web`, `query` |
|
|
503
529
|
| `import ... from "@solidjs/start"` / `vinxi` / `h3` / `"use client"` | `GET` from `@solidjs/web/server-functions`; Solid has no `"use client"` |
|
|
504
530
|
| `render(<App />, root)` | `render(() => <App />, root)` (same for `hydrate` / `renderToString` / `renderToStream`) |
|
|
505
531
|
| `<MetaProvider>` | no provider — render `<Title>` / `<Meta>` / `<Link>` from `@solidjs/meta` anywhere |
|
|
506
|
-
| `use:directive`, `on:`/`oncapture:`, `attr:`/`bool:`, `/*@once*/` | `ref` callbacks, camelCase event props, standard attributes, keep values reactive; `prop:` remains for DOM properties (`prop:indeterminate`, objects/arrays on custom elements — plain props are attributes and stringify) |
|
|
532
|
+
| `use:directive`, `on:`/`oncapture:`, `attr:`/`bool:`, `/*@once*/` (also lowercase `onclick`-style attributes — plain attributes, not events <!-- upstream:lowercase-event-attribute -->) | `ref` callbacks, camelCase `on`-capitalized event props, standard attributes, keep values reactive; `prop:` remains for DOM properties (`prop:indeterminate`, objects/arrays on custom elements — plain props are attributes and stringify) |
|
|
507
533
|
| `resource.loading` / `resource.error` | `<Loading>` boundary / `<Errored>` boundary |
|
|
508
534
|
|
|
509
535
|
When unsure about any API, verify against the official Solid 2.0 docs — fetchable URLs are
|
|
510
536
|
listed in `references/official-docs.md` next to the `solid-2` skill. Do not guess from
|
|
511
|
-
Solid 1.x or React memory.
|
|
537
|
+
Solid 1.x or React memory. Two official pages still describe the previous release —
|
|
538
|
+
these rules win there: the router navigation page lists `data-pending` as automatic
|
|
539
|
+
(it needs `links: pendingLinks`) <!-- upstream:docs-pending-links-stale -->, and the
|
|
540
|
+
`createSignal` reference says a write to a writable derivation wins over a same-tick
|
|
541
|
+
recompute (a source change in the same update now re-runs it with the write as
|
|
542
|
+
`prev`). <!-- upstream:docs-derived-write-stale -->
|
|
512
543
|
|
|
513
544
|
`createRenderEffect` exists in Solid 2 but is not a default. `storePath`
|
|
514
545
|
(the 1.x path-setter migration helper) is no longer exported from `solid-js` as of
|
|
@@ -39,7 +39,7 @@ Practical consequences:
|
|
|
39
39
|
| Object that is replaced wholesale (fetch response, selected item, `User \| null`) | `createSignal(objectValue)` |
|
|
40
40
|
| Value derived from other reactive values | plain function `() => ...` (never a signal synced by an effect) |
|
|
41
41
|
| Expensive derivation, multiple consumers, or equality boundary needed | `createMemo(() => ...)` |
|
|
42
|
-
| Follows a prop but user can locally override; prop change resets | writable derivation: `createSignal(() => props.value)` / `createStore(() => props.value, fallback)` |
|
|
42
|
+
| Follows a prop but user can locally override; prop change resets | writable derivation: `createSignal(() => props.value)` / `createStore(() => props.value, fallback)` — a write lands at once and the function receives it as `prev` (the draft on a store) on re-run; honoring `prev` keeps the edit across source changes <!-- upstream:derived-write-prev --> |
|
|
43
43
|
| Shared by a subtree, independent per provider instance | signal/store created inside a provider component, passed via context |
|
|
44
44
|
| App-wide state (theme, session, cart, locale) | the same provider, placed at the root of `App` — not a module-level signal/store (under SSR one module instance serves every request) |
|
|
45
45
|
| Constants (a category list, a formatter, the `createContext` call) | module scope |
|
|
@@ -69,12 +69,12 @@ 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) <!-- upstream:dynamic-deprecated --> |
|
|
72
|
+
| Pick a component/tag from reactive state | `dynamic(() => ...)` from `@solidjs/web` (stable identity; `<Dynamic>` is deprecated) <!-- upstream:dynamic-deprecated --> — `dynamicComponent(() => ...)` when the source only answers with a component, never a tag <!-- upstream:dynamic-component-export --> |
|
|
73
73
|
| Overlay / modal | `<Portal>` — hoist async reads *above* the portal (reads inside start on the client) |
|
|
74
74
|
| Async value used several layers down | Create the memo high; pass `value={memo()}` through intermediates (they do not wait); put `<Loading>` around the leaf read |
|
|
75
75
|
| Nested child with its own fetch | Leave it nested — it runs in parallel. Sequential only when the second call needs the first response (`fetchAuthor(story().authorId)`) |
|
|
76
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
|
|
@@ -264,6 +268,13 @@ is **deprecated** since 2.0.0-rc.9 — `@deprecated` in the types, no runtime wa
|
|
|
264
268
|
still shipped in 2.0. <!-- upstream:dynamic-deprecated --> Hoist a `dynamic()` per component instance (or per module for a
|
|
265
269
|
constant tag) instead.
|
|
266
270
|
|
|
271
|
+
When the source only ever answers with a component — never a tag name — use
|
|
272
|
+
`dynamicComponent()` instead: same contract and hydration shape minus the tag arm, so
|
|
273
|
+
a tag name is a compile error and a page whose only dynamic mounts are components
|
|
274
|
+
retains no element runtime (`createElement` / `spread` / the namespace tables). It
|
|
275
|
+
is the documented way to mount a server component: `dynamicComponent(() =>
|
|
276
|
+
getStory(props.id))`. <!-- upstream:dynamic-component-export -->
|
|
277
|
+
|
|
267
278
|
### Two-phase effect (imperative boundary only)
|
|
268
279
|
|
|
269
280
|
```tsx
|
|
@@ -327,7 +338,12 @@ const [signInError, setSignInError] = createSignal<string | null>(() => {
|
|
|
327
338
|
```
|
|
328
339
|
|
|
329
340
|
The writable form spells exactly this: derive from the source, allow a local
|
|
330
|
-
override, reset when the source changes.
|
|
341
|
+
override, reset when the source changes. The mechanics: a write lands at once,
|
|
342
|
+
and on the next source change the function re-runs receiving the write as `prev`
|
|
343
|
+
(a store receives it as the draft). A function that ignores `prev` resets — this
|
|
344
|
+
pattern — and also discards a write staged in the same update as a source change;
|
|
345
|
+
one that honors `prev` can keep the local value. <!-- upstream:derived-write-prev -->
|
|
346
|
+
(React needs `useState(props.x)` plus a
|
|
331
347
|
`previousX` comparison reset during render for the same behavior.)
|
|
332
348
|
|
|
333
349
|
### Async data — fetch high, block low
|
|
@@ -399,8 +415,12 @@ return <StoryDetail story={story} />;
|
|
|
399
415
|
Lifting the fetch does **not** mean lifting the boundary — that is consume glued
|
|
400
416
|
back onto block. After first paint the
|
|
401
417
|
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
|
|
418
|
+
not `isPending(results())`) is the indicator. Add `on={query()}` (a tracked **read** —
|
|
419
|
+
the accessor `on={query}` reads nothing) only when that change of subject should put the
|
|
420
|
+
fallback back on screen. `on` is a dependency list, not a compared key: its value is
|
|
421
|
+
irrelevant, several inputs go in as `on={[query(), page()]}`, and a change to anything it
|
|
422
|
+
reads re-arms the boundary. <!-- upstream:loading-on-dependency-list --> `[LOADING_ON_OUTSIDE_HOLD]` in dev means the same data is
|
|
423
|
+
also read outside the boundary, so the fallback can never show — move that read under it.
|
|
404
424
|
Do not use `isPending` as the first-load spinner.
|
|
405
425
|
- **Nesting is not a waterfall.** `Comments` above fetches in parallel with `story`
|
|
406
426
|
because it reads `props.storyId` (already known), not `props.story`. Passing
|
|
@@ -426,7 +446,8 @@ return <StoryDetail story={story} />;
|
|
|
426
446
|
the same rule: bind `value={latest(query)}` so a controlled search box reflects
|
|
427
447
|
keystrokes while its write is held (an uncontrolled input needs nothing).
|
|
428
448
|
- Read every reactive input **before the first `await`** in an async computation. A read
|
|
429
|
-
after `await` does not subscribe
|
|
449
|
+
after `await` does not subscribe (dev reports `[UNTRACKED_READ_AFTER_AWAIT]` on V8 —
|
|
450
|
+
Chromium, Node, Deno, Bun); production can sit pending with no retry.
|
|
430
451
|
- `<Errored>` function fallbacks receive an error **accessor** and a `reset` callback:
|
|
431
452
|
`fallback={(error, reset) => ...}`. The boundary **heals** when the source succeeds
|
|
432
453
|
again (refresh, live reconnect, input change) — it is graph status, not a terminal
|
|
@@ -498,7 +519,11 @@ rendered), `render` / `client` (a `<Loading>` fragment rejected, client re-rende
|
|
|
498
519
|
hydration value would not serialize), `server-function` / `thrown` (`direct: true` for
|
|
499
520
|
an in-process call during SSR), `server-function` / `channel` (a rejection or throw
|
|
500
521
|
escaping through the result graph — a promise, an iterable, or a stream — after the
|
|
501
|
-
head committed)
|
|
522
|
+
head committed), and `request` / `failed` — a failure outside render and server
|
|
523
|
+
functions (middleware, request setup) reported via
|
|
524
|
+
`reportRequestFailure(error, event)` from `@solidjs/web`. A synchronous throw out of
|
|
525
|
+
`renderToString` / `renderToStream`'s first pass is reported `render` / `failed`
|
|
526
|
+
before rethrow. <!-- upstream:report-request-failure --> A monitoring SDK's `init()` that
|
|
502
527
|
registers this hook must load before the server graph — put it in the plugin's
|
|
503
528
|
`start: { instrument: "./src/instrument.ts" }`, not at the top of an entry (see
|
|
504
529
|
[Production observability](#production-observability-the-observe-build)).
|
|
@@ -956,7 +981,12 @@ onSettled(() => {
|
|
|
956
981
|
|
|
957
982
|
Effect callbacks follow the same shape: return the cleanup from `apply` (an
|
|
958
983
|
`onCleanup` there never runs), and `flush()` inside one is a no-op
|
|
959
|
-
(`[FLUSH_IN_EFFECT_CALLBACK]`).
|
|
984
|
+
(`[FLUSH_IN_EFFECT_CALLBACK]`). Creating a memo, an effect, or a root inside `apply`
|
|
985
|
+
throws in dev (`[PRIMITIVE_IN_EFFECT_CALLBACK]` — the effect phase has no owner to
|
|
986
|
+
dispose them; `runWithOwner(null, …)` does not lift it): create them in the
|
|
987
|
+
component body or the compute phase, or attach them to an owner captured with
|
|
988
|
+
`getOwner()` in the body via `runWithOwner(owner, …)`.
|
|
989
|
+
<!-- upstream:primitive-in-effect-callback -->
|
|
960
990
|
|
|
961
991
|
### Per-value SSR policy: `ssrSource`
|
|
962
992
|
|
|
@@ -988,8 +1018,8 @@ function DraftPreview(props: { documentId: string }) {
|
|
|
988
1018
|
| Policy | Meaning | Use when |
|
|
989
1019
|
|---|---|---|
|
|
990
1020
|
| `"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) |
|
|
1021
|
+
| `"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 |
|
|
1022
|
+
| `"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
1023
|
|
|
994
1024
|
With `"client"`, the bare form suspends on the server and the nearest `<Loading>`
|
|
995
1025
|
fallback renders into the HTML; a declared first value (`loadingValue` on
|
|
@@ -1068,19 +1098,24 @@ see [Production observability](#production-observability-the-observe-build) —
|
|
|
1068
1098
|
dev setting. Diagnostics come in two layers: **findings** (facts about a render, present
|
|
1069
1099
|
in observe and dev) and **checks** (dev-only guidance printed by the console reporter).
|
|
1070
1100
|
Always-on dev checks include misplaced reads
|
|
1071
|
-
(`[STRICT_READ_UNTRACKED]`, `[PENDING_ASYNC_UNTRACKED_READ]`
|
|
1101
|
+
(`[STRICT_READ_UNTRACKED]`, `[PENDING_ASYNC_UNTRACKED_READ]`, `[UNTRACKED_READ_AFTER_AWAIT]`
|
|
1102
|
+
for a first read made after an `await` in an async compute — V8 engines only), misplaced writes
|
|
1072
1103
|
(`[REACTIVE_WRITE_IN_OWNED_SCOPE]` — since rc.9 also for store setters called in a
|
|
1073
1104
|
component or root body <!-- upstream:store-write-owned-scope -->, `[ASYNC_STORE_SETTER]` for a store setter callback that returns
|
|
1074
1105
|
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
|
|
1106
|
+
`[ASYNC_OUTSIDE_LOADING_BOUNDARY]` when a tracked read has no boundary above it, and
|
|
1107
|
+
`[LOADING_ON_OUTSIDE_HOLD]` when a `<Loading on>` boundary's data is also read outside it.
|
|
1076
1108
|
Each entry carries a stable `[CODE]`, what the runtime observed, and an owner chain
|
|
1077
1109
|
(`in <App> › <Cart> › <LineItem>`); never silence a code before understanding it.
|
|
1078
1110
|
Since rc.9 the server render reports on the same channel with the same owner chains:
|
|
1079
1111
|
`[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
|
-
`[
|
|
1112
|
+
it was thrown, `boundaryPath` where it was met) <!-- upstream:ssr-render-error-contained -->, `[SERVER_ERROR_SANITIZED]` (the original
|
|
1113
|
+
behind a production-sanitized error, from a server function or an SSR render),
|
|
1114
|
+
`[SSR_CLIENT_CONTENT_MASKED]` (client-only content that surfaced only after a real
|
|
1115
|
+
server wait — the wait was wasted), `[SSR_BOUNDARY_WATERFALL]` (sequential waits inside
|
|
1116
|
+
one server `<Loading>` boundary), `[SSR_UNDECLARED_LIVE_SOURCE]` (an async iterable
|
|
1117
|
+
still pumping into a document render — declare it `live()`), and
|
|
1118
|
+
`[REVEAL_IN_RENDER_TO_STRING]`.
|
|
1084
1119
|
For the repair behind any code, read the `reactivity-diagnostics` skill Solid ships
|
|
1085
1120
|
in the repo (`node_modules/solid-js/skills/reactivity-diagnostics/SKILL.md`) — every
|
|
1086
1121
|
code maps to what the runtime observed and what to change.
|
|
@@ -1091,13 +1126,18 @@ every scope that re-ran, what changed to cause it, and how long it took. Since
|
|
|
1091
1126
|
no longer exists <!-- upstream:dev-attribution-removed --> (it is a type error; `DEV` holds only devtools hooks, graph
|
|
1092
1127
|
traversal, and console reporting). `OBSERVE` is the public observability surface on
|
|
1093
1128
|
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
|
-
|
|
1129
|
+
`OBSERVE.exclude` / `OBSERVE.include`, `OBSERVE.ownerPath`, `OBSERVE.server.trace` are
|
|
1130
|
+
for app and tool code; only the `OBSERVE.attribution` slot (`withInteraction` /
|
|
1131
|
+
`withOrigin`) belongs to engines, routers, and devtools. Since 2.0.0-rc.9 the folds,
|
|
1132
|
+
queries, and formatters are **named exports** of that subpath, not methods of
|
|
1133
|
+
`attribution` (`attribution.why` / `.costs` / `.feedback` / `.subscriptions` / `.format`
|
|
1134
|
+
are type errors) <!-- upstream:attribution-named-exports -->; `attribution` keeps `enable` (a hold on the one shared engine — it
|
|
1135
|
+
returns the release), `disable` (full teardown), `markFlight`, and one ring-buffer reader,
|
|
1136
|
+
`attribution.history(type)` (`"rerun"` / `"hold"` / `"waterfall"` / `"navigation"` /
|
|
1137
|
+
`"interaction"`). Live records — the engine's included — arrive on
|
|
1138
|
+
`OBSERVE.records.subscribe(type, (event, live) => …)`; `attribution.subscribe` and the
|
|
1139
|
+
`holds()` / `waterfalls()` / `navigations()` / `interactions()` methods are gone since
|
|
1140
|
+
2.0.0-rc.10. <!-- upstream:attribution-history-records -->
|
|
1101
1141
|
|
|
1102
1142
|
```ts
|
|
1103
1143
|
import { attribution, formatRerun, why } from 'solid-js/attribution';
|
|
@@ -1116,13 +1156,15 @@ to query the tables yourself. Name the scopes you intend to interrogate
|
|
|
1116
1156
|
(`createMemo(..., { name: "total" })`) — chains refer to nodes by name, and
|
|
1117
1157
|
anonymous nodes print as `computed` / `effect` / `signal`; component labels
|
|
1118
1158
|
(`<Checkout>`) are always present under `vite dev` and survive minification only with
|
|
1119
|
-
the compiler's `
|
|
1159
|
+
the compiler's `sourceNames` option (components, binding effects, primitives — the Vite
|
|
1160
|
+
plugin turns it on under `vite dev` and `observe: true`; `componentNames` was its rc.9 name
|
|
1161
|
+
and is rejected now) <!-- upstream:source-names-option -->. On the
|
|
1120
1162
|
**prod build** the import resolves to an inert same-surface twin whose `enable()` is a
|
|
1121
1163
|
no-op, so the import can stay in the code. On the **observe build** it is the real
|
|
1122
1164
|
engine and an unguarded `enable()` records in production — keep the `isDev` guard
|
|
1123
1165
|
unless recording in production is the intent. The split is pay-for-use: importing
|
|
1124
1166
|
`costs` or `feedback` is what turns their tables on, so a records-only consumer
|
|
1125
|
-
(`
|
|
1167
|
+
(`OBSERVE.records.subscribe("rerun", …)`) ships neither.
|
|
1126
1168
|
|
|
1127
1169
|
While attribution is enabled, Solid also warns on its own about over-subscription,
|
|
1128
1170
|
chained async waterfalls (`[ASYNC_WATERFALL]`), memos whose fresh-but-equal output
|
|
@@ -1136,10 +1178,18 @@ ran while held; info from 100ms, warning from 200ms, tunable via
|
|
|
1136
1178
|
`enable({ holds: { infoMs, warnMs } })`). A hold that *was* acknowledged but still ran
|
|
1137
1179
|
long is `[LONG_HOLD]` (info from 500ms, warning from 1000ms;
|
|
1138
1180
|
`enable({ longHolds: { infoMs, warnMs } })`) — the suggestion is a `<Loading on={...}>`
|
|
1139
|
-
boundary, never removing the hold.
|
|
1140
|
-
`
|
|
1141
|
-
`
|
|
1142
|
-
|
|
1181
|
+
boundary, never removing the hold. Beside them: `[UNTRACKED_ASYNC_HANDLER]` (an `async`
|
|
1182
|
+
handler that awaits with no write before the `await` and no `action` — no hold exists, so
|
|
1183
|
+
the click looks dead; make it an `action`, or write a `createOptimistic(false)` flag
|
|
1184
|
+
first), `[ABANDONED_FLIGHTS]` (a source re-asked on every keystroke, answers thrown away
|
|
1185
|
+
— debounce at the handler), `[STACKED_HOLDS]` (repeated clicks piling into one hold),
|
|
1186
|
+
`[FALLBACK_FLASH]` (a fallback shown under 150ms), `[WASTED_RECOMPUTE]` (a scope whose
|
|
1187
|
+
runs mostly compared equal — narrow its reads upstream), `[GRAPH_GROWTH]` (the graph grows
|
|
1188
|
+
on every visit to a route — an undisposed root or subscription), and `[OPTIMISTIC_REVERTED]`
|
|
1189
|
+
(info: a guess the screen showed was replaced). `costs()` and `feedback()` (named imports)
|
|
1190
|
+
rank the session worst-first; `attribution.history("hold")` / `("waterfall")` /
|
|
1191
|
+
`("navigation")` / `("interaction")` hold the fact tables behind the verdicts (each hold
|
|
1192
|
+
carries its `silent` / `long` verdict). <!-- upstream:attribution-history-records -->
|
|
1143
1193
|
`subscriptions(scope)` (named import) lists a scope's current dependencies to compare
|
|
1144
1194
|
against what it uses.
|
|
1145
1195
|
|
|
@@ -1202,11 +1252,19 @@ effects** — a pure markup or styling change does not need it.
|
|
|
1202
1252
|
`attribution.enable({ log: false })` in the client entry, then `why(scope)` /
|
|
1203
1253
|
`costs()` / `feedback()` from the console or a scratch effect — never `log: true`
|
|
1204
1254
|
left on (one collapsed `[why-run]` group per re-run) and never unguarded (on the
|
|
1205
|
-
observe build it records in production).
|
|
1255
|
+
observe build it records in production). Under `vite dev` the plugin also paints the
|
|
1256
|
+
same records as tracks in the Chrome Performance panel (`performanceTracks`, on by
|
|
1257
|
+
default; `false` opts out) — record there when a timeline reads better than tables.
|
|
1258
|
+
The six attribution cost checks (`hotRuns`, `hotTime`, `wideDeps`, `unstableMemos`,
|
|
1259
|
+
`fanOut`, `wastedRecompute`) are opt-in there: `performanceTracks: { attribution:
|
|
1260
|
+
{ checks: true } }` (or `enablePerformanceTracks({ attribution: { checks: true } })`,
|
|
1261
|
+
or your own `attribution.enable()` hold — the diagnostics-session path above already
|
|
1262
|
+
turns them on). <!-- upstream:perf-tracks-cost-checks -->
|
|
1206
1263
|
4. **Read the verdicts, then the tables.** Fix every coded warning the session raised
|
|
1207
1264
|
(`[SILENT_HOLD]`, `[IMMUTABLE_UPDATE_IN_STORE]`, `[UNSTABLE_LIST_IDENTITY]`,
|
|
1208
1265
|
`[EFFECT_RELAY_TEAR]`, `[EFFECT_WRITES_OWN_SOURCE]`, `[ASYNC_WATERFALL]`,
|
|
1209
|
-
`[HOT_SCOPE_RERUNS]
|
|
1266
|
+
`[HOT_SCOPE_RERUNS]`, `[WASTED_RECOMPUTE]`, `[ABANDONED_FLIGHTS]`,
|
|
1267
|
+
`[UNTRACKED_ASYNC_HANDLER]`). Then `costs().scopes` (self time, `wastedMs` for runs whose
|
|
1210
1268
|
result did not change), `costs().writes` (root writes by downstream work),
|
|
1211
1269
|
`feedback().sources` / `.interactions` / `.flights` / `.fallbacks` (silent holds,
|
|
1212
1270
|
held time per interaction, abandoned requests, fallback flashes). A scope that
|
|
@@ -1233,14 +1291,16 @@ export default { plugins: [solid({ ssr: true, observe: true })] };
|
|
|
1233
1291
|
```
|
|
1234
1292
|
|
|
1235
1293
|
`observe: true` adds the `observe` condition to every environment (client and server,
|
|
1236
|
-
`vite build` and preview) and turns on the compiler's `
|
|
1237
|
-
`ownerPath` / `boundaryPath` name components instead
|
|
1238
|
-
set `resolve.conditions` (or `node --conditions=observe`
|
|
1239
|
-
`
|
|
1294
|
+
`vite build` and preview) and turns on the compiler's `sourceNames` option (components,
|
|
1295
|
+
binding effects, and primitives) so `ownerPath` / `boundaryPath` name components instead
|
|
1296
|
+
of `computed` <!-- upstream:source-names-option -->. Without the plugin, set `resolve.conditions` (or `node --conditions=observe`
|
|
1297
|
+
for an unbundled server) and `sourceNames` yourself. Cost in Solid's size suite: ~16.8 KB against ~15.3 KB brotli
|
|
1240
1298
|
for a small client app; importing the attribution engine on top raises the cap to
|
|
1241
1299
|
~27 KB (a separate entry — an observe build that never imports it never ships it). The
|
|
1242
1300
|
two error hooks are **not** part of this: they fire on every build, prod included, only
|
|
1243
|
-
the paths are `undefined` there.
|
|
1301
|
+
the paths are `undefined` there. `enablePerformanceTracks()` from
|
|
1302
|
+
`@solidjs/web/performance-tracks` paints the records on the Chrome Performance panel in
|
|
1303
|
+
production too (observe build; a no-op on the prod build).
|
|
1244
1304
|
|
|
1245
1305
|
- **Instrumentation order.** A monitoring SDK's `init()` (and `configureServerErrors`)
|
|
1246
1306
|
must run before the modules it patches load. `import "./instrument"` at the top of an
|
|
@@ -1257,22 +1317,31 @@ the paths are `undefined` there.
|
|
|
1257
1317
|
`OBSERVE?.server.trace.provide((request) => ({ traceId, spanId, entries }))`, once per
|
|
1258
1318
|
request. Do not write middleware to inject trace headers.
|
|
1259
1319
|
- **Records.** `OBSERVE?.records.subscribe(type, (event, live) => …)` delivers one plain
|
|
1260
|
-
record per `"
|
|
1261
|
-
|
|
1262
|
-
`
|
|
1320
|
+
record per `"render"` (a server render: `mode`, `shellMs`, `durationMs`, `boundaries`,
|
|
1321
|
+
`outcome`, the matched `route`), `"boundary"` (a `<Loading>` that waited on the server),
|
|
1322
|
+
`"invocation"` (server-function execution), `"request"` / `"call"` (a server-function
|
|
1323
|
+
call from the page, at send and at settle — `call` shares `id` with its invocation),
|
|
1324
|
+
`"frame"` (produced / applied), and `"recovery"` (a boundary the client re-rendered
|
|
1325
|
+
after a server failure); the attribution engine's own records (`"rerun"`, `"hold"`,
|
|
1326
|
+
`"interaction"`, `"navigation"`, …) ride the same channel while it is enabled. Records are
|
|
1263
1327
|
serializable data; live handles (request, args, the error as thrown) ride in the
|
|
1264
1328
|
second argument. Listeners run synchronously inside the runtime, **must not write
|
|
1265
1329
|
signals**, and should subscribe from a module that loads before the app.
|
|
1266
|
-
- **Interactions.** `attribution.enable()` then
|
|
1267
|
-
`
|
|
1330
|
+
- **Interactions.** `attribution.enable()` (keep the release it returns) then
|
|
1331
|
+
`OBSERVE?.records.subscribe("interaction", (event) => …)` <!-- upstream:attribution-history-records --> says what each click waited on
|
|
1268
1332
|
(`event.settledMs`, `event.holds[].blockers`, `event.holds[].holdMs`); a hold nothing acknowledged
|
|
1269
1333
|
is the dead click. The fold tables stay separate imports.
|
|
1270
1334
|
- **What leaves the process.** Beyond names, four record fields carry page data —
|
|
1271
1335
|
`target` (element as `tag#id "text"`, 30 chars), `prev` / `value` previews (40 chars),
|
|
1272
|
-
navigation `to` / `from` / `params`, and `data.error` on server render findings
|
|
1273
|
-
|
|
1336
|
+
navigation `to` / `from` / `params`, and `data.error` on server render findings. The
|
|
1337
|
+
engine's `values` option governs the first three at the source — `"full"` (the dev
|
|
1338
|
+
default), `"labels"` (no previews; element text only on buttons and links), `"none"`
|
|
1339
|
+
(the observe-build default) — so an exporter on the observe build passes its level
|
|
1340
|
+
explicitly (`attribution.enable({ values: "labels" })`) and decides what to do with the
|
|
1341
|
+
rest. A tool that renders inside the app it watches
|
|
1274
1342
|
(devtools panel) marks its root with `OBSERVE?.exclude(getOwner()!)` inside its
|
|
1275
|
-
`createRoot` so its own effects and stores never become findings
|
|
1343
|
+
`createRoot` so its own effects and stores never become findings (`OBSERVE?.include(owner)`
|
|
1344
|
+
re-admits the app's root under an excluded shell); do not route its
|
|
1276
1345
|
writes through `runWithOwner` (that is a write in an owned scope).
|
|
1277
1346
|
|
|
1278
1347
|
## Client mode: no server HTML, no hydration reflexes
|
|
@@ -1355,7 +1424,8 @@ export default function App() {
|
|
|
1355
1424
|
|
|
1356
1425
|
The `<Loading>` covers the first load of the lazy pages (and lets SSR stream the shell);
|
|
1357
1426
|
later navigations hold the current page while the next one loads (links get
|
|
1358
|
-
`data-pending`
|
|
1427
|
+
`data-pending` when the router is created with `links: pendingLinks`
|
|
1428
|
+
<!-- upstream:pending-links-opt-in -->). Create the instance at **module scope**. Nested layouts are `children` arrays
|
|
1359
1429
|
on the route objects, not nested `<Route>` / nested routers. Solid Router does
|
|
1360
1430
|
**not** support nested `<Router>` instances — compose one route tree (or a lazy
|
|
1361
1431
|
`children` thunk). Navigate with
|
|
@@ -1418,13 +1488,20 @@ Link warming: `preload="false"` skips that link's *data* preload; `preloadLinks:
|
|
|
1418
1488
|
disables automatic link preloading. Intent values are `"initial"` / `"navigate"` /
|
|
1419
1489
|
`"native"` / `"preload"`. One router per app.
|
|
1420
1490
|
|
|
1421
|
-
Link state needs no component code: the router sets `aria-current="page"` (
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
`
|
|
1491
|
+
Link state needs no component code: the router sets `aria-current="page"` (same path
|
|
1492
|
+
and same query — parameter order and hash aside), `data-active` (exact or descendant
|
|
1493
|
+
on the path only — the `/` link is active only on exact match), and, with the opt-in
|
|
1494
|
+
`createRouter({ links: pendingLinks })` plugin, `data-pending` (in-flight navigation
|
|
1495
|
+
target) on the anchors it handles; style them in CSS. Highlight nav links on
|
|
1496
|
+
`data-active`, not `aria-current`: a nav link to `/products` is not current on
|
|
1497
|
+
`/products?page=2`. An `aria-current` you write
|
|
1498
|
+
yourself is yours — the router only manages the attribute on links where it set it.
|
|
1499
|
+
For non-anchors, or state needed in JSX, `useLinkState(() => href,
|
|
1500
|
+
{ end })` returns the same three as accessors (`active` / `current` / `pending` —
|
|
1501
|
+
`pending` works without the plugin); `useIsRouting()` is the page-level pending
|
|
1502
|
+
flag for progress bars. Hand-rolled
|
|
1427
1503
|
`location.pathname === ...` comparisons are the wrong tool.
|
|
1504
|
+
<!-- upstream:pending-links-opt-in -->
|
|
1428
1505
|
|
|
1429
1506
|
Build URLs from `paths`: static segments are properties (`Router.paths.about`, never
|
|
1430
1507
|
`Router.paths.about()`), parameterized segments are calls (`paths.products(id)`),
|
|
@@ -1642,10 +1719,10 @@ status, so `return respond({ error }, { status: 400 })` reads as success to the
|
|
|
1642
1719
|
is success metadata a scripted caller unwraps. Arguments are JSON: send `Date` / `Map` /
|
|
1643
1720
|
`Set` as ISO strings / arrays (they throw otherwise; one `File` / `Blob` / `FormData`
|
|
1644
1721
|
argument travels natively). `enableRichArguments()` from
|
|
1645
|
-
`@solidjs/web/server-functions/rich-args` (at `src/App.tsx` module scope) lifts that
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1722
|
+
`@solidjs/web/server-functions/rich-args` (at `src/App.tsx` module scope) lifts that. Its
|
|
1723
|
+
import failed `vite build` in rc.9 (`"./client" is not exported`); that is fixed since
|
|
1724
|
+
2.0.0-rc.10, so a `resolve: { dedupe: ["@solidjs/web"] }` workaround left in a Vite config
|
|
1725
|
+
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 -->
|
|
1649
1726
|
`GET()` is only for idempotent reads (URLs leak into logs/history);
|
|
1650
1727
|
import it from `@solidjs/web/server-functions`, never `@solidjs/start`.
|
|
1651
1728
|
Reads are function calls too: `GET(async (id) => { "use server"; ... })` is invoked
|
|
@@ -1673,7 +1750,10 @@ export const fetchStory = GET(async (id: number) => {
|
|
|
1673
1750
|
A value that changes over time is `live(GET(fn))` — **`live()` outermost**: `GET()`
|
|
1674
1751
|
chooses the read transport and `live()` wraps the call behavior around it. `live()`
|
|
1675
1752
|
does not imply `GET()`; without it the source streams over POST, which is fine when a
|
|
1676
|
-
cacheable URL is of no use.
|
|
1753
|
+
cacheable URL is of no use. A server component rendered into a document that reads an
|
|
1754
|
+
*undeclared* async iterable still producing after 5s is `[SSR_UNDECLARED_LIVE_SOURCE]` in
|
|
1755
|
+
dev — declare it `live()` (the document takes the first value and closes it; the client
|
|
1756
|
+
connects after hydration) or bound the source.
|
|
1677
1757
|
|
|
1678
1758
|
```ts
|
|
1679
1759
|
// src/data/auction.ts
|
|
@@ -1806,7 +1886,7 @@ how to reuse. Prefer the form on the right.
|
|
|
1806
1886
|
| `createEffect(() => user, (u) => { log(u.name); })` | `createEffect(() => user.name, (name) => { log(name); })` |
|
|
1807
1887
|
| `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 |
|
|
1808
1888
|
| `<Loading fallback={<PageSkeleton />}>{/* header + data */}</Loading>` | wrap only the data slot; chrome stays outside |
|
|
1809
|
-
| `<Loading on={id} fallback={...}>` (the accessor) | `on={id()}` — a
|
|
1889
|
+
| `<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 --> |
|
|
1810
1890
|
| `setCount(count() + 1)` when writes can batch | `setCount((c) => c + 1)` |
|
|
1811
1891
|
| `setHandler(fn)` to store a function | `setHandler(() => fn)` (otherwise `fn` is an updater) |
|
|
1812
1892
|
| `{user() ? <P user={user()!} /> : <SignIn />}` | `<Show when={user()}>{(u) => <P user={u()} />}</Show>` |
|
|
@@ -1814,6 +1894,7 @@ how to reuse. Prefer the form on the right.
|
|
|
1814
1894
|
| `lazy(() => import("./p").then((m) => ({ default: m.About })))` | `lazy(() => import("./p"), { export: "About" })` |
|
|
1815
1895
|
| `dangerouslySetInnerHTML={{ __html }}` | `innerHTML={html()}` (sanitized); never with JSX children |
|
|
1816
1896
|
| `onClick={setCount}` | `onClick={() => setCount((c) => c + 1)}` |
|
|
1897
|
+
| `onclick={fn}` / `onmouseover={fn}` (lowercase handlers) | `onClick={fn}` — a lowercase `on*` name is a plain attribute (dev warns `[LOWERCASE_EVENT_ATTRIBUTE]`), not an event <!-- upstream:lowercase-event-attribute --> |
|
|
1817
1898
|
| `event.target.value` | `event.currentTarget.value` |
|
|
1818
1899
|
| `createContext(emptyStore)` for reactive state | `createContext<Todos>()` — dummy defaults silently no-op |
|
|
1819
1900
|
| `<For keyed={(t) => t.id}>{(todo) => todo.title}` | `todo().title` — key-function items are accessors |
|
|
@@ -1838,7 +1919,7 @@ how to reuse. Prefer the form on the right.
|
|
|
1838
1919
|
| `renderToString(() => <App />)` for an async or lazy-route tree | `await renderToStream(() => <App />)` (one consumer: `pipe` / `pipeTo` / `readable`). String render emits `<Loading>` fallbacks |
|
|
1839
1920
|
| `clientOnly(() => import("./Map"))` for a rarely shown widget | same + `{ lazy: true }` so the import waits until first render (default starts at declaration) |
|
|
1840
1921
|
| `<NoHydration><Chart /></NoHydration>` to skip SSR of a widget | `clientOnly(() => import("./Chart"))` — `NoHydration`/`Hydration` split ownership, they do not choose visible content |
|
|
1841
|
-
| `clientOnly` split (or `isServer` branch) for one browser-only *value* | `createMemo(() => localStorage.getItem(k), { ssrSource: "client" })` — `"hybrid"`
|
|
1922
|
+
| `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 --> |
|
|
1842
1923
|
| `await flush()` / `flush()` to wait on a pending memo | `await resolve(() => value())` (or Testing Library async queries); `flush()` only drains staged *sync* writes |
|
|
1843
1924
|
| `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 |
|
|
1844
1925
|
| `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` |
|
|
@@ -1865,7 +1946,7 @@ how to reuse. Prefer the form on the right.
|
|
|
1865
1946
|
| `action={"/_server/" + id}` / `action={createTodo.url}` on a POST form | a router action: `const save = action(createTodo, "create-todo")` + `<form action={save} method="post">` over a `(form: FormData)` server function (other signatures are TS2322) — `.url` on a bare `"use server"` function is TS2339; it is typed only on `GET()` / `live()` references (GET search forms) |
|
|
1866
1947
|
| `<form method="get" action={update.url}>` for a mutation | GET forms only for idempotent search; mutations are POST |
|
|
1867
1948
|
| `window.location.href = ...` / `history.pushState` | `useNavigate()` or `<a href={Router.paths...}>` |
|
|
1868
|
-
| `class={{ active: location.pathname === "/about" }}` hand-rolled link state | CSS on
|
|
1949
|
+
| `class={{ active: location.pathname === "/about" }}` hand-rolled link state | CSS on `aria-current` / `data-active` (+ `data-pending` via `links: pendingLinks`); `useLinkState` in JSX, `useIsRouting()` for progress bars <!-- upstream:pending-links-opt-in --> |
|
|
1869
1950
|
| `new URLSearchParams(location.search)` hand-rolled query parsing | `useSearchParams()` (setter merges, no scroll); typed via a route `search` schema + `useSearchParams(Router.paths.search)` |
|
|
1870
1951
|
| `Router.paths.about()` / string-concatenated URLs | static nodes are properties (`Router.paths.about`); calls bind params, then search object and hash |
|
|
1871
1952
|
| `httpStatus(404)` in a click handler | call it bare in the component / error-fallback body (a scope declaration, not a mutation) |
|
|
@@ -1901,7 +1982,7 @@ how to reuse. Prefer the form on the right.
|
|
|
1901
1982
|
| `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
1983
|
| `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 --> |
|
|
1903
1984
|
| `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 |
|
|
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 + `
|
|
1985
|
+
| `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 -->) |
|
|
1905
1986
|
| `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 |
|
|
1906
1987
|
| `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 |
|
|
1907
1988
|
| 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` |
|
|
@@ -2046,6 +2127,16 @@ agent use the mirror, which serves every page as plain markdown:
|
|
|
2046
2127
|
|
|
2047
2128
|
A curated URL list is in [references/official-docs.md](references/official-docs.md).
|
|
2048
2129
|
|
|
2130
|
+
Known stale pages — this skill is verified against the installed release and wins:
|
|
2131
|
+
|
|
2132
|
+
- `routing/solid-router/navigation` lists `data-pending` among the attributes the router
|
|
2133
|
+
sets on its own; it needs `createRouter({ links: pendingLinks })`, and `aria-current`
|
|
2134
|
+
also compares the query (the link-state paragraph under Solid Router 2 above).
|
|
2135
|
+
<!-- upstream:docs-pending-links-stale -->
|
|
2136
|
+
- `reference/solid-js/reactivity/create-signal` says a write to a writable derivation
|
|
2137
|
+
wins over a same-tick recompute; a source change in the same update re-runs the
|
|
2138
|
+
function, which receives the write as `prev`. <!-- upstream:docs-derived-write-stale -->
|
|
2139
|
+
|
|
2049
2140
|
**Never** consult Solid 1.x sources (docs.solidjs.com, pre-2.0 tutorials, old Stack Overflow).
|
|
2050
2141
|
Solid 2.0 is a breaking rewrite; 1.x answers are wrong here. The banned-API table is in the
|
|
2051
2142
|
always-applied rules installed alongside this skill.
|
|
@@ -2112,7 +2203,7 @@ always-applied rules installed alongside this skill.
|
|
|
2112
2203
|
memo/effect. Effect `apply` functions have block bodies and return only a
|
|
2113
2204
|
cleanup or nothing.
|
|
2114
2205
|
- [ ] Dev console shows no diagnostics: reads in tracking scopes (no
|
|
2115
|
-
`[STRICT_READ_UNTRACKED]`), store setters synchronous and never called at
|
|
2206
|
+
`[STRICT_READ_UNTRACKED]`, no `[UNTRACKED_READ_AFTER_AWAIT]`), store setters synchronous and never called at
|
|
2116
2207
|
body level (no `[ASYNC_STORE_SETTER]` <!-- upstream:async-store-setter-throws --> /
|
|
2117
2208
|
`[REACTIVE_WRITE_IN_OWNED_SCOPE]` <!-- upstream:store-write-owned-scope -->), held
|
|
2118
2209
|
writes paired with `isPending` / `latest` / optimistic values / `affects()`
|
|
@@ -2130,8 +2221,8 @@ always-applied rules installed alongside this skill.
|
|
|
2130
2221
|
core `action`/`refresh`. Forms: `<form action={save} method="post">`. Core `action`
|
|
2131
2222
|
bodies are generators (`function*` / `async function*`) suspending on `yield`,
|
|
2132
2223
|
never plain `async` functions. Link state is automatic attributes + CSS
|
|
2133
|
-
(`aria-current` / `data-active
|
|
2134
|
-
in JSX). Search params via `useSearchParams`. Forms validate on the server
|
|
2224
|
+
(`aria-current` / `data-active`, `data-pending` via `links: pendingLinks`;
|
|
2225
|
+
`useLinkState` / `useIsRouting` in JSX). <!-- upstream:pending-links-opt-in --> Search params via `useSearchParams`. Forms validate on the server
|
|
2135
2226
|
(`throw respond({ issues }, { status: 400 })`) with inline errors from
|
|
2136
2227
|
`useSubmissions`.
|
|
2137
2228
|
- [ ] `render(() => <App />, root)` — a function, not `render(<App />)`. `hydrate` when
|
|
@@ -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.20.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",
|