solid2-agent-kit 0.19.0 → 0.21.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 +14 -5
- package/bin/solid2-kit.mjs +37 -2
- package/files/commands/solid2-review.md +3 -2
- package/files/shared/agents-section.md +1 -1
- package/files/shared/rules-body.md +35 -10
- package/files/skills/solid-2/SKILL.md +257 -38
- package/package.json +1 -1
package/bin/baseline.mjs
CHANGED
|
@@ -10,12 +10,21 @@
|
|
|
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/
|
|
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' },
|
|
19
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
|
};
|
package/bin/solid2-kit.mjs
CHANGED
|
@@ -695,7 +695,7 @@ const CHECKS = [
|
|
|
695
695
|
pattern: /<(?:HashRouter|MemoryRouter|Route|Navigate|A|FileRoutes|StartClient|StartServer)\b/g,
|
|
696
696
|
exemptImportsFrom: TANSTACK_ROUTER_MODULE,
|
|
697
697
|
message:
|
|
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
|
|
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).',
|
|
699
699
|
},
|
|
700
700
|
{
|
|
701
701
|
id: 'meta-provider',
|
|
@@ -750,6 +750,23 @@ const CHECKS = [
|
|
|
750
750
|
message:
|
|
751
751
|
'React DOM prop. Native dblclick is onDblClick; keypress is onKeyDown; Solid has no suppressHydrationWarning.',
|
|
752
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
|
+
},
|
|
753
770
|
{
|
|
754
771
|
id: 'history-nav',
|
|
755
772
|
pattern: /\b(?:history\.pushState|history\.replaceState|window\.location\.href\s*=)/g,
|
|
@@ -1315,6 +1332,19 @@ function versionProblem(name, version, declared) {
|
|
|
1315
1332
|
return null;
|
|
1316
1333
|
}
|
|
1317
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
|
+
|
|
1318
1348
|
function installedVersion(target, name) {
|
|
1319
1349
|
try {
|
|
1320
1350
|
return JSON.parse(readFileSync(join(target, 'node_modules', name, 'package.json'), 'utf8')).version;
|
|
@@ -1342,7 +1372,12 @@ function doctorFindings(target) {
|
|
|
1342
1372
|
for (const name of VERSION_CHECKED) {
|
|
1343
1373
|
const id = SOLID2_LINE_PACKAGES.find((p) => p.name === name)?.id ?? `${name.replace('@solidjs/', '')}-version`;
|
|
1344
1374
|
const range = deps[name];
|
|
1345
|
-
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
|
+
}
|
|
1346
1381
|
const declared = versionProblem(name, range, true);
|
|
1347
1382
|
if (declared) {
|
|
1348
1383
|
report(id, `package.json pins ${name} "${range}" — ${declared}`);
|
|
@@ -68,9 +68,10 @@ Steps:
|
|
|
68
68
|
(`filter` for removal is fine — survivors keep identity; only fresh-tree wholesale
|
|
69
69
|
replacement needs `reconcile` / the derived form);
|
|
70
70
|
- hand-rolled link-active comparisons (`location.pathname === ...`) instead of the
|
|
71
|
-
automatic `aria-current` / `data-active`
|
|
71
|
+
automatic `aria-current` / `data-active` attributes (+ CSS) — `data-pending` is
|
|
72
|
+
opt-in via `createRouter({ links: pendingLinks })` — or
|
|
72
73
|
`useLinkState` / `useIsRouting`; hand-rolled query parsing instead of
|
|
73
|
-
`useSearchParams`;
|
|
74
|
+
`useSearchParams`; <!-- upstream:pending-links-opt-in -->
|
|
74
75
|
- search inputs bound to a held value without `latest`, effect-based debounce, fully
|
|
75
76
|
controlled forms without `name`s, `{ success: false }` result objects instead of
|
|
76
77
|
thrown failures, or router forms without server-side validation (`throw respond(...)`)
|
|
@@ -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: "client"` on the memo/signal/derived store, not a `clientOnly` component split. Router link state is
|
|
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).
|
|
@@ -200,7 +207,11 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
200
207
|
updated *synchronous* state or DOM. Waiting on an async memo is
|
|
201
208
|
`await resolve(() => value())` (or Testing Library async queries), not `flush()`.
|
|
202
209
|
Never call `flush()` inside an `action`, an `onSettled` callback (throws in dev), or an
|
|
203
|
-
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 -->
|
|
204
215
|
14. Do not port these React tools — they have no Solid equivalent because the problems they
|
|
205
216
|
solve don't exist: `useCallback`, `React.memo`, `forwardRef`, `useSyncExternalStore`,
|
|
206
217
|
dependency arrays, `startTransition`/`useTransition` (updates are held and coordinated
|
|
@@ -244,7 +255,9 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
244
255
|
from `@solidjs/web` — do not call `captureException` inside `<Errored fallback>`.
|
|
245
256
|
The server hook's return value replaces the sanitized error on the wire: return
|
|
246
257
|
nothing or a reference id, never the error itself (message/stack/secrets would
|
|
247
|
-
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 -->
|
|
248
261
|
Making a client store "real" is additive: same setters, wrap mutations in
|
|
249
262
|
`action`, swap in the **function form** `createOptimisticStore(() => api.list(), [])`
|
|
250
263
|
(with `refresh` after the `yield`) and a file of server functions — do
|
|
@@ -387,9 +400,14 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
387
400
|
the route is matched. One router instance —
|
|
388
401
|
Solid Router does not support nested `<Router>` / nested `createRouter`.
|
|
389
402
|
In-app navigation is `useNavigate` or `<a href={Router.paths...}>`, not
|
|
390
|
-
`window.location` / `history.pushState`. Link state
|
|
391
|
-
|
|
392
|
-
|
|
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
|
|
393
411
|
`getRequestEvent()`, never a caller-supplied user id. Unscripted (no-JS) POST
|
|
394
412
|
forms use a router `action` over a `(form: FormData)` server function (the post
|
|
395
413
|
arrives as one `FormData`; other signatures make `action={save}` TS2322) —
|
|
@@ -501,22 +519,29 @@ directory (default `src/`).
|
|
|
501
519
|
| `startTransition`, `useTransition` | automatic held updates + `isPending` |
|
|
502
520
|
| `onError` / `catchError` | `<Errored>` or effect bundle `{ effect, error }` |
|
|
503
521
|
| `from(...)` / `observable(...)` (the solid-js helpers) | inbound: async iterable from a memo; outbound: split effect |
|
|
504
|
-
| `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 --> |
|
|
505
523
|
| `renderToStringAsync(...)` | `await renderToStream(() => <App />)` (one consumer: `pipe` / `pipeTo` / `readable`) |
|
|
506
524
|
| `clearDelegatedEvents` | delete — delegated listeners are scoped to each render root |
|
|
507
525
|
| `vite-plugin-solid` / `jsxImportSource: "solid-js"` | `@solidjs/vite-plugin` / `"jsxImportSource": "@solidjs/web"` |
|
|
508
|
-
| 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 --> |
|
|
509
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 })` |
|
|
510
528
|
| `createAsync` / `createAsyncStore` / `useSubmission` / router `json()` / `cache()` | `createMemo(() => getUser(id()))`, `useSubmissions`, `respond()` from `@solidjs/web`, `query` |
|
|
511
529
|
| `import ... from "@solidjs/start"` / `vinxi` / `h3` / `"use client"` | `GET` from `@solidjs/web/server-functions`; Solid has no `"use client"` |
|
|
512
530
|
| `render(<App />, root)` | `render(() => <App />, root)` (same for `hydrate` / `renderToString` / `renderToStream`) |
|
|
513
531
|
| `<MetaProvider>` | no provider — render `<Title>` / `<Meta>` / `<Link>` from `@solidjs/meta` anywhere |
|
|
514
|
-
| `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) |
|
|
515
533
|
| `resource.loading` / `resource.error` | `<Loading>` boundary / `<Errored>` boundary |
|
|
516
534
|
|
|
517
535
|
When unsure about any API, verify against the official Solid 2.0 docs — fetchable URLs are
|
|
518
536
|
listed in `references/official-docs.md` next to the `solid-2` skill. Do not guess from
|
|
519
|
-
Solid 1.x or React memory.
|
|
537
|
+
Solid 1.x or React memory. Some 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 -->, 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 -->, and the performance guide and
|
|
543
|
+
attribution reference give `[WIDE_SCOPE_DEPS]` a threshold of 30 (the default
|
|
544
|
+
`wideDeps` is 200). <!-- upstream:docs-wide-deps-stale -->
|
|
520
545
|
|
|
521
546
|
`createRenderEffect` exists in Solid 2 but is not a default. `storePath`
|
|
522
547
|
(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,7 +69,7 @@ Most React `useEffect` code should NOT become `createEffect`:
|
|
|
69
69
|
| Inspect, count, or iterate children | `children(() => props.children)` then `.toArray()` |
|
|
70
70
|
| Code-split a component | `lazy(() => import("./X"))` read under `<Loading>` |
|
|
71
71
|
| Named export from a lazy module | `lazy(() => import("./pages"), { export: "About" })` |
|
|
72
|
-
| Pick a component/tag from reactive state | `dynamic(() => ...)` from `@solidjs/web` (stable identity; `<Dynamic>` is deprecated) <!-- 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)`) |
|
|
@@ -268,6 +268,13 @@ is **deprecated** since 2.0.0-rc.9 — `@deprecated` in the types, no runtime wa
|
|
|
268
268
|
still shipped in 2.0. <!-- upstream:dynamic-deprecated --> Hoist a `dynamic()` per component instance (or per module for a
|
|
269
269
|
constant tag) instead.
|
|
270
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
|
+
|
|
271
278
|
### Two-phase effect (imperative boundary only)
|
|
272
279
|
|
|
273
280
|
```tsx
|
|
@@ -331,7 +338,12 @@ const [signInError, setSignInError] = createSignal<string | null>(() => {
|
|
|
331
338
|
```
|
|
332
339
|
|
|
333
340
|
The writable form spells exactly this: derive from the source, allow a local
|
|
334
|
-
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
|
|
335
347
|
`previousX` comparison reset during render for the same behavior.)
|
|
336
348
|
|
|
337
349
|
### Async data — fetch high, block low
|
|
@@ -507,7 +519,11 @@ rendered), `render` / `client` (a `<Loading>` fragment rejected, client re-rende
|
|
|
507
519
|
hydration value would not serialize), `server-function` / `thrown` (`direct: true` for
|
|
508
520
|
an in-process call during SSR), `server-function` / `channel` (a rejection or throw
|
|
509
521
|
escaping through the result graph — a promise, an iterable, or a stream — after the
|
|
510
|
-
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
|
|
511
527
|
registers this hook must load before the server graph — put it in the plugin's
|
|
512
528
|
`start: { instrument: "./src/instrument.ts" }`, not at the top of an entry (see
|
|
513
529
|
[Production observability](#production-observability-the-observe-build)).
|
|
@@ -532,29 +548,65 @@ and thrown errors reach `<Errored>`. Integration layers should return a plain ac
|
|
|
532
548
|
```tsx
|
|
533
549
|
import { createMemo, onCleanup } from 'solid-js';
|
|
534
550
|
|
|
535
|
-
function createSubscriptionQuery<T>(
|
|
536
|
-
|
|
551
|
+
function createSubscriptionQuery<A, T>(
|
|
552
|
+
args: () => A,
|
|
553
|
+
subscribe: (args: A, next: (value: T) => void, fail: (error: unknown) => void) => () => void,
|
|
554
|
+
name: string,
|
|
537
555
|
) {
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
556
|
+
return createMemo(() => {
|
|
557
|
+
const resolved = args(); // tracked, and read before any await: a change re-subscribes
|
|
558
|
+
let current: T | undefined;
|
|
559
|
+
let version = 0;
|
|
560
|
+
let failure: unknown;
|
|
561
|
+
let disposed = false;
|
|
562
|
+
let wake = () => {};
|
|
563
|
+
const unsubscribe = subscribe(
|
|
564
|
+
resolved,
|
|
565
|
+
(value) => { current = value; version += 1; failure = undefined; wake(); },
|
|
566
|
+
(error) => { failure = error; wake(); },
|
|
567
|
+
);
|
|
568
|
+
// Inside a computation, onCleanup runs before the next run and on disposal.
|
|
569
|
+
onCleanup(() => { disposed = true; unsubscribe(); wake(); });
|
|
570
|
+
|
|
571
|
+
return (async function* () {
|
|
572
|
+
let seen = 0;
|
|
573
|
+
while (!disposed) {
|
|
574
|
+
if (failure !== undefined) throw failure;
|
|
575
|
+
if (version > seen) { seen = version; yield current as T; continue; }
|
|
576
|
+
await new Promise<void>((resolve) => { wake = resolve; });
|
|
577
|
+
}
|
|
578
|
+
})();
|
|
579
|
+
}, { name }); // e.g. `query:${functionName}` — attribution tables name rows by it
|
|
555
580
|
}
|
|
556
581
|
```
|
|
557
582
|
|
|
583
|
+
Open the subscription **inside the compute**, not around the memo. Then a change of
|
|
584
|
+
`args` closes the old subscription and opens the new one, `refresh(query)` and
|
|
585
|
+
`<Errored>`'s `reset` re-run the compute into a fresh subscription with the failure
|
|
586
|
+
cleared (a subscription opened outside the memo keeps its failure, so a retry never
|
|
587
|
+
recovers), and unmounting the owner closes it. Results that are snapshots (query
|
|
588
|
+
results) conflate to the latest value, as above; an event stream where every message
|
|
589
|
+
matters keeps a queue instead. A memo's compute runs **when it is created**, not on
|
|
590
|
+
first read (only `{ lazy: true }` defers it, and that also tears the memo down whenever
|
|
591
|
+
nothing observes it), so a subscription memo created in a provider connects even if
|
|
592
|
+
nothing reads it — resolve the client inside the compute and return a never-settling
|
|
593
|
+
iterable when there is none, rather than constructing it at module scope.
|
|
594
|
+
|
|
595
|
+
Identity-like subscriptions (the session, the current user) are app-wide state
|
|
596
|
+
(rule 11): create them **once**, in a provider at the root. One per page is torn down
|
|
597
|
+
on unmount and re-opened on mount, so every navigation starts with a fallback for one
|
|
598
|
+
round trip, and a page query that waits on it behind its own boundary is a sequential
|
|
599
|
+
pair — `[ASYNC_WATERFALL]` under attribution.
|
|
600
|
+
|
|
601
|
+
Clients that share one subscription per query-and-args and hand its current value to
|
|
602
|
+
every new subscriber (Convex, most reactive database clients) prefetch by subscribing
|
|
603
|
+
**early**: in the route's `preload` on link intent, held open for a short grace period
|
|
604
|
+
(seconds) and released at once on error, so a hover while signed out leaves no failure
|
|
605
|
+
for the page to inherit. Skip `intent === "initial"` there — on the first match the page
|
|
606
|
+
subscribes on mount anyway, and an identity-gated read would run before the session is
|
|
607
|
+
restored. Nothing is cached outside the client; the page's own subscription is the only
|
|
608
|
+
one left when the hold expires.
|
|
609
|
+
|
|
558
610
|
Server-function `live()` sources are the same shape (async iterable → memo). Connection
|
|
559
611
|
state is `source.onstatus` (`"connected"` / `"reconnecting"` / `"closed"`), not a field
|
|
560
612
|
in the yielded value — see [Server functions](#server-functions--use-server).
|
|
@@ -783,6 +835,30 @@ Never `{todos().map((t) => <Row todo={t} />)}` and never `<For each={todos().map
|
|
|
783
835
|
The `.map` form *renders*; it just rebuilds every row. `children().toArray().map` is
|
|
784
836
|
fine — those nodes are already resolved.
|
|
785
837
|
|
|
838
|
+
Join and narrow **upstream of the list**, not per row. A row that narrows its own
|
|
839
|
+
lookup — `<For each={layout().cards}>{(card) => <Show when={byKey().get(card().key)}>…}` —
|
|
840
|
+
makes every row a reactive source of the list's insert effect (one per row; a plain
|
|
841
|
+
element or component row costs none), so every row change re-runs one effect that
|
|
842
|
+
tracks the whole list. Nothing flags it below 2000 rows: `[WIDE_SCOPE_DEPS]` skips that
|
|
843
|
+
insert pass by design (and defaults to 200 sources), and only the always-on
|
|
844
|
+
`[HUGE_FAN_IN]` reports it, from 2000 — so this is a review rule, not something a
|
|
845
|
+
diagnostics gate catches. <!-- upstream:per-row-show-insert-sources --> Build the joined
|
|
846
|
+
rows in one memo and iterate those; `flatMap` drops the misses without a `!`:
|
|
847
|
+
|
|
848
|
+
```tsx
|
|
849
|
+
const cards = createMemo(() => {
|
|
850
|
+
const byKey = new Map(props.matches.map((m) => [m.key, m]));
|
|
851
|
+
return layout().cards.flatMap((card) => {
|
|
852
|
+
const match = byKey.get(card.key);
|
|
853
|
+
return match === undefined ? [] : [{ card, match }];
|
|
854
|
+
});
|
|
855
|
+
}, { name: "cards" });
|
|
856
|
+
|
|
857
|
+
<For each={cards()} keyed={(row) => row.card.key}>
|
|
858
|
+
{(row) => <MatchCard match={row().match} x={row().card.x} />}
|
|
859
|
+
</For>
|
|
860
|
+
```
|
|
861
|
+
|
|
786
862
|
`<Repeat from={start()} count={20}>{(index) => ...}</Repeat>` renders by absolute index with
|
|
787
863
|
no array diffing — use for fixed slot counts and virtual scrolling windows.
|
|
788
864
|
|
|
@@ -965,7 +1041,12 @@ onSettled(() => {
|
|
|
965
1041
|
|
|
966
1042
|
Effect callbacks follow the same shape: return the cleanup from `apply` (an
|
|
967
1043
|
`onCleanup` there never runs), and `flush()` inside one is a no-op
|
|
968
|
-
(`[FLUSH_IN_EFFECT_CALLBACK]`).
|
|
1044
|
+
(`[FLUSH_IN_EFFECT_CALLBACK]`). Creating a memo, an effect, or a root inside `apply`
|
|
1045
|
+
throws in dev (`[PRIMITIVE_IN_EFFECT_CALLBACK]` — the effect phase has no owner to
|
|
1046
|
+
dispose them; `runWithOwner(null, …)` does not lift it): create them in the
|
|
1047
|
+
component body or the compute phase, or attach them to an owner captured with
|
|
1048
|
+
`getOwner()` in the body via `runWithOwner(owner, …)`.
|
|
1049
|
+
<!-- upstream:primitive-in-effect-callback -->
|
|
969
1050
|
|
|
970
1051
|
### Per-value SSR policy: `ssrSource`
|
|
971
1052
|
|
|
@@ -1057,9 +1138,11 @@ Testing Library's `render(…, { location })` is typed but not implemented (1.0.
|
|
|
1057
1138
|
`jsxImportSource` for tests and app code is `"@solidjs/web"`, not `"solid-js"`.
|
|
1058
1139
|
Vite plugin is `@solidjs/vite-plugin`, not `vite-plugin-solid`.
|
|
1059
1140
|
|
|
1060
|
-
To turn those dev diagnostics into a hard gate,
|
|
1061
|
-
|
|
1062
|
-
|
|
1141
|
+
To turn those dev diagnostics into a hard gate on every test, capture them per test
|
|
1142
|
+
with `@solidjs/diagnostics` — see
|
|
1143
|
+
[Gating every test on diagnostics](#gating-every-test-on-diagnostics). Patching
|
|
1144
|
+
`console.warn` sees only what the console reporter prints, and none of the
|
|
1145
|
+
attribution-tier findings or the hold tables.
|
|
1063
1146
|
|
|
1064
1147
|
### Dev diagnostics and attribution
|
|
1065
1148
|
|
|
@@ -1187,7 +1270,9 @@ expect(artifact).toHaveNoDiagnostics();
|
|
|
1187
1270
|
expect(artifact).toHaveNoSilentHolds();
|
|
1188
1271
|
```
|
|
1189
1272
|
|
|
1190
|
-
`toHaveNoDiagnostics` fails on
|
|
1273
|
+
`toHaveNoDiagnostics` fails on every captured finding, `info` ones included
|
|
1274
|
+
(`[FALLBACK_FLASH]`, `[OPTIMISTIC_REVERTED]`, …) — its `allow` list names codes to
|
|
1275
|
+
tolerate;
|
|
1191
1276
|
`toStayWithinRerunBudget` / `toHaveNoWaste` catch recompute regressions. The
|
|
1192
1277
|
`agent-loops` skill the package ships
|
|
1193
1278
|
(`node_modules/@solidjs/diagnostics/skills/agent-loops/SKILL.md`) documents the agent
|
|
@@ -1195,6 +1280,101 @@ verification loops: capture until the channel is quiet, scenario budgets as the
|
|
|
1195
1280
|
definition of done, and hold/latency gates. See the debugging-reactivity guide in
|
|
1196
1281
|
[references/official-docs.md](references/official-docs.md).
|
|
1197
1282
|
|
|
1283
|
+
### Gating every test on diagnostics
|
|
1284
|
+
|
|
1285
|
+
Solid grades its findings `info` / `warn` / `error` and offers no switch to escalate
|
|
1286
|
+
them, so "diagnostics are errors" is enforced in tests. The shape that holds up is a
|
|
1287
|
+
setup file that wraps **every** test body in one capture, so a test written by someone
|
|
1288
|
+
who never heard of the engine is gated anyway:
|
|
1289
|
+
|
|
1290
|
+
```ts
|
|
1291
|
+
// src/test-setup.ts — listed in the vitest project's `setupFiles`
|
|
1292
|
+
import { afterEach, beforeEach } from 'vitest';
|
|
1293
|
+
import { flush } from 'solid-js';
|
|
1294
|
+
import { captureArtifact, expectNoDiagnostics, expectNoSilentHolds } from '@solidjs/diagnostics';
|
|
1295
|
+
|
|
1296
|
+
let end: (() => void) | undefined;
|
|
1297
|
+
let capture: ReturnType<typeof captureArtifact<void>> | undefined;
|
|
1298
|
+
export const roots: (() => void)[] = []; // a `mount()` helper pushes each render's dispose
|
|
1299
|
+
|
|
1300
|
+
beforeEach((context) => {
|
|
1301
|
+
if (capture) throw new Error('tests under the gate must run sequentially');
|
|
1302
|
+
capture = captureArtifact<void>(() => new Promise<void>((resolve) => { end = resolve; }), {
|
|
1303
|
+
scenario: context.task.name,
|
|
1304
|
+
attribution: { hotTime: false, fallbackFlashes: false }, // wall-clock: see below
|
|
1305
|
+
});
|
|
1306
|
+
});
|
|
1307
|
+
|
|
1308
|
+
afterEach(async () => {
|
|
1309
|
+
for (const dispose of roots.splice(0).reverse()) dispose();
|
|
1310
|
+
flush();
|
|
1311
|
+
end?.();
|
|
1312
|
+
const pending = capture;
|
|
1313
|
+
capture = undefined;
|
|
1314
|
+
if (pending === undefined) throw new Error('this test ran outside the gate');
|
|
1315
|
+
const { artifact } = await pending; // close the capture before asserting anything
|
|
1316
|
+
expectNoDiagnostics(artifact);
|
|
1317
|
+
expectNoSilentHolds(artifact);
|
|
1318
|
+
});
|
|
1319
|
+
```
|
|
1320
|
+
|
|
1321
|
+
What the shape has to get right:
|
|
1322
|
+
|
|
1323
|
+
- **A DOM environment** (`happy-dom` / `jsdom`) for every reactive test, `.test.ts`
|
|
1324
|
+
included. Under vitest's `node` environment the export conditions select Solid's
|
|
1325
|
+
**server build**: writes are inert (`[SERVER_WRITE]`), effects never run, and the
|
|
1326
|
+
attribution tables stay empty — a green test that proves nothing. Components that only
|
|
1327
|
+
exist in the server build (a `<html>` document shell) get their own `node` project
|
|
1328
|
+
rendered with `renderToString`, outside the gate.
|
|
1329
|
+
- **No HMR transform in the test project**: `solid({ refresh: { disabled: true } })`.
|
|
1330
|
+
The refresh transform wraps every component declaration in a memo, so the graph
|
|
1331
|
+
under test is not the graph production ships, and tests have no HMR to serve. (The
|
|
1332
|
+
wrappers are marked as plumbing, so the engine does not count them as sources.)
|
|
1333
|
+
<!-- upstream:per-row-show-insert-sources -->
|
|
1334
|
+
- **Two assertions, not one.** `[SILENT_HOLD]` is emitted only once a hold outlasts
|
|
1335
|
+
`holds.infoMs` (100ms), so a shorter unacknowledged hold is recorded and coded
|
|
1336
|
+
nowhere; `expectNoSilentHolds` asks the hold table, where no threshold applies.
|
|
1337
|
+
- **Wall-clock checks off where the clock means nothing.** `hotTime`'s 8ms budget is
|
|
1338
|
+
half a real browser frame; an emulated DOM spends more than that rendering a page, and
|
|
1339
|
+
a loaded runner several times more, so the failure names an innocent component.
|
|
1340
|
+
`[FALLBACK_FLASH]` measures backend latency, and a fake that settles at once flashes
|
|
1341
|
+
every fallback. Keep the count-based and structural checks (holds, waterfalls, list
|
|
1342
|
+
identity, wide scopes); measure time in a real browser.
|
|
1343
|
+
- **Sequential tests only.** The diagnostics channel and the engine are process-global,
|
|
1344
|
+
so under `test.concurrent` one test's finding lands in another's artifact — refuse the
|
|
1345
|
+
overlap rather than blame the wrong test.
|
|
1346
|
+
- **Dispose what the test mounted** before the capture closes. A root left alive re-runs
|
|
1347
|
+
on later writes, and its finding fails whichever test is open then.
|
|
1348
|
+
- **Settle deferred work** (`await resolve(...)`, `flush()`) before the body returns: an
|
|
1349
|
+
un-awaited timer or promise lands in the next test's capture, or in none. Module scope
|
|
1350
|
+
runs before the first capture opens — keep fixtures there, never a render.
|
|
1351
|
+
- **The app's own `attribution.enable()` stands down under a harness**
|
|
1352
|
+
(`if (import.meta.env.VITEST) return;`, and in the browser gate's mode). A hold taken
|
|
1353
|
+
while the engine is enabled opens a fresh window over `history()` / `costs()` /
|
|
1354
|
+
`feedback()`, which erases what the capture recorded so far, and options combine by
|
|
1355
|
+
the most demanding value per key, so the app's default hold re-arms every threshold
|
|
1356
|
+
the gate turned off. <!-- upstream:attribution-hold-window -->
|
|
1357
|
+
- **A test whose subject is a finding requires it** (`expectDiagnostic(artifact, code)`
|
|
1358
|
+
and then `allow` it), so the test fails when it stops provoking the code instead of
|
|
1359
|
+
decaying into a mute. Never `allow` a finding about real code.
|
|
1360
|
+
- **Fixtures above the threshold** for scale-dependent codes. `[WIDE_SCOPE_DEPS]` fires
|
|
1361
|
+
at 200 dependencies by default, `[HUGE_FAN_OUT]` / `[HUGE_FAN_IN]` and
|
|
1362
|
+
`[HOT_SCOPE_RERUNS]` at their own counts, so a fixture of three rows cannot falsify a
|
|
1363
|
+
per-row cost. When a gate is meant to catch one, give it a fixture past the code's
|
|
1364
|
+
threshold (or a lower threshold in the capture's `attribution` options) and check it
|
|
1365
|
+
fails with the defect re-injected. A per-row cost in a list's own insert pass is
|
|
1366
|
+
outside `[WIDE_SCOPE_DEPS]` entirely (see [Lists](#lists-for-child-signatures-per-keying-mode)).
|
|
1367
|
+
|
|
1368
|
+
An emulated DOM cannot see everything, so keep one **real-browser gate** for the main
|
|
1369
|
+
path: `captureBrowserArtifact(page, interact, { scenario })` from
|
|
1370
|
+
`@solidjs/diagnostics/playwright` drives the same capture through the bridge the Vite
|
|
1371
|
+
plugin injects into `vite dev`, and returns the same artifact, so one verdict function
|
|
1372
|
+
judges both. Serve that dev server in a dedicated mode with `refresh: { disabled: true }`
|
|
1373
|
+
and `performanceTracks: false`: the tracks paint every record from inside the scopes the
|
|
1374
|
+
engine times, enough to push a page's first render past the 8ms `[HOT_SCOPE_TIME]`
|
|
1375
|
+
budget. The wall-clock thresholds mean something there — except `fallbackFlashes`
|
|
1376
|
+
against a backend on loopback.
|
|
1377
|
+
|
|
1198
1378
|
### Development loop: self-diagnose before finishing
|
|
1199
1379
|
|
|
1200
1380
|
Enable observability in development **by default** and lean on it. The dev build
|
|
@@ -1234,6 +1414,11 @@ effects** — a pure markup or styling change does not need it.
|
|
|
1234
1414
|
observe build it records in production). Under `vite dev` the plugin also paints the
|
|
1235
1415
|
same records as tracks in the Chrome Performance panel (`performanceTracks`, on by
|
|
1236
1416
|
default; `false` opts out) — record there when a timeline reads better than tables.
|
|
1417
|
+
The six attribution cost checks (`hotRuns`, `hotTime`, `wideDeps`, `unstableMemos`,
|
|
1418
|
+
`fanOut`, `wastedRecompute`) are opt-in there: `performanceTracks: { attribution:
|
|
1419
|
+
{ checks: true } }` (or `enablePerformanceTracks({ attribution: { checks: true } })`,
|
|
1420
|
+
or your own `attribution.enable()` hold — the diagnostics-session path above already
|
|
1421
|
+
turns them on). <!-- upstream:perf-tracks-cost-checks -->
|
|
1237
1422
|
4. **Read the verdicts, then the tables.** Fix every coded warning the session raised
|
|
1238
1423
|
(`[SILENT_HOLD]`, `[IMMUTABLE_UPDATE_IN_STORE]`, `[UNSTABLE_LIST_IDENTITY]`,
|
|
1239
1424
|
`[EFFECT_RELAY_TEAR]`, `[EFFECT_WRITES_OWN_SOURCE]`, `[ASYNC_WATERFALL]`,
|
|
@@ -1398,7 +1583,8 @@ export default function App() {
|
|
|
1398
1583
|
|
|
1399
1584
|
The `<Loading>` covers the first load of the lazy pages (and lets SSR stream the shell);
|
|
1400
1585
|
later navigations hold the current page while the next one loads (links get
|
|
1401
|
-
`data-pending`
|
|
1586
|
+
`data-pending` when the router is created with `links: pendingLinks`
|
|
1587
|
+
<!-- upstream:pending-links-opt-in -->). Create the instance at **module scope**. Nested layouts are `children` arrays
|
|
1402
1588
|
on the route objects, not nested `<Route>` / nested routers. Solid Router does
|
|
1403
1589
|
**not** support nested `<Router>` instances — compose one route tree (or a lazy
|
|
1404
1590
|
`children` thunk). Navigate with
|
|
@@ -1461,13 +1647,25 @@ Link warming: `preload="false"` skips that link's *data* preload; `preloadLinks:
|
|
|
1461
1647
|
disables automatic link preloading. Intent values are `"initial"` / `"navigate"` /
|
|
1462
1648
|
`"native"` / `"preload"`. One router per app.
|
|
1463
1649
|
|
|
1464
|
-
Link state needs no component code: the router sets `aria-current="page"` (
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1650
|
+
Link state needs no component code: the router sets `aria-current="page"` (same path
|
|
1651
|
+
and same query — parameter order and hash aside), `data-active` (exact or descendant
|
|
1652
|
+
on the path only — the `/` link is active only on exact match), and, with the opt-in
|
|
1653
|
+
`createRouter({ links: pendingLinks })` plugin, `data-pending` (in-flight navigation
|
|
1654
|
+
target) on the anchors it handles; style them in CSS. The plugin is also a *reader* of
|
|
1655
|
+
the in-flight navigation: without it, or a `useLinkState().pending` / `isPending` read of
|
|
1656
|
+
the location somewhere on screen, nothing reads it, so every navigation that waits on a
|
|
1657
|
+
lazy page or its first data is an unacknowledged hold (`[SILENT_HOLD]` past 100ms,
|
|
1658
|
+
shorter ones only in `toHaveNoSilentHolds`). Create the router with
|
|
1659
|
+
`links: pendingLinks` by default, even when no stylesheet uses `data-pending` yet. <!-- upstream:pending-links-opt-in --> Highlight nav links on
|
|
1660
|
+
`data-active`, not `aria-current`: a nav link to `/products` is not current on
|
|
1661
|
+
`/products?page=2`. An `aria-current` you write
|
|
1662
|
+
yourself is yours — the router only manages the attribute on links where it set it.
|
|
1663
|
+
For non-anchors, or state needed in JSX, `useLinkState(() => href,
|
|
1664
|
+
{ end })` returns the same three as accessors (`active` / `current` / `pending` —
|
|
1665
|
+
`pending` works without the plugin); `useIsRouting()` is the page-level pending
|
|
1666
|
+
flag for progress bars. Hand-rolled
|
|
1470
1667
|
`location.pathname === ...` comparisons are the wrong tool.
|
|
1668
|
+
<!-- upstream:pending-links-opt-in -->
|
|
1471
1669
|
|
|
1472
1670
|
Build URLs from `paths`: static segments are properties (`Router.paths.about`, never
|
|
1473
1671
|
`Router.paths.about()`), parameterized segments are calls (`paths.products(id)`),
|
|
@@ -1860,6 +2058,7 @@ how to reuse. Prefer the form on the right.
|
|
|
1860
2058
|
| `lazy(() => import("./p").then((m) => ({ default: m.About })))` | `lazy(() => import("./p"), { export: "About" })` |
|
|
1861
2059
|
| `dangerouslySetInnerHTML={{ __html }}` | `innerHTML={html()}` (sanitized); never with JSX children |
|
|
1862
2060
|
| `onClick={setCount}` | `onClick={() => setCount((c) => c + 1)}` |
|
|
2061
|
+
| `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 --> |
|
|
1863
2062
|
| `event.target.value` | `event.currentTarget.value` |
|
|
1864
2063
|
| `createContext(emptyStore)` for reactive state | `createContext<Todos>()` — dummy defaults silently no-op |
|
|
1865
2064
|
| `<For keyed={(t) => t.id}>{(todo) => todo.title}` | `todo().title` — key-function items are accessors |
|
|
@@ -1911,7 +2110,7 @@ how to reuse. Prefer the form on the right.
|
|
|
1911
2110
|
| `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) |
|
|
1912
2111
|
| `<form method="get" action={update.url}>` for a mutation | GET forms only for idempotent search; mutations are POST |
|
|
1913
2112
|
| `window.location.href = ...` / `history.pushState` | `useNavigate()` or `<a href={Router.paths...}>` |
|
|
1914
|
-
| `class={{ active: location.pathname === "/about" }}` hand-rolled link state | CSS on
|
|
2113
|
+
| `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 --> |
|
|
1915
2114
|
| `new URLSearchParams(location.search)` hand-rolled query parsing | `useSearchParams()` (setter merges, no scroll); typed via a route `search` schema + `useSearchParams(Router.paths.search)` |
|
|
1916
2115
|
| `Router.paths.about()` / string-concatenated URLs | static nodes are properties (`Router.paths.about`); calls bind params, then search object and hash |
|
|
1917
2116
|
| `httpStatus(404)` in a click handler | call it bare in the component / error-fallback body (a scope declaration, not a mutation) |
|
|
@@ -1932,6 +2131,8 @@ how to reuse. Prefer the form on the right.
|
|
|
1932
2131
|
| `Object.assign({}, props)` / expecting `merge` to skip `undefined` like 1.x `mergeProps` | `omit` / `merge` — explicit `undefined` **overrides** (Object.assign) |
|
|
1933
2132
|
| `isPending(user())` / `latest(user())` | `isPending(user)` / `latest(user)` — pass the accessor (or `() => user()`). Calling it first evaluates the read before the helper runs |
|
|
1934
2133
|
| `isPending(...)` as the first-load spinner | `<Loading>` owns first flight; `isPending` is the *refetch* indicator after a settled answer exists |
|
|
2134
|
+
| `<For each={rows()}>{(row) => <Show when={byKey().get(row().key)}>…</Show>}</For>` (narrowing per row) | join in one memo upstream and iterate the joined rows (`flatMap` drops misses) — a per-row `<Show>` makes every row a source of the list's insert effect, which no diagnostic flags below `[HUGE_FAN_IN]`'s 2000 <!-- upstream:per-row-show-insert-sources --> |
|
|
2135
|
+
| a subscription opened beside `createMemo(() => iterate())` instead of inside its compute | open it inside the compute with `onCleanup` there — args changes re-subscribe, `refresh` / `reset` retry with a fresh subscription, unmount closes it |
|
|
1935
2136
|
| a held write with no feedback anywhere | pair it with `isPending()` / `latest()` / an optimistic value / `affects()` — otherwise dev + attribution reports `[SILENT_HOLD]` |
|
|
1936
2137
|
| `refresh(getUser.key)` / `revalidate(user)` / `return refresh()` from `"use server"` | three APIs: core `refresh(source)` reruns a reactive source; router `revalidate(getUser.key)` invalidates the query cache; `return reload({ revalidate: "todos" })` asks the integration to refresh cached data |
|
|
1937
2138
|
| `return redirect(...)` as the only redirect shape | guards `throw redirect("/sign-in")`; `redirect()` / `respond()` take `revalidate` too when the mutation also moves or returns a value |
|
|
@@ -2092,6 +2293,20 @@ agent use the mirror, which serves every page as plain markdown:
|
|
|
2092
2293
|
|
|
2093
2294
|
A curated URL list is in [references/official-docs.md](references/official-docs.md).
|
|
2094
2295
|
|
|
2296
|
+
Known stale pages — this skill is verified against the installed release and wins:
|
|
2297
|
+
|
|
2298
|
+
- `routing/solid-router/navigation` lists `data-pending` among the attributes the router
|
|
2299
|
+
sets on its own; it needs `createRouter({ links: pendingLinks })`, and `aria-current`
|
|
2300
|
+
also compares the query (the link-state paragraph under Solid Router 2 above).
|
|
2301
|
+
<!-- upstream:docs-pending-links-stale -->
|
|
2302
|
+
- `reference/solid-js/reactivity/create-signal` says a write to a writable derivation
|
|
2303
|
+
wins over a same-tick recompute; a source change in the same update re-runs the
|
|
2304
|
+
function, which receives the write as `prev`. <!-- upstream:docs-derived-write-stale -->
|
|
2305
|
+
- `guides/performance` ("tracks 30 or more sources") and the attribution reference
|
|
2306
|
+
(`wideDeps`, "default 30") give the old `[WIDE_SCOPE_DEPS]` threshold; the default is
|
|
2307
|
+
200, and a list's insert pass is exempt from the check (see
|
|
2308
|
+
[Lists](#lists-for-child-signatures-per-keying-mode)). <!-- upstream:docs-wide-deps-stale -->
|
|
2309
|
+
|
|
2095
2310
|
**Never** consult Solid 1.x sources (docs.solidjs.com, pre-2.0 tutorials, old Stack Overflow).
|
|
2096
2311
|
Solid 2.0 is a breaking rewrite; 1.x answers are wrong here. The banned-API table is in the
|
|
2097
2312
|
always-applied rules installed alongside this skill.
|
|
@@ -2170,14 +2385,18 @@ always-applied rules installed alongside this skill.
|
|
|
2170
2385
|
`feedback`) whenever the change touched stores, lists, async, actions, or
|
|
2171
2386
|
effects. Name interrogated scopes (`{ name: "total" }`). `observe: true` is a
|
|
2172
2387
|
production opt-in, not a dev step.
|
|
2388
|
+
- [ ] Tests that render run under a DOM environment with the HMR transform off and a
|
|
2389
|
+
per-test capture asserting `expectNoDiagnostics` + `expectNoSilentHolds`; app code
|
|
2390
|
+
does not call `attribution.enable()` under the harness. List rows do not narrow
|
|
2391
|
+
themselves with a per-row `<Show>` (no diagnostic catches it).
|
|
2173
2392
|
- [ ] If the project has a router: `createRouter({ routes })`, not JSX `<Route>` / `<A>`.
|
|
2174
2393
|
One instance, no nested `<Router>`. Navigate with `useNavigate` / `<a href>`, not
|
|
2175
2394
|
`window.location`. Router `action`/`query` come from `@solidjs/router` (POST forms + cache), not
|
|
2176
2395
|
core `action`/`refresh`. Forms: `<form action={save} method="post">`. Core `action`
|
|
2177
2396
|
bodies are generators (`function*` / `async function*`) suspending on `yield`,
|
|
2178
2397
|
never plain `async` functions. Link state is automatic attributes + CSS
|
|
2179
|
-
(`aria-current` / `data-active
|
|
2180
|
-
in JSX). Search params via `useSearchParams`. Forms validate on the server
|
|
2398
|
+
(`aria-current` / `data-active`, `data-pending` via `links: pendingLinks`;
|
|
2399
|
+
`useLinkState` / `useIsRouting` in JSX). <!-- upstream:pending-links-opt-in --> Search params via `useSearchParams`. Forms validate on the server
|
|
2181
2400
|
(`throw respond({ issues }, { status: 400 })`) with inline errors from
|
|
2182
2401
|
`useSubmissions`.
|
|
2183
2402
|
- [ ] `render(() => <App />, root)` — a function, not `render(<App />)`. `hydrate` when
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "solid2-agent-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.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",
|