44
55## Project overview
66
7- The package ` solid-querybuilder ` is a Solid 1.x/2.x port of
7+ The package ` solid-querybuilder ` is a ** Solid 2.0 ** port of
88[ React Query Builder] ( https://react-querybuilder.js.org ) , built on the published
99` @react-querybuilder/core ` . The port's defining constraint is ** full DOM parity** : tag name,
1010document order, ` data-testid ` , ` data-path ` , and byte-identical ` class ` attributes must match
@@ -19,11 +19,17 @@ solid-querybuilder/
1919│ ├── src/ # components, reactive layer, types, styles
2020│ ├── test/conformance/ # DOM-parity harness (fixtures gitignored)
2121│ └── scripts/ # build/check/ssr-smoke helpers
22- └── examples/ # demo (Vite) and a SolidStart SSR gate
22+ └── examples/ # demo (Vite) and an SSR gate
2323```
2424
25+ ** Target is Solid 2 only.** Peers are ` solid-js@^2.0.0-beta.32 ` ** and
26+ ` @solidjs/web@^2.0.0-beta.32 ` ** — in Solid 2 the DOM runtime is its own package. There is no
27+ ` ^1.9 ` leg anywhere: in the manifest, in CI, or in the source. A v1-target port, if it ever
28+ happens, is a separate repo publishing as ` @react-querybuilder/solid1 ` ; do not add compatibility
29+ shims or ` solid-js@1 ` code paths here.
30+
2531Wiring strategy is ** hybrid** : ` QueryManager ` owns every write (history, guards, ` reconfigure ` );
26- an internal ` createStore ` mirror (` reconcile ` d by ` id ` ) is the read path. See
32+ an internal store mirror (` reconcile ` d by ` id ` ) is the read path. See
2733` ~/git/SOLID_QB_PLAN.md ` for the full rationale and step-by-step plan; this file only records the
2834standing rules that apply to every step.
2935
@@ -34,9 +40,11 @@ standing rules that apply to every step.
3440- ` bun run test ` / ` bun run test:coverage ` — Vitest. ** Never ` bun test ` ** ; that is Bun's builtin
3541 runner and bypasses Vitest entirely.
3642- ` bun run conformance ` — fetch fixtures, then run the DOM-parity suites
43+ - ` bun run check:versions ` — asserts the resolved prerelease toolchain has not drifted; runs
44+ first in CI
3745- ` bun run test:ssr ` — resolves the ` solid ` export condition and renders through
38- ` renderToStringAsync ` ; a real gate from step 1, superseded (but not replaced) by the SolidStart
39- gate at step 8
46+ ` renderToString ` ; a real gate from step 1, superseded (but not replaced) by the SSR gate at
47+ step 8
4048- ` bun run check ` — ` tsc --noEmit `
4149- ` bun run lint ` , ` bun run fmt ` , ` bun run fmt:check `
4250- ` bun run check:all ` — everything CI runs
@@ -59,22 +67,26 @@ A key _lookup_ (`exports['.'].solid`) is order-blind and does not gate anything
5967regress it back to that.
6068
6169Keep the script even after step 8 supersedes it with the SolidStart gate; it is the only thing
62- that checks the export condition in isolation.
70+ that checks the export condition in isolation. (There is no Solid 2 SolidStart yet — step 8's
71+ gate is a plain Vite SSR example — but the export condition is what that gate rests on either
72+ way.)
6373
6474### The SSR smoke test runs one Solid instance
6575
66- ` scripts/ssr-smoke-entry.jsx ` imports ** both** ` solid-js /web` and the library, and is loaded
76+ ` scripts/ssr-smoke-entry.jsx ` imports ** both** ` @solidjs /web` and the library, and is loaded
6777through ` vite.ssrLoadModule ` . That is load-bearing: ` ssr.noExternal ` gives Vite's module graph its
68- own copy of ` solid-js ` , so importing ` renderToStringAsync ` in the host process instead would
69- render with a _ different instance_ than the component was compiled against. Solid keeps
78+ own copy of ` solid-js ` , so importing ` renderToString ` in the host process instead would render
79+ with a _ different instance_ than the component was compiled against. Solid keeps
7080owner/` sharedConfig ` state at module scope, so the copies do not share it — a trivial component
7181survives this, but anything using ` createContext ` /` createStore ` /` createEffect ` (i.e.
7282` QueryBuilder ` , from step 4) does not.
7383
74- Vite's ` ssr.resolve.conditions ` must be ` ['solid', 'node', 'development'] ` : ` solid ` so the library
75- resolves to its raw-JSX entry, ** ` node ` so ` solid-js/web ` resolves to its server build** . Listing
76- ` solid ` alone clobbers Vite's defaults and hands back the browser build, whose
77- ` renderToStringAsync ` is a stub that throws. Never add ` browser ` .
84+ ** Conditions are the plugin's job now, not the config's.** ` vite-plugin-solid@3 ` gives the ssr
85+ environment ` ['solid', 'development', 'module', 'node', 'development|production'] ` on its own, so
86+ the script sets ** no** ` ssr.resolve.conditions ` . Hand-maintaining a list on top of that only
87+ _ removes_ entries. The failure it guards against is unchanged: ` @solidjs/web ` 's exports map lists
88+ ` browser ` ** before** ` node ` , so any condition set carrying ` browser ` hands back the browser build,
89+ whose ` renderToString ` is a stub. Never add ` browser ` .
7890
7991The entry is ` .jsx ` , not ` .tsx ` , deliberately: it stays out of the typecheck project so
8092` bun run check ` does not depend on ` dist/ ` existing.
@@ -86,21 +98,53 @@ specifiers into the emitted `.d.ts` verbatim. `check-dist-specifiers.ts` additio
8698` ./foo.js ` specifier in a ` .d.ts ` to resolve to a sibling ` foo.d.ts ` with no ` foo.js ` beside it (a
8799type-only module erased by the bundler), and allows ` ./foo.jsx ` under ` dist/source ` .
88100
89- ### Reactivity
90-
91- - ** Never destructure props.** ` splitProps ` /` mergeProps ` only. A destructure at the top of a
92- component silently severs reactivity and passes every type check. This is the single most likely
93- Solid-specific defect class — check it at review of every component.
94- - ** ` unwrap() ` before handing anything to the manager.** The manager's Immer deep-freeze rejects a
95- store proxy.
96- - ** ` unwrap() ` the manager itself** before reading its history (` UndoRedoActions ` ).
97- ` QueryManager ` keeps history in private class fields, which a ` Proxy ` cannot read through.
98- - Effects that write back into state use ` createEffect(on([...explicit deps], ...)) ` , never a bare
99- auto-tracking effect — the tracked set changing across branches is exactly the loop failure mode.
100- Writes go through ` untrack ` , plus a re-entrancy flag.
101+ ### Reactivity (Solid 2)
102+
103+ - ** Never destructure props.** ` merge ` /` omit ` only — Solid 2's replacements for
104+ ` mergeProps ` /` splitProps ` . A destructure at the top of a component silently severs reactivity
105+ and passes every type check. This is the single most likely Solid-specific defect class — check
106+ it at review of every component.
107+ - ⚠️ ** ` merge ` treats an explicit ` undefined ` as a real value** and overrides with it, where 1.x's
108+ ` mergeProps ` skipped it. Every ` merge(defaults, props) ` is therefore a latent defaults-erasure
109+ bug. A _ missing_ key still falls through. Where "skip undefined" is wanted, filter explicitly,
110+ or prefer core's ` preferProp ` /` preferFlagProps ` . ` merge ` is lazy (getters), not a snapshot.
111+ - ** ` snapshot() ` — not ` unwrap() ` — before handing anything to the manager.** The manager's Immer
112+ deep-freeze rejects a store proxy. Likewise ` snapshot() ` the manager itself before reading its
113+ history (` UndoRedoActions ` ): ` QueryManager ` keeps history in private class fields, which a
114+ ` Proxy ` cannot read through.
115+ - ** Split effects, not ` on() ` .** ` on() ` is gone; ` createEffect(compute, apply) ` makes the compute
116+ phase the dependency declaration, so the old "always use ` on ` " rule is now enforced by the API
117+ shape. Deps in compute, writes in apply. ` { defer: true } ` survives as an option.
118+ - ** Apply-phase writes are legal — no ` ownedWrite ` needed** (proven at step 1.5). The owned-write
119+ rule rejects writes made while an owner is on the stack, and the apply phase is unowned. Note
120+ ` ownedWrite ` is a ** signal** option, not an effect option; ` createEffect ` has no such option.
121+ - ⚠️ ** The body of ` createRoot(fn) ` IS an owned scope.** A write there throws
122+ ` REACTIVE_WRITE_IN_OWNED_SCOPE ` . Test harnesses must set up inside the root and write from
123+ outside it.
124+ - ⚠️ ** A split effect's apply callback must return a cleanup function or ` undefined ` .** `v =>
125+ setX(v)` returns the setter's return value and throws "invalid cleanup value". Use a block body.
126+ - ⚠️ ** An uncaught error inside an effect halts the entire reactive system** (` REACTIVITY_HALTED ` )
127+ for the rest of the module. Intentionally-throwing tests need their own file.
128+ - ** ` batch ` is gone; reads lag writes.** Not just effects — a plain ` signal() ` read after
129+ ` setSignal() ` still returns the _ old_ value until the next microtask or an explicit ` flush() ` .
130+ Every write-then-read must ` flush() ` first. Never paper over this with ` setTimeout ` or tick
131+ counts. ` @solidjs/testing-library ` 's ` render ` populates the container synchronously; subsequent
132+ updates need ` flush() ` .
101133- Return getter objects (not objects of accessors, not memoized fresh objects) from composables
102134 whose result is read once by a Solid context or passed as a prop.
103135
136+ ### Store mirror
137+
138+ ` createStore ` and ` reconcile ` are exported from ** ` solid-js ` ** now, not ` solid-js/store ` .
139+
140+ - ` reconcile(value, key?) ` — ` key ` is the ** 2nd positional argument** and defaults to ` 'id' ` ,
141+ which is exactly what this port needs. The 1.x ` { key, merge } ` options object is not the 2.0
142+ shape and throws.
143+ - ` createProjection(fn, seed, options?) ` is a derived, ** read-only** store with the same ` 'id' `
144+ default key. It can be driven from a non-reactive external source (the manager's subscribe
145+ callback) by bumping a version signal from that callback and reading the signal in ` fn ` — proven
146+ at step 1.5.
147+
104148### DOM parity
105149
106150- Build class strings with core's ` clsx ` exclusively. Never template interpolation.
@@ -110,6 +154,9 @@ type-only module erased by the bundler), and allows `./foo.jsx` under `dist/sour
110154
111155### Types
112156
157+ - ** ` jsxImportSource ` is ` "@solidjs/web" ` .** ` solid-js@2 ` owns no JSX namespace and no
158+ ` jsx-runtime ` . ` JSX ` and ` ComponentProps ` import from ` @solidjs/web ` ; ` Component ` stays on
159+ ` solid-js ` .
113160- ` ReactNode ` → ` LabelNode ` (` JSX.Element | string ` ); titles stay ` string ` .
114161- ` ComponentType<P> ` → Solid's ` Component<P> ` .
115162- Use ` import type ` for type-only imports (` verbatimModuleSyntax ` is on).
@@ -128,15 +175,22 @@ type-only module erased by the bundler), and allows `./foo.jsx` under `dist/sour
128175** Standing rule: every gate must be proven to fail.** When a step adds a gate, deliberately break
129176it, record that it went red, then revert. A gate that cannot fail is worse than none.
130177
131- Current gates (step 1): ` fmt:check ` , ` build ` , ` check ` , ` check:exports ` , ` lint ` , ` test:coverage `
132- (global 80% lines — vacuous until step 3 adds real executable code in ` src/reactive/ ` ), ` test:ssr ` .
133- (` conformance ` is a stub that exits 0 until step 6; it is not a gate yet.)
178+ Current gates (step 1.5): ` check:versions ` , ` fmt:check ` , ` build ` , ` check ` , ` check:exports ` ,
179+ ` lint ` , ` test:coverage ` (global 80% lines — vacuous until step 3 adds real executable code in
180+ ` src/reactive/ ` ), ` test:ssr ` . (` conformance ` is a stub that exits 0 until step 6; it is not a
181+ gate yet.)
134182
135183All five were proven red at step 1 and reverted: coverage (threshold to 99 + an injected
136184uncovered function), export-condition ** order** (` import ` moved first), export-condition
137185** target** (` solid ` repointed at ` dist/index.js ` ), the SSR ** markup** assertion (component's label
138186dropped), and ` check-dist-specifiers ` (a directory import appended to ` dist/index.d.ts ` ).
139187
188+ ** Four of the five were re-proved red on the Solid 2 toolchain at step 1.5** — a gate proved red
189+ under Solid 1 is not evidence about Solid 2, since the plugin, the resolver behavior and the SSR
190+ renderer all changed. Coverage, condition ** order** (both layers fired), condition ** target** , and
191+ SSR ** markup** under the new synchronous ` renderToString ` . ` check-dist-specifiers ` is unaffected by
192+ the runtime swap; it was re-run against the rebuilt ` dist ` instead.
193+
140194⚠️ Two assertion shapes that look like gates but are not, both found and removed in review — do
141195not reintroduce them:
142196
@@ -163,5 +217,5 @@ is how CI runs it.
163217
164218## Repo status
165219
166- ** Local only .** No ` git init ` , no GitHub remote. ` .gitignore ` and ` .github/workflows/ci.yml ` exist
167- so they are in place whenever the repo is initialized .
220+ ** Remote-less .** The repo is a local git repo with no GitHub remote and nothing pushed.
221+ ` .github/workflows/ci.yml ` exists so it is in place whenever a remote is added .
0 commit comments