solid2-agent-kit 0.22.1 → 0.23.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/baseline.mjs +11 -10
- package/bin/solid2-kit.mjs +47 -10
- package/files/commands/solid2-review.md +15 -8
- package/files/shared/agents-section.md +1 -1
- package/files/shared/rules-body.md +66 -34
- package/files/skills/solid-2/SKILL.md +164 -83
- package/files/skills/solid-2/references/official-docs.md +6 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ Supported agents: **Cursor** and **Claude Code**.
|
|
|
16
16
|
| Always-applied hard rules | `.cursor/rules/solid-2.mdc` (glob-attached to `*.tsx`/`*.jsx`) | managed block in `CLAUDE.md` | 23 hard rules + banned Solid 1.x API table + eslint-plugin-solid lint-heritage table. Injected without the agent having to decide to read anything |
|
|
17
17
|
| Agent skill | `.cursor/skills/solid-2/` | `.claude/skills/solid-2/` | Execution-model primer, decision tables, canonical patterns, a “still runs / write the other form” table, review checklist, official-doc URL index |
|
|
18
18
|
| Shared agent context | managed block in `AGENTS.md` | same (Claude Code users can reference it from `CLAUDE.md`) | Core principles and pointers, always in context |
|
|
19
|
-
| Mechanical gate | `solid2-kit check` | same | Comment-stripped regex guard over `src/**/*.{ts,tsx,jsx}` (or explicit `[paths...]` — files are checked, directories are walked, a missing path fails): fails on props destructuring (including multi-line signatures), `{ ...props }` rest copies, `{list().map(...)}` in JSX, React imports/hooks/JSX props/`React.lazy`/`useOptimistic`/`onDoubleClick`, Solid 1.x imports/APIs/components/JSX namespaces (valueless `use:x` included), Solid 1.x `createEffect(fn)` / `createEffect(fn, init)` / `createMemo(fn, init)` signatures, `const [x] = createProjection(...)` (it returns the store, not a tuple), `vite-plugin-solid`, old-router `<Route>`/`<A>`/`<HashRouter>`/`<FileRoutes>`, `MetaProvider`, `Context.Provider`, camelCase style keys, `key` props (except on `@solidjs/meta` tags), `value()!` hand-narrowing, hand-rolled loading signals, `=== undefined` readiness branches, SolidStart leftovers (`@solidjs/start`, `h3`, `createAsync`, `useSubmission`, `"use client"`), Next.js imports/`useRouter`/`typeof window`, `history.pushState`, `render(<App />)` (pass a function), `isPending(user())` / `latest(user())` (pass the accessor), `<Dynamic>` (use `dynamic()`), `import type { JSX } from "solid-js"` (no `JSX` namespace there — import `JSX` from `@solidjs/web`, or use an aliased `Element` from `solid-js`), two-phase effects whose apply only writes a local signal (use a writable derivation), core `action(async ...)` with a plain async function instead of a generator (only `yield` re-enters the transaction), and `setStore(async ...)` on a `createStore` / `createOptimisticStore` setter (the draft is a synchronous transaction; dev throws `[ASYNC_STORE_SETTER]`) |
|
|
19
|
+
| Mechanical gate | `solid2-kit check` | same | Comment-stripped regex guard over `src/**/*.{ts,tsx,jsx}` (or explicit `[paths...]` — files are checked, directories are walked, a missing path fails): fails on props destructuring (including multi-line signatures), `{ ...props }` rest copies, `{list().map(...)}` in JSX, React imports/hooks/JSX props/`React.lazy`/`useOptimistic`/`onDoubleClick`, Solid 1.x imports/APIs/components/JSX namespaces (valueless `use:x` included), Solid 1.x `createEffect(fn)` / `createEffect(fn, init)` / `createMemo(fn, init)` signatures, `const [x] = createProjection(...)` (it returns the store, not a tuple), `vite-plugin-solid`, old-router `<Route>`/`<A>`/`<HashRouter>`/`<FileRoutes>`, `MetaProvider`, `Context.Provider`, camelCase style keys, `key` props (except on `@solidjs/meta` tags), `value()!` hand-narrowing, hand-rolled loading signals, `=== undefined` readiness branches, SolidStart leftovers (`@solidjs/start`, `h3`, `createAsync`, `useSubmission`, `"use client"`), Next.js imports/`useRouter`/`typeof window`, `history.pushState`, `render(<App />)` (pass a function), `isPending(user())` / `latest(user())` (pass the accessor), `<Dynamic>` (use `dynamic()`), `import type { JSX } from "solid-js"` (no `JSX` namespace there — import `JSX` from `@solidjs/web`, or use an aliased `Element` from `solid-js`), two-phase effects whose apply only writes a local signal (use a writable derivation), core `action(async ...)` with a plain async function instead of a generator (only `yield` re-enters the transaction), the router's deprecated `action` / `Action` aliases and `useAction` (use `formAction`; call server functions directly), and `setStore(async ...)` on a `createStore` / `createOptimisticStore` setter (the draft is a synchronous transaction; dev throws `[ASYNC_STORE_SETTER]`) |
|
|
20
20
|
| Edit-time hook | `postToolUse` entry in `.cursor/hooks.json` | `PostToolUse` entry in `.claude/settings.json` (matcher includes `Bash`) | Runs the mechanical gate automatically on **every agent file edit — including edits made through shell commands** (sed, heredocs, codemods; source paths mentioned in the command are checked after it ran) — and feeds findings straight back into the conversation (Cursor: `additional_context`; Claude Code: stderr + exit 2), so enforcement does not depend on the agent remembering to run `check`. Wired only when the kit is a local devDependency; opt out with `--no-hooks` |
|
|
21
21
|
| Turn-end gate | — | `Stop` entry in `.claude/settings.json` | Whole-project `check` + `doctor` when the agent tries to end its turn: with findings in place the stop is blocked (exit 2) and the findings are fed back, so a turn cannot finish with React/Solid 1.x patterns left behind. Loop-safe via `stop_hook_active` (one forced continuation, never an infinite loop) |
|
|
22
22
|
| Pre-execution guard | `preToolUse` + `beforeShellExecution` entries in `.cursor/hooks.json` | `PreToolUse` entry in `.claude/settings.json` | Denies actions **before they run**: edits/deletes of kit-owned guardrail files (an agent blocked by a gate will try to remove the gate), edits that would strip the kit's hook entries or managed blocks, uninspectable shell rewrites of those files, and `npm/pnpm/yarn/bun install` of banned dependencies (`react`, `vite-plugin-solid`, `eslint-plugin-solid`, SolidStart, ...) — caught proactively instead of post-hoc by `doctor` |
|
package/bin/baseline.mjs
CHANGED
|
@@ -14,19 +14,20 @@
|
|
|
14
14
|
// The JSX compilers (@solidjs/compiler, native and the default;
|
|
15
15
|
// @solidjs/babel-plugin under `compiler: "babel"`) are listed because compiled
|
|
16
16
|
// output carries behavior the guidance states (lowercase `on*` is a plain
|
|
17
|
-
// attribute since rc.14
|
|
17
|
+
// attribute since rc.14; the removed 1.x attribute forms are compile errors
|
|
18
|
+
// since rc.17). Projects rarely declare them — @solidjs/vite-plugin
|
|
18
19
|
// pulls them in — so doctor also checks an undeclared install of any package
|
|
19
20
|
// here, which is where a lockfile holds an older compiler.
|
|
20
21
|
export const BASELINE = {
|
|
21
|
-
'solid-js': { line: 2, baseline: '2.0.0-rc.
|
|
22
|
-
'@solidjs/web': { line: 2, baseline: '2.0.0-rc.
|
|
23
|
-
'@solidjs/signals': { line: 2, baseline: '2.0.0-rc.
|
|
24
|
-
'@solidjs/diagnostics': { line: 2, baseline: '2.0.0-rc.
|
|
25
|
-
'@solidjs/compiler': { line: 2, baseline: '2.0.0-rc.
|
|
26
|
-
'@solidjs/babel-plugin': { line: 2, baseline: '2.0.0-rc.
|
|
27
|
-
'@solidjs/router': { line: 2, baseline: '2.0.0-next.
|
|
28
|
-
'@solidjs/vite-plugin': { line: 3, baseline: '3.0.0-next.
|
|
29
|
-
'@solidjs/testing-library': { line: 1, baseline: '1.0.0-
|
|
22
|
+
'solid-js': { line: 2, baseline: '2.0.0-rc.17' },
|
|
23
|
+
'@solidjs/web': { line: 2, baseline: '2.0.0-rc.17' },
|
|
24
|
+
'@solidjs/signals': { line: 2, baseline: '2.0.0-rc.17' },
|
|
25
|
+
'@solidjs/diagnostics': { line: 2, baseline: '2.0.0-rc.17' },
|
|
26
|
+
'@solidjs/compiler': { line: 2, baseline: '2.0.0-rc.17' },
|
|
27
|
+
'@solidjs/babel-plugin': { line: 2, baseline: '2.0.0-rc.17' },
|
|
28
|
+
'@solidjs/router': { line: 2, baseline: '2.0.0-next.42' },
|
|
29
|
+
'@solidjs/vite-plugin': { line: 3, baseline: '3.0.0-next.51' },
|
|
30
|
+
'@solidjs/testing-library': { line: 1, baseline: '1.0.0-rc.2' },
|
|
30
31
|
};
|
|
31
32
|
|
|
32
33
|
export function parseVersion(version) {
|
package/bin/solid2-kit.mjs
CHANGED
|
@@ -395,9 +395,10 @@ function effectSyncFindings(content) {
|
|
|
395
395
|
}
|
|
396
396
|
|
|
397
397
|
// Core `action` (solid-js) takes a generator (`function*` / `async function*`)
|
|
398
|
-
// so `yield` can re-enter the transaction; router `
|
|
399
|
-
// legitimately takes `async (form) => ...`.
|
|
400
|
-
// file's `action` comes from "solid-js" — a
|
|
398
|
+
// so `yield` can re-enter the transaction; router `formAction` (@solidjs/router,
|
|
399
|
+
// and its deprecated `action` alias) legitimately takes `async (form) => ...`.
|
|
400
|
+
// Only flag `action(async` when this file's `action` comes from "solid-js" — a
|
|
401
|
+
// router import (or none) stays silent.
|
|
401
402
|
function actionAsyncFindings(content) {
|
|
402
403
|
if (!/import\s*\{[^}]*\baction\b[^}]*\}\s*from\s*['"]solid-js['"]/.test(content)) return [];
|
|
403
404
|
if (/import\s*\{[^}]*\baction\b[^}]*\}\s*from\s*['"]@solidjs\/router['"]/.test(content)) return [];
|
|
@@ -576,7 +577,8 @@ const CHECKS = [
|
|
|
576
577
|
{
|
|
577
578
|
id: 'solid1-classlist',
|
|
578
579
|
pattern: /\bclassList=/g,
|
|
579
|
-
message:
|
|
580
|
+
message:
|
|
581
|
+
'Solid 1.x `classList` was removed (a compile error on native elements since 2.0.0-rc.17). Use the object/array form of `class`.',
|
|
580
582
|
},
|
|
581
583
|
{
|
|
582
584
|
id: 'react-hook',
|
|
@@ -587,7 +589,7 @@ const CHECKS = [
|
|
|
587
589
|
{
|
|
588
590
|
id: 'solid1-api',
|
|
589
591
|
pattern:
|
|
590
|
-
/(?<![.\w])(?:createResource|onMount|createMutable|modifyMutable|mergeProps|splitProps|produce|unwrap|createComputed|createSelector|
|
|
592
|
+
/(?<![.\w])(?:createResource|onMount|createMutable|modifyMutable|mergeProps|splitProps|produce|unwrap|createComputed|createSelector|startTransition|batch|onError|catchError|createDynamic|renderToStringAsync|clearDelegatedEvents)\s*\(/g,
|
|
591
593
|
message:
|
|
592
594
|
'Removed Solid 1.x API. Replacements: async createMemo, onSettled, createStore drafts, merge/omit/snapshot, automatic batching, Errored, dynamic(), renderToStream.',
|
|
593
595
|
},
|
|
@@ -688,7 +690,7 @@ const CHECKS = [
|
|
|
688
690
|
pattern:
|
|
689
691
|
/(?<=\s)(?:use|on|oncapture|attr|bool):[A-Za-z][\w-]*(?==|\s*\/?>|\s+(?!(?:as|satisfies|in|instanceof)\b(?!\s*=))[A-Za-z_${])/g,
|
|
690
692
|
message:
|
|
691
|
-
'Solid 1.x JSX namespace. Use ref callbacks (and directive factories), camelCase event props, and standard attributes.',
|
|
693
|
+
'Solid 1.x JSX namespace — a compile error on native elements since 2.0.0-rc.17. Use ref callbacks (and directive factories), camelCase event props, and standard attributes; `prop:` remains for DOM properties.',
|
|
692
694
|
},
|
|
693
695
|
{
|
|
694
696
|
id: 'solid1-router',
|
|
@@ -752,9 +754,9 @@ const CHECKS = [
|
|
|
752
754
|
},
|
|
753
755
|
{
|
|
754
756
|
// Only `on` + uppercase binds an event: since rc.14 a lowercase `on*`
|
|
755
|
-
// attribute is a plain attribute (dev warns [LOWERCASE_EVENT_ATTRIBUTE])
|
|
756
|
-
//
|
|
757
|
-
//
|
|
757
|
+
// attribute is a plain attribute (dev warns [LOWERCASE_EVENT_ATTRIBUTE])
|
|
758
|
+
// that never runs the handler; a 1.x `on:click` on a native element is a
|
|
759
|
+
// compile error since rc.17 (solid1-jsx-namespace, above). A real event name in lowercase is always a leftover. Anchored
|
|
758
760
|
// on `={` (a JSX expression attribute): `const onmessage = ...`,
|
|
759
761
|
// `ws.onerror = fn`, and HTML strings (`onerror="…"`) are not matches,
|
|
760
762
|
// and neither is a string expression (`onclick={"track()"}`) — a string
|
|
@@ -826,10 +828,45 @@ const CHECKS = [
|
|
|
826
828
|
id: 'action-plain-async',
|
|
827
829
|
find: actionAsyncFindings,
|
|
828
830
|
message:
|
|
829
|
-
'Core `action` takes a generator (`function*` / `async function*`), not a plain async function — only `yield` re-enters the transaction, so writes after a bare `await` commit immediately. (Router `
|
|
831
|
+
'Core `action` takes a generator (`function*` / `async function*`), not a plain async function — only `yield` re-enters the transaction, so writes after a bare `await` commit immediately. (Router `formAction` from "@solidjs/router" is the one that takes `async (form) => ...`.)',
|
|
832
|
+
},
|
|
833
|
+
{
|
|
834
|
+
// `action` / `Action` from "@solidjs/router" are deprecated aliases of
|
|
835
|
+
// `formAction` / `FormAction` since 2.0.0-next.41 (dev warns on the first
|
|
836
|
+
// call); `action` is solid-js's own primitive.
|
|
837
|
+
id: 'router-action-alias',
|
|
838
|
+
find: (content) => routerImportFindings(content, ['action', 'Action']),
|
|
839
|
+
message:
|
|
840
|
+
'Router `action` / `Action` are deprecated aliases (@solidjs/router 2.0.0-next.41; dev warns). Import `formAction` / `FormAction` from "@solidjs/router" — `action` is solid-js\'s own primitive.',
|
|
841
|
+
},
|
|
842
|
+
{
|
|
843
|
+
// `useAction` is deprecated with no replacement: a mounted router applies a
|
|
844
|
+
// server function's revalidation and redirect on any call.
|
|
845
|
+
id: 'router-use-action',
|
|
846
|
+
find: (content) => routerImportFindings(content, ['useAction']),
|
|
847
|
+
message:
|
|
848
|
+
'`useAction` is deprecated (@solidjs/router 2.0.0-next.41; dev warns): outside a form, call the server function (or a core `action` from "solid-js") directly — the router applies its revalidation and redirect either way. Bind form actions to forms.',
|
|
830
849
|
},
|
|
831
850
|
];
|
|
832
851
|
|
|
852
|
+
// Named imports (and re-exports) from "@solidjs/router" or its `/data` subpath
|
|
853
|
+
// among `names`, located at the name inside the import list. Only the imported
|
|
854
|
+
// name counts: `action as save` is flagged, `formAction as action` (the local
|
|
855
|
+
// name) is not.
|
|
856
|
+
function routerImportFindings(content, names) {
|
|
857
|
+
const findings = [];
|
|
858
|
+
for (const statement of content.matchAll(/(?:import|export)\s*(?:type\s+)?\{([^}]*)\}\s*from\s*['"]@solidjs\/router(?:\/data)?['"]/g)) {
|
|
859
|
+
const list = statement[1];
|
|
860
|
+
const offset = statement.index + statement[0].indexOf(list);
|
|
861
|
+
for (const name of names) {
|
|
862
|
+
for (const match of list.matchAll(new RegExp(`(?<![\\w$.])(?<!\\bas\\s+)(?:type\\s+)?${name}(?![\\w$])`, 'g'))) {
|
|
863
|
+
findings.push({ index: offset + match.index });
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
return findings;
|
|
868
|
+
}
|
|
869
|
+
|
|
833
870
|
// .ts / .tsx / .jsx sources, excluding .d.ts declaration files.
|
|
834
871
|
const SOURCE_FILE = /(?<!\.d)\.(?:tsx?|jsx)$/;
|
|
835
872
|
|
|
@@ -19,8 +19,9 @@ Steps:
|
|
|
19
19
|
- effects that copy state instead of deriving it, or reading stores in the apply phase;
|
|
20
20
|
- `<For>` over server/refetched rows without a stable-id key function;
|
|
21
21
|
- core `action` bodies that are not generators (plain `async` functions) or that write
|
|
22
|
-
after a bare `await` without re-entering on `yield` (router `
|
|
23
|
-
`@solidjs/router` is the one that takes `async (form) =>
|
|
22
|
+
after a bare `await` without re-entering on `yield` (router `formAction` from
|
|
23
|
+
`@solidjs/router` is the one that takes `async (form) => ...`; its `action` alias
|
|
24
|
+
and `useAction` are deprecated <!-- upstream:router-action-deprecated -->);
|
|
24
25
|
- `<Loading>`/`<Errored>` wrapping page chrome instead of the data slot, or
|
|
25
26
|
`const u = user()` extracted then passed (a real parent-side read). Passing
|
|
26
27
|
`user={user()}` is the colorless form — do not "fix" it into accessors or
|
|
@@ -36,18 +37,19 @@ Steps:
|
|
|
36
37
|
`(v) => setX(v)`) — any return value but a cleanup halts reactivity; `onCleanup`
|
|
37
38
|
inside `apply` (never runs) or inside `onSettled`, and memos/effects/`flush()`
|
|
38
39
|
inside an `onSettled` callback (all throw);
|
|
39
|
-
- writes to a store outside its setter (`todos.push(x)`, `todo.done = ...` —
|
|
40
|
-
|
|
40
|
+
- writes to a store outside its setter (`todos.push(x)`, `todo.done = ...` — ignored;
|
|
41
|
+
dev warns `[STORE_WRITE_OUTSIDE_SETTER]` <!-- upstream:store-write-outside-setter -->) and
|
|
42
|
+
`Map`/`Set`/`Date` fields mutated in a draft (not tracked);
|
|
41
43
|
- app-wide state as a module-level signal/store instead of a provider at the root
|
|
42
44
|
of `App` (shared across SSR requests);
|
|
43
45
|
- value-form `createOptimisticStore([])` / `createOptimistic(v)` holding durable
|
|
44
46
|
data (writes vanish on settle) <!-- upstream:optimistic-value-overlay --> instead of the function form refreshed after `yield`;
|
|
45
47
|
- route `preload` returning data read as `props.data` (captured once per match)
|
|
46
48
|
instead of `void getX(params.id)` + `createMemo(() => getX(props.params.id))`;
|
|
47
|
-
- `throw redirect()` / `return reload()` in server functions
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
router `
|
|
49
|
+
- `throw redirect()` / `return reload()` in server functions of an app with no
|
|
50
|
+
mounted router (the caller gets a raw `Response`, never its data; under a router
|
|
51
|
+
the single-flight hook applies them on any call), and POST forms built on a bare
|
|
52
|
+
function's `.url` (TS2339) instead of a router `formAction`;
|
|
51
53
|
error reporting done as a side effect inside `fallback` instead of
|
|
52
54
|
`configureClientErrors` / `configureServerErrors` / `render(fn, root, undefined, { onError })`
|
|
53
55
|
(options are `render`'s 4th argument — a 3rd-argument object is `init`);
|
|
@@ -74,6 +76,11 @@ Steps:
|
|
|
74
76
|
`useSearchParams`; <!-- upstream:pending-links-opt-in --> route `preload`
|
|
75
77
|
functions written to run on hover in a router without `preloadLinks: intentPreload()`
|
|
76
78
|
(link preloading is opt-in; a boolean `preloadLinks` preloads nothing) <!-- upstream:preload-links-opt-in -->;
|
|
79
|
+
- `value` / `checked` bound to a committed value with no per-edit write
|
|
80
|
+
(`value={props.name} onChange={commit}`): the binding re-runs with its template and
|
|
81
|
+
overwrites uncommitted text (`[UNCOMMITTED_INPUT_OVERWRITTEN]`
|
|
82
|
+
<!-- upstream:uncommitted-input-overwritten -->) — a draft signal from `onInput`,
|
|
83
|
+
`defaultValue`, or an explicit write-back;
|
|
77
84
|
- search inputs bound to a held value without `latest`, effect-based debounce, fully
|
|
78
85
|
controlled forms without `name`s, `{ success: false }` result objects instead of
|
|
79
86
|
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 `
|
|
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 `formAction`/`query` from `@solidjs/router` are POST forms and a read cache (router `action` is a deprecated alias of `formAction`, and `useAction` is deprecated: call the server function directly — a mounted router applies its revalidation and redirect on any call <!-- upstream:router-action-deprecated -->). React `useDeferredValue` is `createDeferred(fn)`: an async memo that lags but never leads <!-- upstream:create-deferred-export -->. 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 ignored (dev warns `[STORE_WRITE_OUTSIDE_SETTER]` <!-- upstream:store-write-outside-setter -->), 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 -->; link preloading is opt-in too (`preloadLinks: intentPreload()` for hover/focus/touch; none by default; opt one link out with the string `preload="false"`) <!-- upstream:preload-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 reads `live(fn)` — it implies `GET` (`live(GET(fn))` is the same) <!-- upstream:live-implies-get -->. 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 -->, a held write needs `isPending`/`latest`/optimistic/`affects` feedback or dev + attribution reports `[SILENT_HOLD]`, and `[UNCOMMITTED_INPUT_OVERWRITTEN]` means a `value`/`checked` binding re-wrote the bound value over text the user typed but never committed (`value={props.name} onChange={commit}` with no draft signal) — bind a draft signal written from `onInput`, or `defaultValue` <!-- upstream:uncommitted-input-overwritten -->. 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.
|
|
@@ -64,7 +64,8 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
64
64
|
(a nested child). Never pass a setter as the handler (`onClick={setCount}`) —
|
|
65
65
|
that writes the event object; wrap it: `onClick={() => setCount((c) => c + 1)}`.
|
|
66
66
|
Only `on` + uppercase binds an event: lowercase `onclick` is a plain attribute
|
|
67
|
-
(dev warns `[LOWERCASE_EVENT_ATTRIBUTE]`), not a handler.
|
|
67
|
+
(dev warns `[LOWERCASE_EVENT_ATTRIBUTE]`), not a handler, and the 1.x `on:click`
|
|
68
|
+
namespace is a compile error on a native element since 2.0.0-rc.17.
|
|
68
69
|
<!-- upstream:lowercase-event-attribute -->
|
|
69
70
|
Debounce at the handler (`onInput={debounce(...)}`), never as an effect copying
|
|
70
71
|
one signal into another.
|
|
@@ -94,7 +95,9 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
94
95
|
from `apply` — `onCleanup` there never runs (`[NO_OWNER_CLEANUP]`).
|
|
95
96
|
Single-argument `createEffect(fn)` is an error in Solid 2. Do not substitute
|
|
96
97
|
`createTrackedEffect` for that — it is `@deprecated` since 2.0.0-rc.9 (kept only for
|
|
97
|
-
1.x migration) <!-- upstream:tracked-effect-deprecated -->. Skip the initial run with `{ defer: true }`.
|
|
98
|
+
1.x migration) <!-- upstream:tracked-effect-deprecated -->. Skip the initial run with `{ defer: true }`. Effects never compare: `apply`
|
|
99
|
+
runs on every re-run of `compute`, equal value or not — read a `createMemo` in
|
|
100
|
+
`compute` when equal results should skip the work. Most React `useEffect` code should not become an effect at all — see the skill.
|
|
98
101
|
9. **Stores update by mutating a draft**: `setStore(draft => { draft.user.name = "Ada" })`.
|
|
99
102
|
Never rebuild with spreads — that destroys property-level subscriptions. The return
|
|
100
103
|
form is for shapes where mutation is awkward — most commonly removal
|
|
@@ -107,8 +110,10 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
107
110
|
`setState({ a: 1 })` throw `fn is not a function`, and an object *returned* from the
|
|
108
111
|
callback replaces the whole store (keys it omits are deleted).
|
|
109
112
|
The store itself is read-only: `todos.push(x)`, `state.count++`, or
|
|
110
|
-
`todo.done = !todo.done` outside a setter is **
|
|
111
|
-
|
|
113
|
+
`todo.done = !todo.done` outside a setter is **ignored** (no throw, TypeScript
|
|
114
|
+
accepts it; dev warns `[STORE_WRITE_OUTSIDE_SETTER]` once per store and key since
|
|
115
|
+
2.0.0-rc.17, production stays silent) <!-- upstream:store-write-outside-setter --> —
|
|
116
|
+
every write goes through the setter's draft.
|
|
112
117
|
`Map`, `Set`, and `Date` are stored as-is, not proxied: `draft.selected.add(id)`
|
|
113
118
|
changes the object but notifies nobody. Model sets as `Record<id, true>` (or an
|
|
114
119
|
array), or assign a new instance in the setter (`draft.tags = new Set([...draft.tags, t])`).
|
|
@@ -177,7 +182,10 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
177
182
|
`createContext<T>()` without a default throws when read outside a provider (good) — use
|
|
178
183
|
that for any reactive payload. `createContext("light")` is only for a primitive static
|
|
179
184
|
fallback; a dummy default on a store/signal context *runs* and then silently no-ops
|
|
180
|
-
without a provider.
|
|
185
|
+
without a provider. `undefined` means *not provided*: `createContext(undefined)` is
|
|
186
|
+
the default-less form, and `<Ctx value={undefined}>` counts as no provider (the read
|
|
187
|
+
throws). An optional value is `createContext<T | null>(null)` and `value={null}`.
|
|
188
|
+
<!-- upstream:context-undefined-unprovided --> **Shared state lives in a provider, including app-wide state**
|
|
181
189
|
(theme, session, cart, locale): a factory such as `createCart()` called by a provider
|
|
182
190
|
component placed at the root of `App`, read through `createContext<T>()` (no default)
|
|
183
191
|
and a `useCart()` hook. Module scope is for constants and the `createContext` call itself —
|
|
@@ -189,8 +197,9 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
189
197
|
turned on.
|
|
190
198
|
12. Refs: `let el!: HTMLDivElement` + `ref={el}` or `ref={(node) => (el = node)}`, or forward
|
|
191
199
|
`props.ref`. Compose with arrays: `ref={[props.ref, (node) => (el = node)]}`. No
|
|
192
|
-
`useRef`/`.current`. Ref callbacks run untracked and **without an owner
|
|
193
|
-
|
|
200
|
+
`useRef`/`.current`. Ref callbacks run untracked and **without an owner**, before the
|
|
201
|
+
element is attached (`el.isConnected` is `false`; measure or focus in `onSettled`);
|
|
202
|
+
their return values are ignored. Do not create effects or register cleanup inside a ref
|
|
194
203
|
callback. For reusable directives, create owned primitives (`onSettled`) in a
|
|
195
204
|
factory and return only the element callback. Component setup/teardown is
|
|
196
205
|
`onSettled(() => { ...; return cleanup })` — `onCleanup` is for custom
|
|
@@ -215,7 +224,12 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
215
224
|
14. Do not port these React tools — they have no Solid equivalent because the problems they
|
|
216
225
|
solve don't exist: `useCallback`, `React.memo`, `forwardRef`, `useSyncExternalStore`,
|
|
217
226
|
dependency arrays, `startTransition`/`useTransition` (updates are held and coordinated
|
|
218
|
-
automatically; use `isPending` for indicators).
|
|
227
|
+
automatically; use `isPending` for indicators). The one with a counterpart is
|
|
228
|
+
`useDeferredValue`: `createDeferred(() => filter(rows(), query()))` is an async memo
|
|
229
|
+
whose value may lag its inputs but never leads them — the UI stays interactive while an
|
|
230
|
+
expensive derivation catches up, and `isPending(deferred)` reports the catch-up. It is
|
|
231
|
+
a Solid 2.0 API, unrelated to the 1.x `createDeferred(source, { timeoutMs })`.
|
|
232
|
+
<!-- upstream:create-deferred-export -->
|
|
219
233
|
15. **Components return once.** Never early-return or conditionally return based on reactive
|
|
220
234
|
values — the branch is chosen once at setup and frozen forever. Put conditionals inside
|
|
221
235
|
JSX (`<Show>`, ternary, `<Switch>`/`<Match>`). Early returns on genuinely non-reactive
|
|
@@ -295,6 +309,15 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
295
309
|
React-style controlled input that ignores invalid keystrokes silently breaks. To reject
|
|
296
310
|
input, write back explicitly:
|
|
297
311
|
`onInput={(e) => { if (valid(e.currentTarget.value)) setV(e.currentTarget.value); else e.currentTarget.value = v(); }}`.
|
|
312
|
+
The converse is the committed-value binding: `value={props.name} onChange={commit}`
|
|
313
|
+
with no per-keystroke write re-applies `props.name` whenever anything that binding's
|
|
314
|
+
template reads changes, wiping text the user typed but had not committed — dev reports
|
|
315
|
+
`[UNCOMMITTED_INPUT_OVERWRITTEN]` since 2.0.0-rc.17
|
|
316
|
+
<!-- upstream:uncommitted-input-overwritten -->. Bind a draft signal written from
|
|
317
|
+
`onInput` (`createSignal(() => props.name)` resets it when the prop changes), use
|
|
318
|
+
`defaultValue` for an uncontrolled field, or write the next value back explicitly;
|
|
319
|
+
`equals: false` on the bound signal is no way to reject input — a source firing with an
|
|
320
|
+
equal value is one of the causes the warning names, so it only adds re-writes.
|
|
298
321
|
22. Miscellaneous defaults: SSR-stable element ids come from `createUniqueId()` (never
|
|
299
322
|
`Math.random()` or hardcoded duplicates); pass store data to `structuredClone` /
|
|
300
323
|
`postMessage` / logs via `snapshot(store)` (proxies fail or leak reactivity) — a
|
|
@@ -380,13 +403,27 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
380
403
|
state with `dynamic(() => ...)` from `@solidjs/web`
|
|
381
404
|
(stable identity); `<Dynamic component={...}>` is deprecated (`@deprecated` in the
|
|
382
405
|
types since 2.0.0-rc.9, still shipped in 2.0) <!-- upstream:dynamic-deprecated -->. If the project has `@solidjs/router`, routes are `createRouter({ routes })`
|
|
383
|
-
at module scope — not JSX `<Route>` / `<A>` / `<HashRouter>`. That package's `
|
|
406
|
+
at module scope — not JSX `<Route>` / `<A>` / `<HashRouter>`. That package's `formAction` /
|
|
384
407
|
`query` are URL-addressable POST forms and a read cache — not core `action` /
|
|
385
|
-
`refresh` from `solid-js
|
|
408
|
+
`refresh` from `solid-js` (router `action` / `Action` are deprecated aliases of
|
|
409
|
+
`formAction` / `FormAction` since 2.0.0-next.41, with a dev warning; `useAction` is
|
|
410
|
+
deprecated with no replacement, below) <!-- upstream:router-action-deprecated -->.
|
|
411
|
+
Router mutations: `<form action={save} method="post">`
|
|
386
412
|
(POST only; `.with(id)` binds args into the URL). Validate on the server
|
|
387
413
|
(`throw respond({ issues }, { status: 400 })`); inline errors from `useSubmissions`,
|
|
388
414
|
pending via `form[aria-busy]` CSS. Keep server functions in `src/data/`, not under
|
|
389
|
-
`src/routes` (a `.ts` file there is an API route).
|
|
415
|
+
`src/routes` (a `.ts` file there is an API route). Outside a form, call the server
|
|
416
|
+
function (or a core `action`) directly — single flight is the router's: a mounted router
|
|
417
|
+
applies any server function's `revalidate` keys, `redirect()` / `reload()` and
|
|
418
|
+
flight data on every call, so `useAction` wrappers are unnecessary. Every POST
|
|
419
|
+
server-function call is a mutation to it: with no `revalidate` keys the server
|
|
420
|
+
re-collects the current page's reads and the client revalidates every query; a POST
|
|
421
|
+
that changed nothing returns `respond(value, { revalidate: "" })`, reads go through
|
|
422
|
+
`query()` (which switches the function to GET) or `GET(fn)`, and
|
|
423
|
+
`createRouter({ routes, singleFlight: false })` opts the whole app out
|
|
424
|
+
<!-- upstream:router-post-mutation -->. Params arrive decoded (`/users/%E7%8C%AB`
|
|
425
|
+
gives `params.id === "猫"`; `Router.paths` encodes on the way out); `useInRouter()`
|
|
426
|
+
tells a component whether a router is mounted. Cache
|
|
390
427
|
reads: `query(fn, "name")` then `createMemo(() => getUser(id()))`; after a mutation
|
|
391
428
|
`revalidate(getUser.key)`. Do not wrap mutations in `query` (an undeclared server
|
|
392
429
|
function becomes GET). `Router.paths` is not the current location (`useLocation` /
|
|
@@ -414,7 +451,7 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
414
451
|
`useIsRouting` in JSX — never hand-rolled `location.pathname` comparisons.
|
|
415
452
|
<!-- upstream:pending-links-opt-in --> Trusted identity is
|
|
416
453
|
`getRequestEvent()`, never a caller-supplied user id. Unscripted (no-JS) POST
|
|
417
|
-
forms use a router `
|
|
454
|
+
forms use a router `formAction` over a `(form: FormData)` server function (the post
|
|
418
455
|
arrives as one `FormData`; other signatures make `action={save}` TS2322) —
|
|
419
456
|
`<form action={save} method="post">` — not the
|
|
420
457
|
function's `.url` and not a hand-built `/_server/` path: a bare `"use server"`
|
|
@@ -434,13 +471,13 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
434
471
|
server render) <!-- upstream:ssr-error-sanitized --> — so intentional client-facing failures are `markSafeError(...)` or
|
|
435
472
|
`throw respond(body, { status })`. Reads are function calls too (`await getUser(id)`
|
|
436
473
|
on a `GET()`-declared read; the method mirrors the operation). `redirect()` /
|
|
437
|
-
`reload()` are signals for an integration: Solid Router's `
|
|
438
|
-
no-JS form post
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
`null`. So guards `throw redirect("/sign-in")`
|
|
443
|
-
|
|
474
|
+
`reload()` are signals for an integration: Solid Router's `formAction` / `query`, the
|
|
475
|
+
no-JS form post, and (since router 2.0.0-next.41) the mounted router's single-flight
|
|
476
|
+
hook on any direct server-function call apply them. Without a mounted router a direct
|
|
477
|
+
caller — a core `action`, a handler, a plain memo — never gets your data: it receives
|
|
478
|
+
the raw `Response` as the resolved value (typed as your data, no navigation); with one,
|
|
479
|
+
the hook navigates and the call resolves to `null`. So guards `throw redirect("/sign-in")`
|
|
480
|
+
only in a routed app; without the router, `throw respond(null, { status: 401 })`
|
|
444
481
|
(the call rejects) or return a value and navigate in the caller. `redirect()` /
|
|
445
482
|
`respond()` take `revalidate` like `reload()`. Cache GET reads with
|
|
446
483
|
`Cache-Control` on that response (the default is `no-store`), not a hand-rolled
|
|
@@ -470,8 +507,9 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
|
|
|
470
507
|
`Blob` / `FormData` argument travels natively). `enableRichArguments()` from
|
|
471
508
|
`@solidjs/web/server-functions/rich-args` (at `src/App.tsx` module scope) lifts that
|
|
472
509
|
(its import builds cleanly since 2.0.0-rc.10 — a `resolve.dedupe: ["@solidjs/web"]`
|
|
473
|
-
workaround left from rc.9 is no longer needed) <!-- upstream:rich-args-vite-build https://github.com/solidjs/solid/issues/3627 -->. Declare live reads as `live(
|
|
474
|
-
`live()`
|
|
510
|
+
workaround left from rc.9 is no longer needed) <!-- upstream:rich-args-vite-build https://github.com/solidjs/solid/issues/3627 -->. Declare live reads as `live(fn)` —
|
|
511
|
+
it implies `GET` since 2.0.0-rc.17 (`live(GET(fn))` is the same declaration, `live()`
|
|
512
|
+
outermost) <!-- upstream:live-implies-get -->; an undeclared async iterable still producing while a document
|
|
475
513
|
renders is `[SSR_UNDECLARED_LIVE_SOURCE]` in dev. A plain `async function*` server function is an event stream on
|
|
476
514
|
one connection (no reconnect when it drops); `live()` is one value that changes over
|
|
477
515
|
time and reconnects. `live()` connection state is
|
|
@@ -501,8 +539,8 @@ directory (default `src/`).
|
|
|
501
539
|
| `solid/imports` | changed paths | banned-API table + `solid2-kit check` |
|
|
502
540
|
| `solid/event-handlers` | still valid | rule 6 |
|
|
503
541
|
| `solid/jsx-no-undef`, `jsx-no-duplicate-props` | covered by TypeScript | project typecheck |
|
|
504
|
-
| `solid/prefer-classlist` | obsolete — `classList` removed | `solid2-kit check` bans `classList` |
|
|
505
|
-
| `solid/no-unknown-namespaces` | obsolete — `on:`/`use:`/`attr:` removed in 2.0 (`prop:` remains) | banned-API table + `solid2-kit check` |
|
|
542
|
+
| `solid/prefer-classlist` | obsolete — `classList` removed (a compile error since 2.0.0-rc.17) | `solid2-kit check` bans `classList` |
|
|
543
|
+
| `solid/no-unknown-namespaces` | obsolete — `on:`/`use:`/`attr:` removed in 2.0 (`prop:` remains); compile errors on native elements since 2.0.0-rc.17 <!-- upstream:removed-attribute-compile-error --> | banned-API table + `solid2-kit check` |
|
|
506
544
|
| `solid/no-proxy-apis` | obsolete — Solid 2 requires Proxy | not carried |
|
|
507
545
|
|
|
508
546
|
## Banned Solid 1.x APIs (Solid 2 replacements)
|
|
@@ -521,8 +559,9 @@ directory (default `src/`).
|
|
|
521
559
|
| `setState("a", "b", value)` path setters, `setState({ a })` merges, `produce(...)` | draft setter: `setState(draft => { ... })` (the others throw `fn is not a function`) |
|
|
522
560
|
| `mergeProps` / `splitProps` / `unwrap` | `merge` / `omit` / `snapshot` |
|
|
523
561
|
| `classList={...}` | `class` object/array form |
|
|
524
|
-
| `createMutable`, `modifyMutable` | `createStore` + draft setters (direct writes to the store are
|
|
525
|
-
| `createComputed`, `createSelector
|
|
562
|
+
| `createMutable`, `modifyMutable` | `createStore` + draft setters (direct writes to the store are ignored; dev warns `[STORE_WRITE_OUTSIDE_SETTER]`) |
|
|
563
|
+
| `createComputed`, `createSelector` | `createMemo`, `createProjection` |
|
|
564
|
+
| `createDeferred(source, { timeoutMs })` (1.x: a signal that lags by idle time) | `createDeferred(fn)` (2.0: an async memo that may lag its inputs but never leads them; `isPending(deferred)` while it catches up) <!-- upstream:create-deferred-export --> |
|
|
526
565
|
| `startTransition`, `useTransition` | automatic held updates + `isPending` |
|
|
527
566
|
| `onError` / `catchError` | `<Errored>` or effect bundle `{ effect, error }` |
|
|
528
567
|
| `from(...)` / `observable(...)` (the solid-js helpers) | inbound: async iterable from a memo; outbound: split effect |
|
|
@@ -536,19 +575,12 @@ directory (default `src/`).
|
|
|
536
575
|
| `import ... from "@solidjs/start"` / `vinxi` / `h3` / `"use client"` | `GET` from `@solidjs/web/server-functions`; Solid has no `"use client"` |
|
|
537
576
|
| `render(<App />, root)` | `render(() => <App />, root)` (same for `hydrate` / `renderToString` / `renderToStream`) |
|
|
538
577
|
| `<MetaProvider>` | no provider — render `<Title>` / `<Meta>` / `<Link>` from `@solidjs/meta` anywhere |
|
|
539
|
-
| `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) |
|
|
578
|
+
| `use:directive`, `on:`/`oncapture:`, `attr:`/`bool:`, `/*@once*/` (the namespaces are compile errors on native elements since 2.0.0-rc.17 <!-- upstream:removed-attribute-compile-error -->; 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) |
|
|
540
579
|
| `resource.loading` / `resource.error` | `<Loading>` boundary / `<Errored>` boundary |
|
|
541
580
|
|
|
542
581
|
When unsure about any API, verify against the official Solid 2.0 docs — fetchable URLs are
|
|
543
582
|
listed in `references/official-docs.md` next to the `solid-2` skill. Do not guess from
|
|
544
|
-
Solid 1.x or React memory.
|
|
545
|
-
these rules win there: the router navigation page lists `data-pending` as automatic
|
|
546
|
-
(it needs `links: pendingLinks`) <!-- upstream:docs-pending-links-stale -->, the
|
|
547
|
-
`createSignal` reference says a write to a writable derivation wins over a same-tick
|
|
548
|
-
recompute (a source change in the same update now re-runs it with the write as
|
|
549
|
-
`prev`) <!-- upstream:docs-derived-write-stale -->, and the performance guide and
|
|
550
|
-
attribution reference give `[WIDE_SCOPE_DEPS]` a threshold of 30 (the default
|
|
551
|
-
`wideDeps` is 200). <!-- upstream:docs-wide-deps-stale -->
|
|
583
|
+
Solid 1.x or React memory.
|
|
552
584
|
|
|
553
585
|
`createRenderEffect` exists in Solid 2 but is not a default. `storePath`
|
|
554
586
|
(the 1.x path-setter migration helper) is no longer exported from `solid-js` as of
|
|
@@ -160,12 +160,16 @@ at component-body top level is a write in an owned scope — dev throws
|
|
|
160
160
|
seed the shape through the `createStore` argument or a derivation.
|
|
161
161
|
|
|
162
162
|
**The store itself is read-only; only the draft writes.** An assignment or mutating
|
|
163
|
-
call on the store proxy outside a setter is *ignored* — no throw,
|
|
164
|
-
|
|
163
|
+
call on the store proxy outside a setter is *ignored* — no throw, the value does not
|
|
164
|
+
change, and TypeScript accepts it (`Store<T>` is `T`). Since 2.0.0-rc.17 dev warns
|
|
165
|
+
`[STORE_WRITE_OUTSIDE_SETTER]` once per store object and key (a later delete of a key
|
|
166
|
+
already reported is not reported again; the operation is only in the event's `data`);
|
|
167
|
+
production stays silent
|
|
168
|
+
<!-- upstream:store-write-outside-setter -->. This is the
|
|
165
169
|
MobX / Vue / Solid 1 `createMutable` reflex:
|
|
166
170
|
|
|
167
171
|
```tsx
|
|
168
|
-
// WRONG — all
|
|
172
|
+
// WRONG — all ignored (dev: [STORE_WRITE_OUTSIDE_SETTER])
|
|
169
173
|
todos.push(todo);
|
|
170
174
|
state.count++;
|
|
171
175
|
todo.done = !todo.done;
|
|
@@ -305,6 +309,10 @@ reactive system (`[REACTIVITY_HALTED]`). TypeScript reports it as TS2345; a JS p
|
|
|
305
309
|
gets no warning. The cleanup of an effect is the `apply` return value — `onCleanup`
|
|
306
310
|
inside `apply` has no owner and never runs (`[NO_OWNER_CLEANUP]`).
|
|
307
311
|
|
|
312
|
+
**Effects never compare.** `apply` runs on every re-run of `compute`, whether or not the
|
|
313
|
+
computed value changed; there is no `equals` on an effect. When equal results should skip
|
|
314
|
+
the imperative work, read a `createMemo` (the equality boundary) in `compute`.
|
|
315
|
+
|
|
308
316
|
```tsx
|
|
309
317
|
// WRONG — returns the assigned string; halts reactivity (dev: first run, prod: next run)
|
|
310
318
|
createEffect(() => title(), (t) => (document.title = t));
|
|
@@ -496,8 +504,8 @@ repeating `String(error())` at each boundary:
|
|
|
496
504
|
error an `<Errored>` collected never reaches `window.onerror`; a `captureException`
|
|
497
505
|
call inside `fallback` is a side effect in render that fires on every re-render of the
|
|
498
506
|
fallback. Since 2.0.0-rc.9 the runtime has a seam for exactly this failure, called
|
|
499
|
-
once per error object
|
|
500
|
-
(`boundaryPath`):
|
|
507
|
+
once per error object (on the server: once per error object, per `kind`, per request)
|
|
508
|
+
with where it was thrown (`ownerPath`) and where it was met (`boundaryPath`):
|
|
501
509
|
|
|
502
510
|
```ts
|
|
503
511
|
import { configureClientErrors } from 'solid-js';
|
|
@@ -921,6 +929,29 @@ unless you rewind the DOM yourself:
|
|
|
921
929
|
/>
|
|
922
930
|
```
|
|
923
931
|
|
|
932
|
+
The opposite mistake is binding a **committed** value with no per-keystroke write —
|
|
933
|
+
`value={props.name} onChange={commit}`, the React "controlled by the saved value" shape.
|
|
934
|
+
The `value` binding re-runs whenever anything its template expression reads changes
|
|
935
|
+
(the prop, a store row, a `class` that shares the template), and re-writes the committed
|
|
936
|
+
value over text the user typed but has not committed yet. Since 2.0.0-rc.17 dev reports
|
|
937
|
+
`[UNCOMMITTED_INPUT_OVERWRITTEN]` with the element and the bound value
|
|
938
|
+
<!-- upstream:uncommitted-input-overwritten -->. Three repairs, in order of preference:
|
|
939
|
+
|
|
940
|
+
```tsx
|
|
941
|
+
// 1. a draft signal written per keystroke; resets when the committed value changes
|
|
942
|
+
const [draft, setDraft] = createSignal(() => props.name);
|
|
943
|
+
<input value={draft()} onInput={(e) => setDraft(e.currentTarget.value)} onChange={() => props.onCommit(draft())} />
|
|
944
|
+
|
|
945
|
+
// 2. uncontrolled: the DOM owns the text, the prop only seeds it
|
|
946
|
+
<input defaultValue={props.name} onChange={(e) => props.onCommit(e.currentTarget.value)} />
|
|
947
|
+
|
|
948
|
+
// 3. explicit write-back when the field must follow an external value exactly
|
|
949
|
+
```
|
|
950
|
+
|
|
951
|
+
`equals: false` on the committed signal makes it worse, not better — a source firing with
|
|
952
|
+
an equal value is one of the three causes the warning names, beside another binding in the
|
|
953
|
+
same template changing and a spread re-running — and a `createEffect` that copies the prop into the field is rule 4 again.
|
|
954
|
+
|
|
924
955
|
### Context
|
|
925
956
|
|
|
926
957
|
```tsx
|
|
@@ -946,6 +977,14 @@ Pass accessors/setters/stores through context — never `value={theme()}` (a dea
|
|
|
946
977
|
No `useMemo` for the value object, no context splitting: the object is created once and
|
|
947
978
|
fine-grained updates flow through the signals inside it.
|
|
948
979
|
|
|
980
|
+
**`undefined` means "not provided".** `createContext<T>()` and `createContext(undefined)`
|
|
981
|
+
are the same default-less context, and a provider whose `value` is `undefined`
|
|
982
|
+
(`<ThemeContext value={maybeTheme()}>` while the memo has no value yet) counts as no
|
|
983
|
+
provider: `useContext` below it throws `ContextNotFoundError`, whose message points at
|
|
984
|
+
`createContext<T | null>(null)`. An intentionally optional payload is modelled with
|
|
985
|
+
`null` — `createContext<Session | null>(null)` and `value={session() ?? null}` — so the
|
|
986
|
+
read distinguishes "no session" from "no provider" <!-- upstream:context-undefined-unprovided -->.
|
|
987
|
+
|
|
949
988
|
**App-wide state is the same provider, placed at the root of `App`** — not a
|
|
950
989
|
module-level signal or store. Module scope is for constants and the `createContext`
|
|
951
990
|
call itself:
|
|
@@ -1018,7 +1057,10 @@ function SearchField(props: { ref?: Ref<HTMLInputElement>; onInput: EventListene
|
|
|
1018
1057
|
|
|
1019
1058
|
Ref arrays flatten recursively; each callback runs in order. No `forwardRef` needed.
|
|
1020
1059
|
|
|
1021
|
-
**Ref callbacks run untracked
|
|
1060
|
+
**Ref callbacks run untracked, without an owner, and before the element is attached.**
|
|
1061
|
+
The node exists but `el.isConnected` is `false` and it has no layout yet — measuring,
|
|
1062
|
+
focusing or scrolling in a ref callback reads zeros; do that in `onSettled`, which fires
|
|
1063
|
+
after the tree is in the document. Their return values are
|
|
1022
1064
|
ignored — do not create effects, memos, or `onCleanup` inside the callback, and
|
|
1023
1065
|
do not `return () => cleanup`. Put owned setup in a **directive factory** (like
|
|
1024
1066
|
`listen` above) and return only the element callback. Use this for
|
|
@@ -1141,7 +1183,7 @@ write and `flush()`: `const { setA, b, dispose } = createRoot((dispose) => { …
|
|
|
1141
1183
|
{ setA, b, dispose }; }); setA(2); flush();`. A hook that reads context runs under
|
|
1142
1184
|
`renderHook(useCart, { wrapper: CartProvider })`. A routed component gets a router built in
|
|
1143
1185
|
the test — `createRouter({ routes, history: memoryHistory("/orders/42") })` — because
|
|
1144
|
-
Testing Library's `render(…, { location })` is typed but not implemented (1.0.0-
|
|
1186
|
+
Testing Library's `render(…, { location })` is typed but not implemented (1.0.0-rc.2). <!-- upstream:testing-library-location -->
|
|
1145
1187
|
|
|
1146
1188
|
An end-to-end test that must wait for hydration of an SSR page waits on a marker set from
|
|
1147
1189
|
`onHydrationEnd` (`solid-js`, `@experimental`), registered while hydration is running:
|
|
@@ -1185,11 +1227,18 @@ Always-on dev checks include misplaced reads
|
|
|
1185
1227
|
for a first read made after an `await` in an async compute — V8 engines only), misplaced writes
|
|
1186
1228
|
(`[REACTIVE_WRITE_IN_OWNED_SCOPE]` — since rc.9 also for store setters called in a
|
|
1187
1229
|
component or root body <!-- upstream:store-write-owned-scope -->, `[ASYNC_STORE_SETTER]` for a store setter callback that returns
|
|
1188
|
-
a promise <!-- upstream:async-store-setter-throws -->, `[FLUSH_IN_ACTION]`, `[SERVER_WRITE]
|
|
1230
|
+
a promise <!-- upstream:async-store-setter-throws -->, `[FLUSH_IN_ACTION]`, `[SERVER_WRITE]`,
|
|
1231
|
+
and since rc.17 `[STORE_WRITE_OUTSIDE_SETTER]` for a write to the store proxy itself
|
|
1232
|
+
<!-- upstream:store-write-outside-setter -->), misplaced bindings
|
|
1233
|
+
(`[UNCOMMITTED_INPUT_OVERWRITTEN]` — a `value` / `checked` binding re-applied its
|
|
1234
|
+
committed value over uncommitted user input <!-- upstream:uncommitted-input-overwritten -->),
|
|
1189
1235
|
`[ASYNC_OUTSIDE_LOADING_BOUNDARY]` when a tracked read has no boundary above it, and
|
|
1190
1236
|
`[LOADING_ON_OUTSIDE_HOLD]` when a `<Loading on>` boundary's data is also read outside it.
|
|
1191
|
-
Each entry carries a stable `[CODE]`, what the runtime observed,
|
|
1192
|
-
(`in <App> › <Cart> › <LineItem>`)
|
|
1237
|
+
Each entry carries a stable `[CODE]`, what the runtime observed, an owner chain
|
|
1238
|
+
(`in <App> › <Cart> › <LineItem>`), and — since rc.16, under `vite dev` — an
|
|
1239
|
+
`at src/Cart.tsx:12:5` line naming where the scope was created (the compiler's
|
|
1240
|
+
`sourceNames.locations`; the same `loc` sits on every artifact event)
|
|
1241
|
+
<!-- upstream:source-names-locations -->; never silence a code before understanding it.
|
|
1193
1242
|
Since rc.9 the server render reports on the same channel with the same owner chains:
|
|
1194
1243
|
`[SSR_RENDER_ERROR_CONTAINED]` (a boundary routed a render error; `ownerPath` is where
|
|
1195
1244
|
it was thrown, `boundaryPath` where it was met) <!-- upstream:ssr-render-error-contained -->, `[SERVER_ERROR_SANITIZED]` (the original
|
|
@@ -1197,7 +1246,10 @@ behind a production-sanitized error, from a server function or an SSR render),
|
|
|
1197
1246
|
`[SSR_CLIENT_CONTENT_MASKED]` (client-only content that surfaced only after a real
|
|
1198
1247
|
server wait — the wait was wasted), `[SSR_BOUNDARY_WATERFALL]` (sequential waits inside
|
|
1199
1248
|
one server `<Loading>` boundary), `[SSR_UNDECLARED_LIVE_SOURCE]` (an async iterable
|
|
1200
|
-
still pumping into a document render — declare it `live()`),
|
|
1249
|
+
still pumping into a document render — declare it `live()`), `[SSR_LIVE_HOLE_LATCHED]`
|
|
1250
|
+
(a live markup hole or attribute stopped updating: re-running it created an async scope
|
|
1251
|
+
— a memo over a fetch inside the hole's expression — or called a slot, so it keeps the
|
|
1252
|
+
markup it last sent; create async sources in the component body), and
|
|
1201
1253
|
`[REVEAL_IN_RENDER_TO_STRING]`.
|
|
1202
1254
|
For the repair behind any code, read the `reactivity-diagnostics` skill Solid ships
|
|
1203
1255
|
in the repo (`node_modules/solid-js/skills/reactivity-diagnostics/SKILL.md`) — every
|
|
@@ -1241,7 +1293,12 @@ anonymous nodes print as `computed` / `effect` / `signal`; component labels
|
|
|
1241
1293
|
(`<Checkout>`) are always present under `vite dev` and survive minification only with
|
|
1242
1294
|
the compiler's `sourceNames` option (components, binding effects, primitives — the Vite
|
|
1243
1295
|
plugin turns it on under `vite dev` and `observe: true`; `componentNames` was its rc.9 name
|
|
1244
|
-
and is rejected now) <!-- upstream:source-names-option -->.
|
|
1296
|
+
and is rejected now) <!-- upstream:source-names-option -->. Its `locations` kind (rc.16)
|
|
1297
|
+
adds `file:line:col` to every name — `createComponent(Home, props, "Home", "src/App.tsx:12:5")`,
|
|
1298
|
+
`{ name, loc }` on primitives — which is what the `at …` line in a diagnostic and
|
|
1299
|
+
`event.loc` in an artifact print; the plugin turns it on under `vite dev` only, and an
|
|
1300
|
+
observe build opts in with `sourceNames: { locations: true }` (paths relative to the
|
|
1301
|
+
compiler's `root`) <!-- upstream:source-names-locations -->. On the
|
|
1245
1302
|
**prod build** the import resolves to an inert same-surface twin whose `enable()` is a
|
|
1246
1303
|
no-op, so the import can stay in the code. On the **observe build** it is the real
|
|
1247
1304
|
engine and an unguarded `enable()` records in production — keep the `isDev` guard
|
|
@@ -1342,7 +1399,8 @@ afterEach(async () => {
|
|
|
1342
1399
|
What the shape has to get right:
|
|
1343
1400
|
|
|
1344
1401
|
- **A DOM environment** (`happy-dom` / `jsdom`) for every reactive test, `.test.ts`
|
|
1345
|
-
included
|
|
1402
|
+
included — set `test.environment` yourself; the Vite plugin does not set it, so a
|
|
1403
|
+
config without one runs under Vitest's default `node`. There the export conditions select Solid's
|
|
1346
1404
|
**server build**: writes are inert (`[SERVER_WRITE]`), effects never run, and the
|
|
1347
1405
|
attribution tables stay empty — a green test that proves nothing. Components that only
|
|
1348
1406
|
exist in the server build (a `<html>` document shell) get their own `node` project
|
|
@@ -1473,7 +1531,10 @@ export default { plugins: [solid({ ssr: true, observe: true })] };
|
|
|
1473
1531
|
`observe: true` adds the `observe` condition to every environment (client and server,
|
|
1474
1532
|
`vite build` and preview) and turns on the compiler's `sourceNames` option (components,
|
|
1475
1533
|
binding effects, and primitives) so `ownerPath` / `boundaryPath` name components instead
|
|
1476
|
-
of `computed` <!-- upstream:source-names-option
|
|
1534
|
+
of `computed` <!-- upstream:source-names-option -->; source *locations* stay off in that
|
|
1535
|
+
build (names only) unless `sourceNames: { locations: true }` is passed — they cost bundle
|
|
1536
|
+
bytes and expose file paths, so opt in when the monitoring tool consumes `loc`
|
|
1537
|
+
<!-- upstream:source-names-locations -->. Without the plugin, set `resolve.conditions` (or `node --conditions=observe`
|
|
1477
1538
|
for an unbundled server) and `sourceNames` yourself. Cost in Solid's size suite: ~16.8 KB against ~15.3 KB brotli
|
|
1478
1539
|
for a small client app; importing the attribution engine on top raises the cap to
|
|
1479
1540
|
~27 KB (a separate entry — an observe build that never imports it never ships it). The
|
|
@@ -1486,7 +1547,14 @@ production too (observe build; a no-op on the prod build).
|
|
|
1486
1547
|
must run before the modules it patches load. `import "./instrument"` at the top of an
|
|
1487
1548
|
entry does not achieve that in ESM (static imports are hoisted in dependency order);
|
|
1488
1549
|
the plugin's `start: { instrument: "./src/instrument.ts" }` awaits that server-only
|
|
1489
|
-
module before anything else in the server graph.
|
|
1550
|
+
module before anything else in the server graph — since vite-plugin 3.0.0-next.51 the
|
|
1551
|
+
`node` entry too: `dist/server/node.js` awaits `./server.js` (which awaits the
|
|
1552
|
+
instrument module) and then loads `node-runtime.js`, so deploy both files. Failures the
|
|
1553
|
+
handler contains itself reach the hook as `{ kind: "request", handling: "failed" }`.
|
|
1554
|
+
The client-side counterpart is `start: { clientSetup: "./src/client-setup.ts" }`: a
|
|
1555
|
+
browser module run (and awaited, top-level `await` allowed) before `hydrate` / `render`,
|
|
1556
|
+
for a client SDK's `init()` or `enableRichArguments()`; it conflicts with an authored
|
|
1557
|
+
`entry-client.tsx`, which already owns that order.
|
|
1490
1558
|
- **Tracing.** The server continues an incoming W3C `traceparent` or originates one;
|
|
1491
1559
|
`getTraceContext()` from `@solidjs/web` reads it during the request (in-process
|
|
1492
1560
|
server-function calls included) — forward it downstream with
|
|
@@ -1713,16 +1781,19 @@ the raw strings rather than throwing). Query values are strings, so coerce in th
|
|
|
1713
1781
|
`page: v.optional(v.pipe(v.unknown(), v.transform(Number)), 1)`; a plain `v.number()`
|
|
1714
1782
|
rejects every value and hands back the string, still typed `number`.
|
|
1715
1783
|
|
|
1716
|
-
|
|
1717
|
-
(optimistic writes that span an async gap). Router `
|
|
1718
|
-
a URL-addressable POST mutation (`useSubmissions`,
|
|
1719
|
-
|
|
1784
|
+
**`formAction` and core `action` are different things.** Core `action` from `solid-js` is
|
|
1785
|
+
a generator transaction (optimistic writes that span an async gap). Router `formAction`
|
|
1786
|
+
from `@solidjs/router` is a URL-addressable POST mutation (`useSubmissions`,
|
|
1787
|
+
`<form action={save} method="post">`). The router's `action` / `Action` exports are
|
|
1788
|
+
deprecated aliases of `formAction` / `FormAction` since 2.0.0-next.41 — dev warns on the
|
|
1789
|
+
first call, and `solid2-kit check` flags the import — so a router `action` import is
|
|
1790
|
+
always the stale spelling, never the core primitive <!-- upstream:router-action-deprecated -->.
|
|
1720
1791
|
|
|
1721
1792
|
```tsx
|
|
1722
|
-
import {
|
|
1793
|
+
import { formAction } from '@solidjs/router';
|
|
1723
1794
|
import { redirect } from '@solidjs/web';
|
|
1724
1795
|
|
|
1725
|
-
const save =
|
|
1796
|
+
const save = formAction(async (form: FormData) => {
|
|
1726
1797
|
await saveProfile(form);
|
|
1727
1798
|
return redirect('/account');
|
|
1728
1799
|
}, 'update-profile');
|
|
@@ -1735,7 +1806,20 @@ const save = action(async (form: FormData) => {
|
|
|
1735
1806
|
|
|
1736
1807
|
Only POST forms are accepted. Bind extra args with `.with(id)` (they go in the
|
|
1737
1808
|
action URL). `onSubmit={(e) => { e.preventDefault(); fetch(...) }}` *runs* and
|
|
1738
|
-
drops the no-JS form fallback. `useAction` is
|
|
1809
|
+
drops the no-JS form fallback. `useAction` is deprecated (next.41, dev warns) and has
|
|
1810
|
+
no replacement because it needs none: **single flight is the router's.** A mounted
|
|
1811
|
+
router intercepts every server-function call — from a handler, a core `action`, a memo
|
|
1812
|
+
— and applies the response's `revalidate` keys, `redirect()` / `reload()` and flight
|
|
1813
|
+
data, so outside a form you call the server function (or a core `action` around it)
|
|
1814
|
+
directly. The router treats **every POST server-function call as a mutation**: with no
|
|
1815
|
+
`revalidate` keys on the response the server re-collects the current page's reads into
|
|
1816
|
+
the flight and the client revalidates every query; a POST that changed nothing returns
|
|
1817
|
+
`respond(value, { revalidate: "" })`; reads stay exempt through `query()` (which
|
|
1818
|
+
switches the function to GET) or `GET(fn)`; `createRouter({ routes, singleFlight: false })` opts the app
|
|
1819
|
+
out <!-- upstream:router-post-mutation -->. Route params arrive **decoded**
|
|
1820
|
+
(`/users/%E7%8C%AB` gives `params.id === "猫"`, and `Router.paths` encodes on the way
|
|
1821
|
+
out), and `useInRouter()` answers whether a router is mounted — for a component shared
|
|
1822
|
+
between routed and unrouted trees.
|
|
1739
1823
|
Optimistic rows for a router form keep the `<form>` and add no core `action`: read the
|
|
1740
1824
|
list `query` with `createOptimisticStore(() => getList(), [])` and register
|
|
1741
1825
|
`save.onSubmit((form) => setList((d) => { d.push({ …, pending: true }); }))` in the
|
|
@@ -1780,9 +1864,9 @@ flow — the status and value survive production sanitization, unlike a plain th
|
|
|
1780
1864
|
`Error`. Pass 3 renders the messages inline from the recorded submission:
|
|
1781
1865
|
|
|
1782
1866
|
```tsx
|
|
1783
|
-
import {
|
|
1867
|
+
import { formAction, useSubmissions } from '@solidjs/router';
|
|
1784
1868
|
|
|
1785
|
-
const submitAddress =
|
|
1869
|
+
const submitAddress = formAction(saveAddress);
|
|
1786
1870
|
const submissions = useSubmissions(submitAddress);
|
|
1787
1871
|
const issues = () => (submissions.at(-1)?.error as { issues?: AddressIssues } | undefined)?.issues ?? {};
|
|
1788
1872
|
|
|
@@ -1863,19 +1947,18 @@ revalidate: `redirect` / `reload` from `@solidjs/web` (see the server-function
|
|
|
1863
1947
|
guides).
|
|
1864
1948
|
|
|
1865
1949
|
**`redirect()` / `reload()` are signals for an integration, not navigation.** Solid
|
|
1866
|
-
Router's `
|
|
1867
|
-
follows the 302
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
caller never gets its data, so the redirect guard belongs to
|
|
1873
|
-
the router:
|
|
1950
|
+
Router's `formAction` / `query` apply them (navigate, revalidate), a no-JS form post
|
|
1951
|
+
follows the 302, and since router 2.0.0-next.41 the mounted router's single-flight hook
|
|
1952
|
+
applies them on *any* server-function call made while it is mounted (the call then
|
|
1953
|
+
resolves to `null`). Without a mounted router a caller — a core `action`, an event
|
|
1954
|
+
handler, a plain `createMemo(() => listOrders())` — receives the raw `Response` as the
|
|
1955
|
+
resolved value: no navigation happens, and the type still claims your data. Either way
|
|
1956
|
+
the caller never gets its data, so the redirect guard belongs to routed apps:
|
|
1874
1957
|
|
|
1875
1958
|
```ts
|
|
1876
1959
|
import { getRequestEvent, redirect, respond } from '@solidjs/web';
|
|
1877
1960
|
|
|
1878
|
-
//
|
|
1961
|
+
// a routed app (router formAction/query, or any call under a mounted router): it navigates to /sign-in
|
|
1879
1962
|
export async function listOrders() {
|
|
1880
1963
|
'use server';
|
|
1881
1964
|
const userId = getRequestEvent()?.locals.userId;
|
|
@@ -1942,27 +2025,29 @@ export const fetchStory = GET(async (id: number) => {
|
|
|
1942
2025
|
});
|
|
1943
2026
|
```
|
|
1944
2027
|
|
|
1945
|
-
A value that changes over time is `live(
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
2028
|
+
A value that changes over time is `live(fn)`. Since 2.0.0-rc.17 `live()` **implies
|
|
2029
|
+
`GET`**: a function not already declared `GET` is wrapped at runtime, so the stream has a
|
|
2030
|
+
cacheable URL and is never mistaken for a mutation (the router's single flight treats
|
|
2031
|
+
every POST call as one). `live(GET(fn))` is the same declaration spelled out — keep
|
|
2032
|
+
`live()` outermost when you write both <!-- upstream:live-implies-get -->. A server
|
|
2033
|
+
component rendered into a document that reads an *undeclared* async iterable still
|
|
2034
|
+
producing after 5s is `[SSR_UNDECLARED_LIVE_SOURCE]` in dev — declare it `live()` (the
|
|
2035
|
+
document takes the first value and closes it; the client connects after hydration) or
|
|
2036
|
+
bound the source. `broadcast(fn)` (experimental) is `live()` for one answer shared across
|
|
2037
|
+
every viewer — the server runs the generator once per argument set instead of once per
|
|
2038
|
+
connection.
|
|
1952
2039
|
|
|
1953
2040
|
```ts
|
|
1954
2041
|
// src/data/auction.ts
|
|
1955
|
-
import {
|
|
1956
|
-
|
|
1957
|
-
export const liveAuction = live(
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
}),
|
|
1965
|
-
);
|
|
2042
|
+
import { live } from '@solidjs/web/server-functions';
|
|
2043
|
+
|
|
2044
|
+
export const liveAuction = live(async function* (id: string) {
|
|
2045
|
+
'use server';
|
|
2046
|
+
yield await db.auctions.find(id); // current state first, from storage
|
|
2047
|
+
for await (const _change of db.auctions.subscribe(id)) {
|
|
2048
|
+
yield await db.auctions.find(id);
|
|
2049
|
+
}
|
|
2050
|
+
});
|
|
1966
2051
|
|
|
1967
2052
|
export async function saveBid(id: string, amount: number) {
|
|
1968
2053
|
'use server';
|
|
@@ -2029,8 +2114,8 @@ middleware written from older examples receives the event as `request`.
|
|
|
2029
2114
|
(`serverFunctions: { components: true }`) are experimental preview — do not use
|
|
2030
2115
|
them unless the project already has that flag on.
|
|
2031
2116
|
|
|
2032
|
-
Unscripted (no-JS) POST forms go through a router action —
|
|
2033
|
-
`const save =
|
|
2117
|
+
Unscripted (no-JS) POST forms go through a router form action —
|
|
2118
|
+
`const save = formAction(createTodo, "create-todo")` from `@solidjs/router`, then
|
|
2034
2119
|
`<form action={save} method="post">` — not a client `preventDefault` + `fetch`, not a
|
|
2035
2120
|
hand-built `/_server/` URL, and not `action={createTodo.url}`: a bare `"use server"`
|
|
2036
2121
|
function keeps its declared function type, so `.url` is a type error (TS2339) even
|
|
@@ -2078,9 +2163,9 @@ how to reuse. Prefer the form on the right.
|
|
|
2078
2163
|
| `const rest = { ...props }` | `const rest = omit(props, "label")` |
|
|
2079
2164
|
| `const user = createMemo(async () => { await fetch(...); return id(); })` | read `id()` **before** `await` |
|
|
2080
2165
|
| `action(async function* () { await save(); setX(v); })` | `yield save()` or `await save(); yield; setX(v)` |
|
|
2081
|
-
| `action(async (item) => { ... })` with core `action` from `solid-js` | `action(function* (item) { ...; yield ...; ... })` — only a generator re-enters the transaction (`solid2-kit check` flags this). Router `
|
|
2166
|
+
| `action(async (item) => { ... })` with core `action` from `solid-js` | `action(function* (item) { ...; yield ...; ... })` — only a generator re-enters the transaction (`solid2-kit check` flags this). Router `formAction` from `@solidjs/router` is the one that takes `async (form) => ...` |
|
|
2082
2167
|
| `todos()` on a store | `todos.length` / `todo.title` (property reads) |
|
|
2083
|
-
| `todos.push(x)` / `state.count++` / `todo.done = !todo.done` on the store itself | `setTodos((draft) => { draft.push(x); })` — writes to the store proxy outside a setter are
|
|
2168
|
+
| `todos.push(x)` / `state.count++` / `todo.done = !todo.done` on the store itself | `setTodos((draft) => { draft.push(x); })` — writes to the store proxy outside a setter are ignored (no error, tsc passes; dev warns `[STORE_WRITE_OUTSIDE_SETTER]` since rc.17 <!-- upstream:store-write-outside-setter -->) |
|
|
2084
2169
|
| `createStore({ selected: new Set<string>() })` + `draft.selected.add(id)` | `selected: {} as Record<string, true>` + `draft.selected[id] = true` / `delete draft.selected[id]`, or `draft.selected = new Set([...draft.selected, id])` — `Map` / `Set` / `Date` are stored as-is, not tracked |
|
|
2085
2170
|
| `draft.todos = fresh` (wholesale fresh-tree assignment) | `reconcile(fresh, "id")`, or function-form `createStore` / `createProjection` (auto-keyed by `"id"`). Removal via `setTodos((t) => t.filter(...))` is fine — survivors keep identity |
|
|
2086
2171
|
| `createEffect(() => user, (u) => { log(u.name); })` | `createEffect(() => user.name, (name) => { log(name); })` |
|
|
@@ -2095,8 +2180,12 @@ how to reuse. Prefer the form on the right.
|
|
|
2095
2180
|
| `dangerouslySetInnerHTML={{ __html }}` | `innerHTML={html()}` (sanitized); never with JSX children |
|
|
2096
2181
|
| `onClick={setCount}` | `onClick={() => setCount((c) => c + 1)}` |
|
|
2097
2182
|
| `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 --> |
|
|
2183
|
+
| `on:click={fn}` / `use:tooltip` / `attr:title` / `classList={{ on: x() }}` on a native element | `onClick`, a ref callback from a directive factory, `title`, `class={["btn", { on: x() }]}` — the 1.x forms are compile errors since rc.17 (`prop:` remains) <!-- upstream:removed-attribute-compile-error --> |
|
|
2184
|
+
| `value={props.name} onChange={(e) => props.onCommit(e.currentTarget.value)}` (bound to the committed value, no per-edit write) | a draft signal `createSignal(() => props.name)` written from `onInput`, or `defaultValue={props.name}` — the binding re-applies the committed value over uncommitted typing (`[UNCOMMITTED_INPUT_OVERWRITTEN]`); `equals: false` is one of its causes, not a fix <!-- upstream:uncommitted-input-overwritten --> |
|
|
2185
|
+
| `useDeferredValue(query())` / `startTransition(() => setQuery(q))` for an expensive derivation | `const rows = createDeferred(() => filter(all(), query()))` — an async memo that lags but never leads its inputs; `isPending(rows)` while it catches up. Unrelated to the 1.x `createDeferred(source, { timeoutMs })` <!-- upstream:create-deferred-export --> |
|
|
2098
2186
|
| `event.target.value` | `event.currentTarget.value` |
|
|
2099
2187
|
| `createContext(emptyStore)` for reactive state | `createContext<Todos>()` — dummy defaults silently no-op |
|
|
2188
|
+
| `createContext<Session | undefined>(undefined)` + `value={session()}` for an optional payload | `createContext<Session | null>(null)` + `value={session() ?? null}` — `undefined` means *not provided*: a provider whose value is `undefined` counts as none and the read throws `ContextNotFoundError` <!-- upstream:context-undefined-unprovided --> |
|
|
2100
2189
|
| `<For keyed={(t) => t.id}>{(todo) => todo.title}` | `todo().title` — key-function items are accessors |
|
|
2101
2190
|
| `{list().length ? <For each={list()}> : <Empty />}` | `<For each={list()} fallback={<Empty />}>` |
|
|
2102
2191
|
| `<For each={rows.slice(from, from + n)}>` | `<Repeat from={from()} count={n}>` |
|
|
@@ -2105,14 +2194,16 @@ how to reuse. Prefer the form on the right.
|
|
|
2105
2194
|
| `const r = await api.x(); if (!r.ok) ...` result-object checks at every read | throw unusable responses; contain each region with `<Errored>` |
|
|
2106
2195
|
| `GET(async (id) => { "use server"; await db.delete(id) })` | mutations stay on POST; `GET()` is for idempotent reads |
|
|
2107
2196
|
| `live()` yields as an event log to append | each yield **replaces** the current answer; yield current state first, read from the state mutations write to — a reconnect calls the function again, so generator locals or per-connection memory are not the source |
|
|
2108
|
-
| `GET(live(async function* () { ... }))` | `live(
|
|
2197
|
+
| `GET(live(async function* () { ... }))` | `live(async function* () { ... })` — `live()` implies `GET` since rc.17 (`live(GET(fn))` is the same; `live()` outermost) <!-- upstream:live-implies-get --> |
|
|
2109
2198
|
| a plain `async function*` `"use server"` stream for a value that must survive a dropped connection | `live()` — a plain stream is an event sequence on one connection (`for await`, accumulate); nothing reopens it when it drops |
|
|
2110
2199
|
| `yield saveBid(...)` then `refresh(auction)` on a store derived from `live()` | `yield saveBid(...); yield until(() => auction.highBid >= amount, { timeout })` — `live()` is outside revalidation/single-flight; `refresh` re-runs the derivation (closes the stream, calls the source again): a reconnect per mutation, and the old value still flashes back whenever the new first value predates the write |
|
|
2111
2200
|
| `<article innerHTML={html()}>…children…</article>` | `innerHTML` **or** children, not both |
|
|
2112
2201
|
| `fallback={(error) => <p>{error.message}</p>}` | `error` is an accessor: `error().message` (or `String(error())`) |
|
|
2113
|
-
| `import { action } from "solid-js"` on a `<form>` | `import {
|
|
2202
|
+
| `import { action } from "solid-js"` on a `<form>` | `import { formAction } from "@solidjs/router"` + `<form action={save} method="post">` (core `action` is a generator transaction, not a form URL) |
|
|
2203
|
+
| `import { action, useAction } from "@solidjs/router"` | `formAction` (`action` / `Action` are deprecated aliases since next.41, dev warns; `solid2-kit check` flags them); `useAction` is deprecated with no replacement — call the server function directly, the mounted router applies its revalidation and redirect <!-- upstream:router-action-deprecated --> |
|
|
2204
|
+
| `async function markRead(id) { "use server"; … }` called from a handler, no `revalidate` anywhere | fine — the mounted router treats every POST call as a mutation and revalidates the page's queries; a POST that changed nothing returns `respond(value, { revalidate: "" })`, reads go through `query()` / `GET()`, and `createRouter({ routes, singleFlight: false })` opts out <!-- upstream:router-post-mutation --> |
|
|
2114
2205
|
| `createMemo(async () => fetchUser(id()))` when `@solidjs/router` is present | `query(fetchUser, "users")` + `createMemo(() => getUser(id()))`; after mutations `revalidate(getUser.key)`, not core `refresh()` |
|
|
2115
|
-
| `onSubmit={(e) => { e.preventDefault(); fetch(...) }}` for a router/server action | `<form action={save} method="post">` (POST only; `.with(id)` binds args into the URL)
|
|
2206
|
+
| `onSubmit={(e) => { e.preventDefault(); fetch(...) }}` for a router/server action | `<form action={save} method="post">` (POST only; `.with(id)` binds args into the URL); outside a form, call the server function directly |
|
|
2116
2207
|
| `query(saveUser, "users")` wrapping a mutation | don't wrap mutations in `query` — an undeclared server function becomes GET |
|
|
2117
2208
|
| `render(<App />, root)` / Testing Library `render(<Counter />)` | `render(() => <App />, root)` — pass a function so Solid creates the root first |
|
|
2118
2209
|
| `render` onto SSR HTML / `hydrate` into an empty node | `hydrate(() => <App />, root)` when HTML already exists; `render` for an empty mount |
|
|
@@ -2142,8 +2233,8 @@ how to reuse. Prefer the form on the right.
|
|
|
2142
2233
|
| `createOptimistic` for a local edit draft | writable derivation / plain store. Optimistic is for an in-flight mutation |
|
|
2143
2234
|
| `createMemo(async fn, { loadingValue })` / `{ seedLoadingValue: true }` as the default first-flight UI | `<Loading>` for first flight; those options are escape hatches (store projections use `seedLoadingValue`) |
|
|
2144
2235
|
| `setSubmitted(true)` + an effect that watches it | do the work in the handler or an `action` |
|
|
2145
|
-
| `findUser(userId)` with the id from the client as identity | `const userId = getRequestEvent()?.locals.userId; if (!userId) throw respond(null, { status: 401 });` (`throw redirect("/sign-in")`
|
|
2146
|
-
| `action={"/_server/" + id}` / `action={createTodo.url}` on a POST form | a router action: `const save =
|
|
2236
|
+
| `findUser(userId)` with the id from the client as identity | `const userId = getRequestEvent()?.locals.userId; if (!userId) throw respond(null, { status: 401 });` (`throw redirect("/sign-in")` in a routed app) |
|
|
2237
|
+
| `action={"/_server/" + id}` / `action={createTodo.url}` on a POST form | a router form action: `const save = formAction(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) |
|
|
2147
2238
|
| `<form method="get" action={update.url}>` for a mutation | GET forms only for idempotent search; mutations are POST |
|
|
2148
2239
|
| `window.location.href = ...` / `history.pushState` | `useNavigate()` or `<a href={Router.paths...}>` |
|
|
2149
2240
|
| `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 --> |
|
|
@@ -2174,7 +2265,7 @@ how to reuse. Prefer the form on the right.
|
|
|
2174
2265
|
| a held write with no feedback anywhere | pair it with `isPending()` / `latest()` / an optimistic value / `affects()` — otherwise dev + attribution reports `[SILENT_HOLD]` |
|
|
2175
2266
|
| `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 |
|
|
2176
2267
|
| `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 |
|
|
2177
|
-
| `throw redirect("/sign-in")` / `return reload(...)` in a function called directly (core `action`, handler, plain memo) |
|
|
2268
|
+
| `throw redirect("/sign-in")` / `return reload(...)` in a function called directly (core `action`, handler, plain memo) in an app with no router | Solid Router `formAction` / `query`, no-JS form posts, and a mounted router's single-flight hook (any call, since next.41; the call resolves to `null`) apply them — with no router a direct caller resolves to the raw `Response`, typed as your data. There: `throw respond(null, { status: 401 })` or return a value and navigate in the caller |
|
|
2178
2269
|
| `submission.clear()` from an effect | on dismiss/resubmit only — from an effect, errors flash and vanish |
|
|
2179
2270
|
| `revalidate(liveSource)` | `live()` updates through the open stream; do not revalidate it |
|
|
2180
2271
|
| `refresh(user)` without `affects` when the reload should look pending | pair `affects(user)` with `refresh(user)` — a bare `refresh` re-asks quietly |
|
|
@@ -2182,13 +2273,13 @@ how to reuse. Prefer the form on the right.
|
|
|
2182
2273
|
| `setStore(async (draft) => { draft.x = await load(); })` | `const x = await load(); setStore((draft) => { draft.x = x; })` — the setter is a synchronous transaction; dev throws `[ASYNC_STORE_SETTER]` <!-- upstream:async-store-setter-throws --> |
|
|
2183
2274
|
| `setStore((d) => { d.items = seed; })` in a component body | seed through `createStore(seed)` / `createStore(() => props.items, fallback)` — a body-level store write is `[REACTIVE_WRITE_IN_OWNED_SCOPE]` <!-- upstream:store-write-owned-scope --> |
|
|
2184
2275
|
| `const saved = await api.save(); yield until(() => ...)` | `await api.save(); yield; yield until(() => ...)` — a bare `yield` re-enters the transaction before any reader is created, not only before writes |
|
|
2185
|
-
| `fallback={(err) => { captureException(err()); return <Fallback />; }}` | `configureClientErrors({ onError })` from `solid-js` (or per root `render(fn, root, undefined, { onError })`), `configureServerErrors({ onError })` from `@solidjs/web` — once per error, with `ownerPath` / `boundaryPath` |
|
|
2276
|
+
| `fallback={(err) => { captureException(err()); return <Fallback />; }}` | `configureClientErrors({ onError })` from `solid-js` (or per root `render(fn, root, undefined, { onError })`), `configureServerErrors({ onError })` from `@solidjs/web` — once per error (server: per error, kind and request), with `ownerPath` / `boundaryPath` |
|
|
2186
2277
|
| `latest(user) ?? placeholder` in a handler as a null-safe read | `latest()` throws `NotReadyError` before the first value in every scope; read settled values under `<Loading>`, or `await until(() => user())` <!-- upstream:latest-throws-unsettled --> |
|
|
2187
2278
|
| `attribution.why(total)` / `attribution.format(e)` / `attribution.costs()` | named imports from `solid-js/attribution`: `why(total)`, `formatRerun(e)`, `costs()`, `feedback()`, `subscriptions(scope)` (rc.9) <!-- upstream:attribution-named-exports --> |
|
|
2188
2279
|
| `attribution.enable()` unguarded in app code, or `enable()` with the default `log: true` left on | `if (isDev) attribution.enable({ log: false })` — on the observe build an unguarded `enable()` records in production; prefer the `/__solid/diagnostics` session (`begin` enables, `end` disables) so app code carries nothing |
|
|
2189
2280
|
| `solid({ observe: true })` added "to see diagnostics in dev" | `vite dev` is always the dev build; `observe: true` is the production-observability opt-in (observe condition + `sourceNames` for `vite build`/preview <!-- upstream:source-names-option -->) |
|
|
2190
2281
|
| `configureServerErrors({ onError: (e) => { report(e); return e; } })` | return nothing or a reference (`new Error(\`ref ${id}\`)`) — the return value replaces the sanitized wire value; the error itself leaks message/stack/secrets |
|
|
2191
|
-
| `import "./instrument"` at the top of the server entry for a monitoring SDK | `start: { instrument: "./src/instrument.ts" }` — ESM hoists static imports in dependency order, so the entry's own deps load first |
|
|
2282
|
+
| `import "./instrument"` at the top of the server entry for a monitoring SDK | `start: { instrument: "./src/instrument.ts" }` — ESM hoists static imports in dependency order, so the entry's own deps load first (next.51 wraps the `node` entry too: deploy `node.js` with `server.js`). Browser-side init before `hydrate` / `render` is `start: { clientSetup: "./src/client-setup.ts" }` |
|
|
2192
2283
|
| Express/middleware injecting `traceparent` / trace headers into responses | the runtime carries a sampled or provided trace on `Server-Timing` + `<meta>`; forward downstream with `getTraceContext()?.entries`; SDKs use `OBSERVE?.server.trace.provide` |
|
|
2193
2284
|
| `OBSERVE.records.subscribe("call", (e) => setStats(...))` | listeners run synchronously inside the runtime and must not write signals; collect into a plain buffer and drain from outside |
|
|
2194
2285
|
| `import { storePath } from "solid-js"` | gone from `solid-js` (rc.9); write draft setters `setState((d) => { d.user.name = "Grace"; })` <!-- upstream:store-path-removed --> |
|
|
@@ -2283,9 +2374,9 @@ const save = action(async function* (form: FormData) {
|
|
|
2283
2374
|
<button type="submit">Save</button>
|
|
2284
2375
|
</form>
|
|
2285
2376
|
|
|
2286
|
-
// CORRECT — router
|
|
2287
|
-
import {
|
|
2288
|
-
const save =
|
|
2377
|
+
// CORRECT — router formAction is POST-addressable (only if `@solidjs/router` is in the project)
|
|
2378
|
+
import { formAction } from '@solidjs/router';
|
|
2379
|
+
const save = formAction(async (form: FormData) => persist(form), 'save');
|
|
2289
2380
|
<form action={save} method="post">
|
|
2290
2381
|
<button type="submit">Save</button>
|
|
2291
2382
|
</form>
|
|
@@ -2330,20 +2421,8 @@ agent use the mirror, which serves every page as plain markdown:
|
|
|
2330
2421
|
- Any page: append `.md`, e.g. `https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/reactivity/create-signal.md`
|
|
2331
2422
|
|
|
2332
2423
|
A curated URL list is in [references/official-docs.md](references/official-docs.md).
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
- `routing/solid-router/navigation` lists `data-pending` among the attributes the router
|
|
2337
|
-
sets on its own; it needs `createRouter({ links: pendingLinks })`, and `aria-current`
|
|
2338
|
-
also compares the query (the link-state paragraph under Solid Router 2 above).
|
|
2339
|
-
<!-- upstream:docs-pending-links-stale -->
|
|
2340
|
-
- `reference/solid-js/reactivity/create-signal` says a write to a writable derivation
|
|
2341
|
-
wins over a same-tick recompute; a source change in the same update re-runs the
|
|
2342
|
-
function, which receives the write as `prev`. <!-- upstream:docs-derived-write-stale -->
|
|
2343
|
-
- `guides/performance` ("tracks 30 or more sources") and the attribution reference
|
|
2344
|
-
(`wideDeps`, "default 30") give the old `[WIDE_SCOPE_DEPS]` threshold; the default is
|
|
2345
|
-
200, and a list's insert pass is exempt from the check (see
|
|
2346
|
-
[Lists](#lists-for-child-signatures-per-keying-mode)). <!-- upstream:docs-wide-deps-stale -->
|
|
2424
|
+
Every diagnostic code has its own page under `reference/solid-js/diagnostics/<code-kebab>.md`
|
|
2425
|
+
(the console footer links it on a code's first report).
|
|
2347
2426
|
|
|
2348
2427
|
**Never** consult Solid 1.x sources (docs.solidjs.com, pre-2.0 tutorials, old Stack Overflow).
|
|
2349
2428
|
Solid 2.0 is a breaking rewrite; 1.x answers are wrong here. The banned-API table is in the
|
|
@@ -2389,7 +2468,7 @@ always-applied rules installed alongside this skill.
|
|
|
2389
2468
|
loading/error branches, disable optimistic rows until ack, or refetch in
|
|
2390
2469
|
`hydrate` / `onSettled`. HTTP does not enforce TypeScript — validate
|
|
2391
2470
|
inside `"use server"`; do not invent tRPC. `live()` owns the client connection,
|
|
2392
|
-
is declared `live(
|
|
2471
|
+
is declared `live(fn)` (it implies `GET`), and a mutation on a live-read value holds with
|
|
2393
2472
|
`yield until(...)` for the echo instead of `refresh()`.
|
|
2394
2473
|
A server bundle uses `handleRequest` from `dist/server/server.js`; client-only start
|
|
2395
2474
|
is static `dist/client`. Do not delay `renderToStream` for visual order
|
|
@@ -2429,8 +2508,10 @@ always-applied rules installed alongside this skill.
|
|
|
2429
2508
|
themselves with a per-row `<Show>` (no diagnostic catches it).
|
|
2430
2509
|
- [ ] If the project has a router: `createRouter({ routes })`, not JSX `<Route>` / `<A>`.
|
|
2431
2510
|
One instance, no nested `<Router>`. Navigate with `useNavigate` / `<a href>`, not
|
|
2432
|
-
`window.location`. Router `
|
|
2433
|
-
core `action`/`refresh
|
|
2511
|
+
`window.location`. Router `formAction`/`query` come from `@solidjs/router` (POST forms + cache), not
|
|
2512
|
+
core `action`/`refresh`; router `action` is a deprecated alias of `formAction` and
|
|
2513
|
+
`useAction` is deprecated <!-- upstream:router-action-deprecated --> (single flight is
|
|
2514
|
+
the router's — call server functions directly). Forms: `<form action={save} method="post">`. Core `action`
|
|
2434
2515
|
bodies are generators (`function*` / `async function*`) suspending on `yield`,
|
|
2435
2516
|
never plain `async` functions. Link state is automatic attributes + CSS
|
|
2436
2517
|
(`aria-current` / `data-active`, `data-pending` via `links: pendingLinks`;
|
|
@@ -122,6 +122,7 @@ verify API names against the reference pages above.
|
|
|
122
122
|
|
|
123
123
|
- https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/reactivity/create-signal.md
|
|
124
124
|
- https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/reactivity/create-memo.md
|
|
125
|
+
- https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/reactivity/create-deferred.md
|
|
125
126
|
- https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/reactivity/create-effect.md
|
|
126
127
|
- https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/reactivity/create-optimistic.md
|
|
127
128
|
- https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/reactivity/flush.md
|
|
@@ -151,7 +152,11 @@ verify API names against the reference pages above.
|
|
|
151
152
|
## Reference: diagnostics, attribution, and observability
|
|
152
153
|
|
|
153
154
|
`DEV` is dev builds only; `OBSERVE` and attribution exist on observe and dev builds;
|
|
154
|
-
the error hooks fire on every build.
|
|
155
|
+
the error hooks fire on every build. Every diagnostic code has a page of its own —
|
|
156
|
+
`reference/solid-js/diagnostics/<code-in-kebab-case>.md` (for example
|
|
157
|
+
`.../diagnostics/silent-hold.md`), the page the console footer links on a code's first report.
|
|
158
|
+
|
|
159
|
+
- https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/diagnostics.md
|
|
155
160
|
|
|
156
161
|
- https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/advanced/diagnostics-dev-hooks/dev.md
|
|
157
162
|
- https://v2-rebuild--solid-docs-v2.netlify.app/reference/solid-js/advanced/diagnostics-dev-hooks/observe.md
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "solid2-agent-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.1",
|
|
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",
|