solid2-agent-kit 0.19.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 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.13' },
15
- '@solidjs/web': { line: 2, baseline: '2.0.0-rc.13' },
16
- '@solidjs/signals': { line: 2, baseline: '2.0.0-rc.13' },
17
- '@solidjs/diagnostics': { line: 2, baseline: '2.0.0-rc.13' },
18
- '@solidjs/router': { line: 2, baseline: '2.0.0-next.34' },
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
  };
@@ -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/data-pending + CSS, or useLinkState).',
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') continue;
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` / `data-pending` attributes (+ CSS) or
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 automatic (`aria-current`/`data-active`/`data-pending` + CSS; `useLinkState`/`useIsRouting` in JSX); search params via `useSearchParams`; router forms validate on the server (`throw respond({issues},{400})`) with inline errors from `useSubmissions`. Confirmations on another channel — fire-and-forget sends, and mutations whose result is read through `live()` — wait with `yield until(predicate, {timeout})`, not `refresh()`. Declare `live(GET(fn))` (`live()` outermost). Debounce at the handler; inputs feeding a request bind `latest(query)`. Dev diagnostics are errors: `[STRICT_READ_UNTRACKED]` means a read outside a tracking scope, `[ASYNC_STORE_SETTER]` means a store setter callback awaited (the draft is a synchronous transaction — await first, then write) <!-- upstream:async-store-setter-throws -->, and a held write needs `isPending`/`latest`/optimistic/`affects` feedback or dev + attribution reports `[SILENT_HOLD]`. Error monitoring is `configureClientErrors` / `configureServerErrors` (or per root `render(fn, root, undefined, { onError })` — options are the 4th argument), not a side effect inside `<Errored fallback>`; the server hook must return a reference or nothing, never the error itself. `<Dynamic>` is deprecated — use `dynamic()`. <!-- upstream:dynamic-deprecated --> Observability is on by default in development: whenever a change touches stores, lists, async, actions, or effects, keep the dev console free of `[CODE]` findings and run an attribution session (`/__solid/diagnostics` `begin` → interact → `whyDidRun` / `costs` → `end` with `@solidjs/diagnostics` installed, otherwise `isDev`-guarded `attribution.enable({ log: false })` + `why()` / `costs()`) before finishing. `vite dev` is always the dev build; `solid({ observe: true })` is a production opt-in.
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. The
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 is automatic
391
- (`aria-current` / `data-active` / `data-pending` + CSS; `useLinkState` /
392
- `useIsRouting` in JSX) — never hand-rolled `location.pathname` comparisons. Trusted identity is
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,27 @@ 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` / `data-pending` + CSS (`useLinkState` in JSX) |
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. 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 -->
520
543
 
521
544
  `createRenderEffect` exists in Solid 2 but is not a default. `storePath`
522
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,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. (React needs `useState(props.x)` plus a
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). A monitoring SDK's `init()` that
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)).
@@ -965,7 +981,12 @@ onSettled(() => {
965
981
 
966
982
  Effect callbacks follow the same shape: return the cleanup from `apply` (an
967
983
  `onCleanup` there never runs), and `flush()` inside one is a no-op
968
- (`[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 -->
969
990
 
970
991
  ### Per-value SSR policy: `ssrSource`
971
992
 
@@ -1234,6 +1255,11 @@ effects** — a pure markup or styling change does not need it.
1234
1255
  observe build it records in production). Under `vite dev` the plugin also paints the
1235
1256
  same records as tracks in the Chrome Performance panel (`performanceTracks`, on by
1236
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 -->
1237
1263
  4. **Read the verdicts, then the tables.** Fix every coded warning the session raised
1238
1264
  (`[SILENT_HOLD]`, `[IMMUTABLE_UPDATE_IN_STORE]`, `[UNSTABLE_LIST_IDENTITY]`,
1239
1265
  `[EFFECT_RELAY_TEAR]`, `[EFFECT_WRITES_OWN_SOURCE]`, `[ASYNC_WATERFALL]`,
@@ -1398,7 +1424,8 @@ export default function App() {
1398
1424
 
1399
1425
  The `<Loading>` covers the first load of the lazy pages (and lets SSR stream the shell);
1400
1426
  later navigations hold the current page while the next one loads (links get
1401
- `data-pending`). Create the instance at **module scope**. Nested layouts are `children` arrays
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
1402
1429
  on the route objects, not nested `<Route>` / nested routers. Solid Router does
1403
1430
  **not** support nested `<Router>` instances — compose one route tree (or a lazy
1404
1431
  `children` thunk). Navigate with
@@ -1461,13 +1488,20 @@ Link warming: `preload="false"` skips that link's *data* preload; `preloadLinks:
1461
1488
  disables automatic link preloading. Intent values are `"initial"` / `"navigate"` /
1462
1489
  `"native"` / `"preload"`. One router per app.
1463
1490
 
1464
- Link state needs no component code: the router sets `aria-current="page"` (exact
1465
- match), `data-active` (exact or descendant — the `/` link is active only on exact
1466
- match), and `data-pending` (in-flight navigation target) on the anchors it handles;
1467
- style them in CSS. For non-anchors, or state needed in JSX, `useLinkState(() => href,
1468
- { end })` returns the same three as accessors (`active` / `current` / `pending`);
1469
- `useIsRouting()` is the page-level pending flag for progress bars. Hand-rolled
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
1470
1503
  `location.pathname === ...` comparisons are the wrong tool.
1504
+ <!-- upstream:pending-links-opt-in -->
1471
1505
 
1472
1506
  Build URLs from `paths`: static segments are properties (`Router.paths.about`, never
1473
1507
  `Router.paths.about()`), parameterized segments are calls (`paths.products(id)`),
@@ -1860,6 +1894,7 @@ how to reuse. Prefer the form on the right.
1860
1894
  | `lazy(() => import("./p").then((m) => ({ default: m.About })))` | `lazy(() => import("./p"), { export: "About" })` |
1861
1895
  | `dangerouslySetInnerHTML={{ __html }}` | `innerHTML={html()}` (sanitized); never with JSX children |
1862
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 --> |
1863
1898
  | `event.target.value` | `event.currentTarget.value` |
1864
1899
  | `createContext(emptyStore)` for reactive state | `createContext<Todos>()` — dummy defaults silently no-op |
1865
1900
  | `<For keyed={(t) => t.id}>{(todo) => todo.title}` | `todo().title` — key-function items are accessors |
@@ -1911,7 +1946,7 @@ how to reuse. Prefer the form on the right.
1911
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) |
1912
1947
  | `<form method="get" action={update.url}>` for a mutation | GET forms only for idempotent search; mutations are POST |
1913
1948
  | `window.location.href = ...` / `history.pushState` | `useNavigate()` or `<a href={Router.paths...}>` |
1914
- | `class={{ active: location.pathname === "/about" }}` hand-rolled link state | CSS on automatic `aria-current` / `data-active` / `data-pending`; `useLinkState` in JSX, `useIsRouting()` for progress bars |
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 --> |
1915
1950
  | `new URLSearchParams(location.search)` hand-rolled query parsing | `useSearchParams()` (setter merges, no scroll); typed via a route `search` schema + `useSearchParams(Router.paths.search)` |
1916
1951
  | `Router.paths.about()` / string-concatenated URLs | static nodes are properties (`Router.paths.about`); calls bind params, then search object and hash |
1917
1952
  | `httpStatus(404)` in a click handler | call it bare in the component / error-fallback body (a scope declaration, not a mutation) |
@@ -2092,6 +2127,16 @@ agent use the mirror, which serves every page as plain markdown:
2092
2127
 
2093
2128
  A curated URL list is in [references/official-docs.md](references/official-docs.md).
2094
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
+
2095
2140
  **Never** consult Solid 1.x sources (docs.solidjs.com, pre-2.0 tutorials, old Stack Overflow).
2096
2141
  Solid 2.0 is a breaking rewrite; 1.x answers are wrong here. The banned-API table is in the
2097
2142
  always-applied rules installed alongside this skill.
@@ -2176,8 +2221,8 @@ always-applied rules installed alongside this skill.
2176
2221
  core `action`/`refresh`. Forms: `<form action={save} method="post">`. Core `action`
2177
2222
  bodies are generators (`function*` / `async function*`) suspending on `yield`,
2178
2223
  never plain `async` functions. Link state is automatic attributes + CSS
2179
- (`aria-current` / `data-active` / `data-pending`; `useLinkState` / `useIsRouting`
2180
- 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
2181
2226
  (`throw respond({ issues }, { status: 400 })`) with inline errors from
2182
2227
  `useSubmissions`.
2183
2228
  - [ ] `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.19.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",