brustjs 0.1.29-alpha → 0.1.31-alpha

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.
@@ -12,29 +12,20 @@ import { Plus } from 'lucide-react'
12
12
  import { client } from 'brustjs/client'
13
13
  import { computed, signal } from 'brustjs/store'
14
14
  import type { Actions } from '../actions'
15
- import type { TeamMember } from '../lib/types'
15
+ import type { AddTeamProps, TeamMember } from '../lib/types'
16
16
  import { teamStore } from '../stores/team'
17
17
 
18
18
  const api = client<Actions>()
19
19
 
20
- interface AddToTeamProps {
21
- id: number
22
- name: string
23
- displayName: string
24
- num: string
25
- types: string[]
26
- artwork: string
27
- }
28
-
29
20
  const BASE =
30
21
  'inline-flex w-full items-center justify-center rounded-lg bg-brand-500 px-4 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-brand-600 disabled:opacity-50'
31
22
  const IN_TEAM =
32
23
  'inline-flex w-full items-center justify-center rounded-lg bg-slate-200 px-4 py-2.5 text-sm font-semibold text-slate-700 transition-colors disabled:opacity-50 dark:bg-slate-700 dark:text-slate-100'
33
24
 
34
25
  // behavior → client bundle, registered as "addToTeamButton" (camelCase filename).
35
- // `props` is the JSON parsed out of the element's x-props attribute (precomputed
36
- // by the loader as a JSON string — native templates can't call JSON.stringify).
37
- export const behavior = ({ props }: { props: AddToTeamProps }) => {
26
+ // `props` is the object parsed out of the element's x-props attribute (the
27
+ // compiler serializes the loader's structured object via `json_attr`).
28
+ export const behavior = ({ props }: { props: AddTeamProps }) => {
38
29
  const busy = signal(false)
39
30
  const full = signal(false)
40
31
  const inTeam = computed(() =>
@@ -83,9 +74,9 @@ export const behavior = ({ props }: { props: AddToTeamProps }) => {
83
74
 
84
75
  // default → jinja (server). The x-* directives are static string attributes the
85
76
  // native compiler passes straight through; the directive runtime binds them to
86
- // the behavior instance on the client. `data` is the loader-precomputed JSON
87
- // string, emitted by the compiler as x-props="{{ (data) | e }}" (XSS-safe).
88
- export default function AddToTeamButton({ data }: { data: string }) {
77
+ // the behavior instance on the client. `data` is the loader's structured object,
78
+ // emitted by the compiler as x-props="{{ (data) | json_attr }}" (XSS-safe).
79
+ export default function AddToTeamButton({ data }: { data: AddTeamProps }) {
89
80
  return (
90
81
  <div x-data="addToTeamButton" x-props={data} className="relative">
91
82
  <Plus
@@ -20,25 +20,39 @@ interface Card {
20
20
  }
21
21
 
22
22
  export const behavior = ({ props }: { el: HTMLElement; props: unknown }) => {
23
- const all = ((props as { items?: Card[] })?.items ?? []) as Card[]
23
+ const all = ((props as Card[]) ?? []) as Card[]
24
+ const items = signal(all) // seeded to the SAME data as the SSR {% for %}
24
25
  const q = signal('')
25
26
  const sortAz = signal(false)
26
- const filtered = computed(() => {
27
+ const apply = () => {
27
28
  const needle = q().trim().toLowerCase()
28
29
  let out = needle ? all.filter((c) => c.name.includes(needle)) : all.slice()
29
30
  if (sortAz()) out = out.slice().sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))
30
- return out
31
- })
32
- const onInput = (e: Event) => q.set((e.target as HTMLInputElement).value)
33
- const setDex = () => sortAz.set(false)
34
- const setAz = () => sortAz.set(true)
35
- const countLabel = computed(() => `${filtered().length} / ${all.length}`)
36
- return { q, sortAz, filtered, onInput, setDex, setAz, countLabel }
31
+ items.set(out)
32
+ }
33
+ const onInput = (e: Event) => {
34
+ q.set((e.target as HTMLInputElement).value)
35
+ apply()
36
+ }
37
+ const setDex = () => {
38
+ sortAz.set(false)
39
+ apply()
40
+ }
41
+ const setAz = () => {
42
+ sortAz.set(true)
43
+ apply()
44
+ }
45
+ const countLabel = computed(() => `${items().length} / ${all.length}`)
46
+ return { items, q, sortAz, onInput, setDex, setAz, countLabel }
37
47
  }
38
48
 
39
- export default function DexFilter({ data }: { data?: string }) {
49
+ // `items` (the loader array) feeds BOTH the SSR list AND the client behavior: the
50
+ // idiomatic `.map()` + bare `x-for` desugars to the `{% for c in items %}` adopt
51
+ // seed, and `x-props={items}` serializes the SAME array (json_attr) into the
52
+ // behavior's props — one prop, no separate pre-stringified `dexProps`.
53
+ export default function DexFilter({ items }: { items: Card[] }) {
40
54
  return (
41
- <section x-data="dexFilter" x-props={data}>
55
+ <section x-data="dexFilter" x-props={items}>
42
56
  <div className="mb-6 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
43
57
  <div className="relative w-full sm:max-w-xs">
44
58
  <Search
@@ -80,28 +94,27 @@ export default function DexFilter({ data }: { data?: string }) {
80
94
  </div>
81
95
 
82
96
  <div className="grid grid-cols-2 gap-3 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6">
83
- {/* biome-ignore lint/a11y/useValidAnchor: href is bound at hydration via x-bind-href (the template clone gets c.detailHref) */}
84
- <a
85
- x-for="c in filtered by c.id"
86
- x-bind-href="c.detailHref"
87
- className="group flex flex-col items-center rounded-2xl border border-slate-200 bg-white p-3 no-underline shadow-sm transition-all hover:-translate-y-0.5 hover:border-brand-500/50 hover:shadow-md dark:border-slate-800 dark:bg-slate-900"
88
- >
89
- <span
90
- x-text="c.num"
91
- className="self-start text-[11px] font-semibold tabular-nums text-slate-400"
92
- />
93
- {/* biome-ignore lint/a11y/useAltText: alt is bound at hydration via x-bind-alt (c.displayName) */}
94
- <img
95
- x-bind-src="c.artwork"
96
- x-bind-alt="c.displayName"
97
- loading="lazy"
98
- className="h-24 w-24 object-contain transition-transform group-hover:scale-110"
99
- />
100
- <div
101
- x-text="c.displayName"
102
- className="mt-1 text-sm font-semibold text-slate-800 dark:text-slate-100"
103
- />
104
- </a>
97
+ {items.map((c) => (
98
+ <a
99
+ x-for
100
+ key={c.id}
101
+ href={c.detailHref}
102
+ className="group flex flex-col items-center rounded-2xl border border-slate-200 bg-white p-3 no-underline shadow-sm transition-all hover:-translate-y-0.5 hover:border-brand-500/50 hover:shadow-md dark:border-slate-800 dark:bg-slate-900"
103
+ >
104
+ <span className="self-start text-[11px] font-semibold tabular-nums text-slate-400">
105
+ {c.num}
106
+ </span>
107
+ <img
108
+ src={c.artwork}
109
+ alt={c.displayName}
110
+ loading="lazy"
111
+ className="h-24 w-24 object-contain transition-transform group-hover:scale-110"
112
+ />
113
+ <div className="mt-1 text-sm font-semibold text-slate-800 dark:text-slate-100">
114
+ {c.displayName}
115
+ </div>
116
+ </a>
117
+ ))}
105
118
  </div>
106
119
  </section>
107
120
  )
@@ -91,7 +91,7 @@ export async function browseLoader({ req }: LoaderCtx): Promise<BrowseData> {
91
91
  }))
92
92
  return {
93
93
  ...chrome(req, 'Pokédex · Browse', 'Pokédex'),
94
- dexProps: JSON.stringify({ items }),
94
+ items,
95
95
  }
96
96
  }
97
97
 
@@ -181,16 +181,16 @@ export async function detailLoader({
181
181
  hasAbilities: abilities.length > 0,
182
182
  evolution,
183
183
  hasEvolution: evolution.length > 1,
184
- // Native templates can't call JSON.stringify, so precompute the x-props JSON
185
- // here. AddToTeamButton's prop contract is unchanged.
186
- addProps: JSON.stringify({
184
+ // The compiler serializes this structured object into AddToTeamButton's
185
+ // x-props via `json_attr`; the behavior receives it as a parsed object.
186
+ addProps: {
187
187
  id: p.id,
188
188
  name: p.name,
189
189
  displayName: cap(p.name),
190
190
  num: pad(p.id),
191
191
  types: p.types,
192
192
  artwork: p.artwork,
193
- }),
193
+ },
194
194
  }
195
195
  }
196
196
 
@@ -216,14 +216,14 @@ function emptyDetail(req: BrustRequest, name: string): DetailData {
216
216
  hasAbilities: false,
217
217
  evolution: [],
218
218
  hasEvolution: false,
219
- addProps: JSON.stringify({
219
+ addProps: {
220
220
  id: 0,
221
221
  name,
222
222
  displayName: cap(name),
223
223
  num: '',
224
224
  types: [],
225
225
  artwork: '',
226
- }),
226
+ },
227
227
  }
228
228
  }
229
229
 
@@ -84,10 +84,23 @@ export interface HomeData extends ChromeData {
84
84
  typeTiles: TypeTileVM[]
85
85
  }
86
86
 
87
- /** Browse (dex grid) page data. `dexProps` is the loader-precomputed JSON
88
- * string handed to the DexFilter native directive (gen-1 grid, keyed x-for). */
87
+ /** Browse (dex grid) page data. `items` feeds BOTH the SSR `{% for %}` and the
88
+ * DexFilter directive's `x-props` (json_attr-serialized) — one array, no separate
89
+ * pre-stringified JSON. */
89
90
  export interface BrowseData extends ChromeData {
90
- dexProps: string
91
+ items: DexCard[] // SSR {% for %} source + serialized x-props (one prop)
92
+ }
93
+
94
+ /** The structured x-props payload for the AddToTeamButton native directive. The
95
+ * compiler serializes it via `json_attr`; the behavior reads it as an object.
96
+ * `types` mirrors the loader's raw `p.types` (string keys). */
97
+ export interface AddTeamProps {
98
+ id: number
99
+ name: string
100
+ displayName: string
101
+ num: string
102
+ types: string[]
103
+ artwork: string
91
104
  }
92
105
 
93
106
  /** Detail page data. Every formatted string / className / inline-style value /
@@ -112,7 +125,7 @@ export interface DetailData extends ChromeData {
112
125
  hasAbilities: boolean
113
126
  evolution: EvoStageVM[]
114
127
  hasEvolution: boolean
115
- addProps: string // loader-precomputed x-props JSON for AddToTeamButton
128
+ addProps: AddTeamProps // structured x-props for AddToTeamButton (json_attr-serialized)
116
129
  }
117
130
 
118
131
  /** One cell of the type chart. */
@@ -1,14 +1,15 @@
1
1
  // Native leaf route "/pokedex" — the dex grid. Renders into AppLayout's
2
2
  // <Outlet/> slot (chrome lives in AppLayout). SINGLE return, NO local bindings.
3
3
  //
4
- // The grid itself is the DexFilter native directive: the loader precomputes the
5
- // full gen-1 item list as the `dexProps` JSON string, handed to DexFilter via
6
- // the native `data` prop emitted as x-props. DexFilter owns search/sort and
7
- // the keyed x-for reconcile.
4
+ // The grid itself is the DexFilter native directive. The loader provides the
5
+ // gen-1 item list ONCE as `items` (the array): it feeds the SSR `{% for c in items %}`
6
+ // seed (151 cards paint server-side) AND `x-props={items}` (json_attr-serialized for
7
+ // the behavior). DexFilter seeds a signal from props + owns search/sort + the keyed
8
+ // x-for reconcile that adopts the SSR'd nodes.
8
9
  import DexFilter from '../components/DexFilter'
9
10
  import type { BrowseData } from '../lib/types'
10
11
 
11
- export default function BrowsePage({ dexProps }: BrowseData) {
12
+ export default function BrowsePage({ items }: BrowseData) {
12
13
  return (
13
14
  <section className="py-2">
14
15
  <div className="mb-6">
@@ -24,7 +25,7 @@ export default function BrowsePage({ dexProps }: BrowseData) {
24
25
  , no full re-render.
25
26
  </p>
26
27
  </div>
27
- <DexFilter native data={dexProps} />
28
+ <DexFilter native items={items} />
28
29
  </section>
29
30
  )
30
31
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brustjs",
3
- "version": "0.1.29-alpha",
3
+ "version": "0.1.31-alpha",
4
4
  "description": "Bun + Rust SSR framework — React on the server, Rust everywhere else (napi cdylib + per-worker SharedArrayBuffer).",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -40,12 +40,12 @@
40
40
  "typescript": "^6.0.3"
41
41
  },
42
42
  "optionalDependencies": {
43
- "brustjs-darwin-x64": "0.1.29-alpha",
44
- "brustjs-darwin-arm64": "0.1.29-alpha",
45
- "brustjs-linux-x64-gnu": "0.1.29-alpha",
46
- "brustjs-linux-arm64-gnu": "0.1.29-alpha",
47
- "brustjs-linux-x64-musl": "0.1.29-alpha",
48
- "brustjs-linux-arm64-musl": "0.1.29-alpha"
43
+ "brustjs-darwin-x64": "0.1.31-alpha",
44
+ "brustjs-darwin-arm64": "0.1.31-alpha",
45
+ "brustjs-linux-x64-gnu": "0.1.31-alpha",
46
+ "brustjs-linux-arm64-gnu": "0.1.31-alpha",
47
+ "brustjs-linux-x64-musl": "0.1.31-alpha",
48
+ "brustjs-linux-arm64-musl": "0.1.31-alpha"
49
49
  },
50
50
  "peerDependencies": {
51
51
  "react": "^19.2.6",
package/runtime/index.js CHANGED
@@ -77,8 +77,8 @@ function requireNative() {
77
77
  try {
78
78
  const binding = require('brustjs-android-arm64')
79
79
  const bindingPackageVersion = require('brustjs-android-arm64/package.json').version
80
- if (bindingPackageVersion !== '0.1.29-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
81
- throw new Error(`Native binding package version mismatch, expected 0.1.29-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
80
+ if (bindingPackageVersion !== '0.1.31-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
81
+ throw new Error(`Native binding package version mismatch, expected 0.1.31-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
82
82
  }
83
83
  return binding
84
84
  } catch (e) {
@@ -93,8 +93,8 @@ function requireNative() {
93
93
  try {
94
94
  const binding = require('brustjs-android-arm-eabi')
95
95
  const bindingPackageVersion = require('brustjs-android-arm-eabi/package.json').version
96
- if (bindingPackageVersion !== '0.1.29-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
97
- throw new Error(`Native binding package version mismatch, expected 0.1.29-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
96
+ if (bindingPackageVersion !== '0.1.31-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
97
+ throw new Error(`Native binding package version mismatch, expected 0.1.31-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
98
98
  }
99
99
  return binding
100
100
  } catch (e) {
@@ -114,8 +114,8 @@ function requireNative() {
114
114
  try {
115
115
  const binding = require('brustjs-win32-x64-gnu')
116
116
  const bindingPackageVersion = require('brustjs-win32-x64-gnu/package.json').version
117
- if (bindingPackageVersion !== '0.1.29-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
118
- throw new Error(`Native binding package version mismatch, expected 0.1.29-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
117
+ if (bindingPackageVersion !== '0.1.31-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
118
+ throw new Error(`Native binding package version mismatch, expected 0.1.31-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
119
119
  }
120
120
  return binding
121
121
  } catch (e) {
@@ -130,8 +130,8 @@ function requireNative() {
130
130
  try {
131
131
  const binding = require('brustjs-win32-x64-msvc')
132
132
  const bindingPackageVersion = require('brustjs-win32-x64-msvc/package.json').version
133
- if (bindingPackageVersion !== '0.1.29-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
134
- throw new Error(`Native binding package version mismatch, expected 0.1.29-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
133
+ if (bindingPackageVersion !== '0.1.31-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
134
+ throw new Error(`Native binding package version mismatch, expected 0.1.31-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
135
135
  }
136
136
  return binding
137
137
  } catch (e) {
@@ -147,8 +147,8 @@ function requireNative() {
147
147
  try {
148
148
  const binding = require('brustjs-win32-ia32-msvc')
149
149
  const bindingPackageVersion = require('brustjs-win32-ia32-msvc/package.json').version
150
- if (bindingPackageVersion !== '0.1.29-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
151
- throw new Error(`Native binding package version mismatch, expected 0.1.29-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
150
+ if (bindingPackageVersion !== '0.1.31-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
151
+ throw new Error(`Native binding package version mismatch, expected 0.1.31-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
152
152
  }
153
153
  return binding
154
154
  } catch (e) {
@@ -163,8 +163,8 @@ function requireNative() {
163
163
  try {
164
164
  const binding = require('brustjs-win32-arm64-msvc')
165
165
  const bindingPackageVersion = require('brustjs-win32-arm64-msvc/package.json').version
166
- if (bindingPackageVersion !== '0.1.29-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
167
- throw new Error(`Native binding package version mismatch, expected 0.1.29-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
166
+ if (bindingPackageVersion !== '0.1.31-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
167
+ throw new Error(`Native binding package version mismatch, expected 0.1.31-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
168
168
  }
169
169
  return binding
170
170
  } catch (e) {
@@ -182,8 +182,8 @@ function requireNative() {
182
182
  try {
183
183
  const binding = require('brustjs-darwin-universal')
184
184
  const bindingPackageVersion = require('brustjs-darwin-universal/package.json').version
185
- if (bindingPackageVersion !== '0.1.29-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
186
- throw new Error(`Native binding package version mismatch, expected 0.1.29-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
185
+ if (bindingPackageVersion !== '0.1.31-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
186
+ throw new Error(`Native binding package version mismatch, expected 0.1.31-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
187
187
  }
188
188
  return binding
189
189
  } catch (e) {
@@ -198,8 +198,8 @@ function requireNative() {
198
198
  try {
199
199
  const binding = require('brustjs-darwin-x64')
200
200
  const bindingPackageVersion = require('brustjs-darwin-x64/package.json').version
201
- if (bindingPackageVersion !== '0.1.29-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
202
- throw new Error(`Native binding package version mismatch, expected 0.1.29-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
201
+ if (bindingPackageVersion !== '0.1.31-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
202
+ throw new Error(`Native binding package version mismatch, expected 0.1.31-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
203
203
  }
204
204
  return binding
205
205
  } catch (e) {
@@ -214,8 +214,8 @@ function requireNative() {
214
214
  try {
215
215
  const binding = require('brustjs-darwin-arm64')
216
216
  const bindingPackageVersion = require('brustjs-darwin-arm64/package.json').version
217
- if (bindingPackageVersion !== '0.1.29-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
218
- throw new Error(`Native binding package version mismatch, expected 0.1.29-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
217
+ if (bindingPackageVersion !== '0.1.31-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
218
+ throw new Error(`Native binding package version mismatch, expected 0.1.31-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
219
219
  }
220
220
  return binding
221
221
  } catch (e) {
@@ -234,8 +234,8 @@ function requireNative() {
234
234
  try {
235
235
  const binding = require('brustjs-freebsd-x64')
236
236
  const bindingPackageVersion = require('brustjs-freebsd-x64/package.json').version
237
- if (bindingPackageVersion !== '0.1.29-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
238
- throw new Error(`Native binding package version mismatch, expected 0.1.29-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
237
+ if (bindingPackageVersion !== '0.1.31-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
238
+ throw new Error(`Native binding package version mismatch, expected 0.1.31-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
239
239
  }
240
240
  return binding
241
241
  } catch (e) {
@@ -250,8 +250,8 @@ function requireNative() {
250
250
  try {
251
251
  const binding = require('brustjs-freebsd-arm64')
252
252
  const bindingPackageVersion = require('brustjs-freebsd-arm64/package.json').version
253
- if (bindingPackageVersion !== '0.1.29-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
254
- throw new Error(`Native binding package version mismatch, expected 0.1.29-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
253
+ if (bindingPackageVersion !== '0.1.31-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
254
+ throw new Error(`Native binding package version mismatch, expected 0.1.31-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
255
255
  }
256
256
  return binding
257
257
  } catch (e) {
@@ -271,8 +271,8 @@ function requireNative() {
271
271
  try {
272
272
  const binding = require('brustjs-linux-x64-musl')
273
273
  const bindingPackageVersion = require('brustjs-linux-x64-musl/package.json').version
274
- if (bindingPackageVersion !== '0.1.29-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
275
- throw new Error(`Native binding package version mismatch, expected 0.1.29-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
274
+ if (bindingPackageVersion !== '0.1.31-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
275
+ throw new Error(`Native binding package version mismatch, expected 0.1.31-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
276
276
  }
277
277
  return binding
278
278
  } catch (e) {
@@ -287,8 +287,8 @@ function requireNative() {
287
287
  try {
288
288
  const binding = require('brustjs-linux-x64-gnu')
289
289
  const bindingPackageVersion = require('brustjs-linux-x64-gnu/package.json').version
290
- if (bindingPackageVersion !== '0.1.29-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
291
- throw new Error(`Native binding package version mismatch, expected 0.1.29-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
290
+ if (bindingPackageVersion !== '0.1.31-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
291
+ throw new Error(`Native binding package version mismatch, expected 0.1.31-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
292
292
  }
293
293
  return binding
294
294
  } catch (e) {
@@ -305,8 +305,8 @@ function requireNative() {
305
305
  try {
306
306
  const binding = require('brustjs-linux-arm64-musl')
307
307
  const bindingPackageVersion = require('brustjs-linux-arm64-musl/package.json').version
308
- if (bindingPackageVersion !== '0.1.29-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
309
- throw new Error(`Native binding package version mismatch, expected 0.1.29-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
308
+ if (bindingPackageVersion !== '0.1.31-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
309
+ throw new Error(`Native binding package version mismatch, expected 0.1.31-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
310
310
  }
311
311
  return binding
312
312
  } catch (e) {
@@ -321,8 +321,8 @@ function requireNative() {
321
321
  try {
322
322
  const binding = require('brustjs-linux-arm64-gnu')
323
323
  const bindingPackageVersion = require('brustjs-linux-arm64-gnu/package.json').version
324
- if (bindingPackageVersion !== '0.1.29-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
325
- throw new Error(`Native binding package version mismatch, expected 0.1.29-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
324
+ if (bindingPackageVersion !== '0.1.31-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
325
+ throw new Error(`Native binding package version mismatch, expected 0.1.31-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
326
326
  }
327
327
  return binding
328
328
  } catch (e) {
@@ -339,8 +339,8 @@ function requireNative() {
339
339
  try {
340
340
  const binding = require('brustjs-linux-arm-musleabihf')
341
341
  const bindingPackageVersion = require('brustjs-linux-arm-musleabihf/package.json').version
342
- if (bindingPackageVersion !== '0.1.29-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
343
- throw new Error(`Native binding package version mismatch, expected 0.1.29-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
342
+ if (bindingPackageVersion !== '0.1.31-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
343
+ throw new Error(`Native binding package version mismatch, expected 0.1.31-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
344
344
  }
345
345
  return binding
346
346
  } catch (e) {
@@ -355,8 +355,8 @@ function requireNative() {
355
355
  try {
356
356
  const binding = require('brustjs-linux-arm-gnueabihf')
357
357
  const bindingPackageVersion = require('brustjs-linux-arm-gnueabihf/package.json').version
358
- if (bindingPackageVersion !== '0.1.29-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
359
- throw new Error(`Native binding package version mismatch, expected 0.1.29-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
358
+ if (bindingPackageVersion !== '0.1.31-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
359
+ throw new Error(`Native binding package version mismatch, expected 0.1.31-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
360
360
  }
361
361
  return binding
362
362
  } catch (e) {
@@ -373,8 +373,8 @@ function requireNative() {
373
373
  try {
374
374
  const binding = require('brustjs-linux-loong64-musl')
375
375
  const bindingPackageVersion = require('brustjs-linux-loong64-musl/package.json').version
376
- if (bindingPackageVersion !== '0.1.29-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
377
- throw new Error(`Native binding package version mismatch, expected 0.1.29-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
376
+ if (bindingPackageVersion !== '0.1.31-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
377
+ throw new Error(`Native binding package version mismatch, expected 0.1.31-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
378
378
  }
379
379
  return binding
380
380
  } catch (e) {
@@ -389,8 +389,8 @@ function requireNative() {
389
389
  try {
390
390
  const binding = require('brustjs-linux-loong64-gnu')
391
391
  const bindingPackageVersion = require('brustjs-linux-loong64-gnu/package.json').version
392
- if (bindingPackageVersion !== '0.1.29-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
393
- throw new Error(`Native binding package version mismatch, expected 0.1.29-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
392
+ if (bindingPackageVersion !== '0.1.31-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
393
+ throw new Error(`Native binding package version mismatch, expected 0.1.31-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
394
394
  }
395
395
  return binding
396
396
  } catch (e) {
@@ -407,8 +407,8 @@ function requireNative() {
407
407
  try {
408
408
  const binding = require('brustjs-linux-riscv64-musl')
409
409
  const bindingPackageVersion = require('brustjs-linux-riscv64-musl/package.json').version
410
- if (bindingPackageVersion !== '0.1.29-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
411
- throw new Error(`Native binding package version mismatch, expected 0.1.29-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
410
+ if (bindingPackageVersion !== '0.1.31-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
411
+ throw new Error(`Native binding package version mismatch, expected 0.1.31-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
412
412
  }
413
413
  return binding
414
414
  } catch (e) {
@@ -423,8 +423,8 @@ function requireNative() {
423
423
  try {
424
424
  const binding = require('brustjs-linux-riscv64-gnu')
425
425
  const bindingPackageVersion = require('brustjs-linux-riscv64-gnu/package.json').version
426
- if (bindingPackageVersion !== '0.1.29-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
427
- throw new Error(`Native binding package version mismatch, expected 0.1.29-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
426
+ if (bindingPackageVersion !== '0.1.31-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
427
+ throw new Error(`Native binding package version mismatch, expected 0.1.31-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
428
428
  }
429
429
  return binding
430
430
  } catch (e) {
@@ -440,8 +440,8 @@ function requireNative() {
440
440
  try {
441
441
  const binding = require('brustjs-linux-ppc64-gnu')
442
442
  const bindingPackageVersion = require('brustjs-linux-ppc64-gnu/package.json').version
443
- if (bindingPackageVersion !== '0.1.29-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
444
- throw new Error(`Native binding package version mismatch, expected 0.1.29-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
443
+ if (bindingPackageVersion !== '0.1.31-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
444
+ throw new Error(`Native binding package version mismatch, expected 0.1.31-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
445
445
  }
446
446
  return binding
447
447
  } catch (e) {
@@ -456,8 +456,8 @@ function requireNative() {
456
456
  try {
457
457
  const binding = require('brustjs-linux-s390x-gnu')
458
458
  const bindingPackageVersion = require('brustjs-linux-s390x-gnu/package.json').version
459
- if (bindingPackageVersion !== '0.1.29-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
460
- throw new Error(`Native binding package version mismatch, expected 0.1.29-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
459
+ if (bindingPackageVersion !== '0.1.31-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
460
+ throw new Error(`Native binding package version mismatch, expected 0.1.31-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
461
461
  }
462
462
  return binding
463
463
  } catch (e) {
@@ -476,8 +476,8 @@ function requireNative() {
476
476
  try {
477
477
  const binding = require('brustjs-openharmony-arm64')
478
478
  const bindingPackageVersion = require('brustjs-openharmony-arm64/package.json').version
479
- if (bindingPackageVersion !== '0.1.29-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
480
- throw new Error(`Native binding package version mismatch, expected 0.1.29-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
479
+ if (bindingPackageVersion !== '0.1.31-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
480
+ throw new Error(`Native binding package version mismatch, expected 0.1.31-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
481
481
  }
482
482
  return binding
483
483
  } catch (e) {
@@ -492,8 +492,8 @@ function requireNative() {
492
492
  try {
493
493
  const binding = require('brustjs-openharmony-x64')
494
494
  const bindingPackageVersion = require('brustjs-openharmony-x64/package.json').version
495
- if (bindingPackageVersion !== '0.1.29-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
496
- throw new Error(`Native binding package version mismatch, expected 0.1.29-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
495
+ if (bindingPackageVersion !== '0.1.31-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
496
+ throw new Error(`Native binding package version mismatch, expected 0.1.31-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
497
497
  }
498
498
  return binding
499
499
  } catch (e) {
@@ -508,8 +508,8 @@ function requireNative() {
508
508
  try {
509
509
  const binding = require('brustjs-openharmony-arm')
510
510
  const bindingPackageVersion = require('brustjs-openharmony-arm/package.json').version
511
- if (bindingPackageVersion !== '0.1.29-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
512
- throw new Error(`Native binding package version mismatch, expected 0.1.29-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
511
+ if (bindingPackageVersion !== '0.1.31-alpha' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
512
+ throw new Error(`Native binding package version mismatch, expected 0.1.31-alpha but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
513
513
  }
514
514
  return binding
515
515
  } catch (e) {
@@ -183,6 +183,11 @@ interface ForEntry {
183
183
  disposers: Array<() => void>
184
184
  }
185
185
 
186
+ // One x-for mount per (parent, listPath): the SSR seed renders N sibling nodes
187
+ // each carrying x-for, and bindTree's parent loop visits every one — without this
188
+ // guard, bindFor would mount the list N times.
189
+ const forMountGuard = new WeakMap<Node, Set<string>>()
190
+
186
191
  // `x-for="item in member"` — the element is the template. Replace it with a comment
187
192
  // anchor. Without a `by` key clause this is a full re-render on each list change
188
193
  // (legacy v1, now with optional plain index). With `by <keypath>...` it is an opt-in
@@ -197,6 +202,27 @@ function bindFor(tplEl: HTMLElement, instance: Instance, disposers: Array<() =>
197
202
  const { itemName, indexName, listPath, keyPaths } = expr
198
203
  const parent = tplEl.parentNode
199
204
  if (!parent) return
205
+
206
+ // Idempotency: the SSR seed renders N sibling x-for nodes; bindTree's parent loop
207
+ // visits each. Mount the list ONCE per (parent, listPath); later siblings no-op.
208
+ let mountedSet = forMountGuard.get(parent)
209
+ if (!mountedSet) {
210
+ mountedSet = new Set()
211
+ forMountGuard.set(parent, mountedSet)
212
+ }
213
+ if (mountedSet.has(listPath)) return
214
+ mountedSet.add(listPath)
215
+ disposers.push(() => mountedSet?.delete(listPath)) // re-mount (SPA nav) works again
216
+
217
+ // ---- SSR adopt: keyed x-for whose seed nodes were server-rendered ----
218
+ if (keyPaths) {
219
+ const seeds = collectSeeds(parent, keyPaths, tplEl.getAttribute('x-for') ?? '')
220
+ if (seeds.length > 0) {
221
+ bindForAdopt(seeds, instance, parent, expr, disposers)
222
+ return
223
+ }
224
+ }
225
+
200
226
  const anchor = tplEl.ownerDocument.createComment(`x-for:${itemName}`)
201
227
  parent.insertBefore(anchor, tplEl)
202
228
  tplEl.removeAttribute('x-for')
@@ -237,8 +263,25 @@ function bindFor(tplEl: HTMLElement, instance: Instance, disposers: Array<() =>
237
263
  return
238
264
  }
239
265
 
240
- // ---- keyed reconcile ----
241
- let map = new Map<string, ForEntry>()
266
+ // ---- keyed reconcile (client-only x-for: no SSR seed) ----
267
+ installKeyedReconcile(instance, parent, expr, template, anchor, new Map(), disposers)
268
+ }
269
+
270
+ /** The 0.1.28 keyed reconcile, parameterized by an INITIAL `map` (empty for a
271
+ * client-only x-for; pre-populated with adopted SSR seeds for an SSR-seeded one)
272
+ * and a `template`/`anchor` already derived by the caller. */
273
+ function installKeyedReconcile(
274
+ instance: Instance,
275
+ parent: Node,
276
+ expr: ForExpr,
277
+ template: HTMLElement,
278
+ anchor: Comment,
279
+ initialMap: Map<string, ForEntry>,
280
+ disposers: Array<() => void>,
281
+ ): void {
282
+ const { itemName, indexName, listPath, keyPaths } = expr
283
+ const keys = keyPaths as string[] // keyed path → always defined
284
+ let live = initialMap
242
285
  const disposeEntry = (e: ForEntry) => {
243
286
  for (const d of e.disposers.splice(0)) {
244
287
  try {
@@ -256,25 +299,21 @@ function bindFor(tplEl: HTMLElement, instance: Instance, disposers: Array<() =>
256
299
  const next = new Map<string, ForEntry>()
257
300
  for (let i = 0; i < arr.length; i++) {
258
301
  const item = arr[i]
259
- const probe: Instance = { [itemName]: item } // plain scope for key extraction
302
+ const probe: Instance = { [itemName]: item }
260
303
  if (indexName) probe[indexName] = i
261
- // resolveRaw (NOT read): never calls signal getters → the reconcile effect
262
- // tracks ONLY the list signal, even if a list item nests a signal on a key
263
- // path. Keys are plain identity data by contract; this just makes the
264
- // no-self-retrigger invariant hold unconditionally.
265
- let key = keyPaths.map((p) => String(resolveRaw(probe, p))).join('\x00')
304
+ let key = keys.map((p) => String(resolveRaw(probe, p))).join('\x00')
266
305
  if (next.has(key)) {
267
306
  console.warn(`[brust] duplicate x-for key "${key}"`)
268
307
  key = `${key}\x00#${i}`
269
308
  }
270
- const existing = map.get(key)
309
+ const existing = live.get(key)
271
310
  if (existing) {
272
311
  batch(() => {
273
312
  existing.itemSig.set(item)
274
313
  existing.idxSig?.set(i)
275
314
  })
276
- parent.insertBefore(existing.node, anchor) // move into new order
277
- map.delete(key)
315
+ parent.insertBefore(existing.node, anchor)
316
+ live.delete(key)
278
317
  next.set(key, existing)
279
318
  } else {
280
319
  const clone = template.cloneNode(true) as HTMLElement
@@ -289,17 +328,122 @@ function bindFor(tplEl: HTMLElement, instance: Instance, disposers: Array<() =>
289
328
  next.set(key, { node: clone, itemSig, idxSig, disposers: entryDisposers })
290
329
  }
291
330
  }
292
- for (const e of map.values()) disposeEntry(e) // not reused this pass → gone
293
- map = next
331
+ for (const e of live.values()) disposeEntry(e)
332
+ live = next
294
333
  }),
295
334
  )
296
- // component-unmount teardown of all live entries (mirrors legacy `clear`)
297
335
  disposers.push(() => {
298
- for (const e of map.values()) disposeEntry(e)
299
- map.clear()
336
+ for (const e of live.values()) disposeEntry(e)
337
+ live.clear()
300
338
  })
301
339
  }
302
340
 
341
+ /** Direct-child seed nodes of THIS x-for (matched by the identical compiled
342
+ * `x-for` string) carrying the SSR key attr (single `data-x-key`, composite
343
+ * `data-x-key-0`). Only direct children — the key attr lives on the for-item
344
+ * root, never a descendant. The x-for match prevents a sibling x-for list under
345
+ * the same parent from having its seeds consumed by this one. */
346
+ function collectSeeds(parent: Node, keyPaths: string[], xforRaw: string): HTMLElement[] {
347
+ const sel = keyPaths.length > 1 ? '[data-x-key-0]' : '[data-x-key]'
348
+ const out: HTMLElement[] = []
349
+ for (const c of Array.from((parent as Element).children ?? [])) {
350
+ if (c instanceof HTMLElement && c.matches(sel) && c.getAttribute('x-for') === xforRaw) {
351
+ out.push(c)
352
+ }
353
+ }
354
+ return out
355
+ }
356
+
357
+ /** The seed's key from markup — must match the reconcile's computed key (single
358
+ * `data-x-key`, OR `data-x-key-*` joined with `\x00` IN JS — NUL never in HTML). */
359
+ function seedKey(node: HTMLElement, keyPaths: string[]): string {
360
+ if (keyPaths.length > 1) {
361
+ return keyPaths.map((_, i) => node.getAttribute(`data-x-key-${i}`) ?? '').join('\x00')
362
+ }
363
+ return node.getAttribute('data-x-key') ?? ''
364
+ }
365
+
366
+ function stripKeyAttrs(el: HTMLElement, keyPaths: string[]): void {
367
+ if (keyPaths.length > 1) {
368
+ for (let i = 0; i < keyPaths.length; i++) el.removeAttribute(`data-x-key-${i}`)
369
+ } else {
370
+ el.removeAttribute('data-x-key')
371
+ }
372
+ }
373
+
374
+ /** Bind an adopted seed node as a plain subtree. The node KEEPS its x-for attr so
375
+ * the parent bindTree loop's later re-visit routes back to bindFor → mount guard
376
+ * no-ops; do NOT route the node itself through bindFor again here. */
377
+ function bindAdoptedNode(node: HTMLElement, scope: Instance, disposers: Array<() => void>): void {
378
+ bindAttrs(node, scope, disposers)
379
+ for (const child of Array.from(node.children)) {
380
+ if (!(child instanceof HTMLElement)) continue
381
+ if (child.hasAttribute('x-data')) continue
382
+ bindTree(child, scope, disposers)
383
+ }
384
+ }
385
+
386
+ /** Adopt SSR-seeded keyed x-for: reuse the seed nodes (identity preserved), seed
387
+ * each item-signal from the matching client item by key, wire reactivity, then
388
+ * hand the pre-populated map to the shared reconcile (first run = all reused). */
389
+ function bindForAdopt(
390
+ seeds: HTMLElement[],
391
+ instance: Instance,
392
+ parent: Node,
393
+ expr: ForExpr,
394
+ disposers: Array<() => void>,
395
+ ): void {
396
+ const { itemName, indexName, listPath, keyPaths } = expr
397
+ // Static fallback: a sugar-marked (or hand-written) x-for whose list has NO
398
+ // backing signal on the instance must NOT reconcile — installKeyedReconcile would
399
+ // wipe the SSR seeds on its first (empty-list) tick. resolveRaw returns the signal
400
+ // OBJECT for a registered signal (truthy); undefined only when truly absent.
401
+ if (resolveRaw(instance, listPath) == null) {
402
+ return // leave the SSR seed nodes exactly as rendered (fully static)
403
+ }
404
+ const keys = keyPaths as string[]
405
+ // template for future creates: stripped clone of the first seed.
406
+ const template = seeds[0].cloneNode(true) as HTMLElement
407
+ template.removeAttribute('x-for')
408
+ stripKeyAttrs(template, keys)
409
+ // anchor AFTER the last seed so future inserts keep document order.
410
+ const last = seeds[seeds.length - 1]
411
+ const anchor = last.ownerDocument.createComment(`x-for:${itemName}`)
412
+ parent.insertBefore(anchor, last.nextSibling)
413
+ // index the client list by key (same computation as the reconcile).
414
+ const list = read(instance, listPath)
415
+ const arr = Array.isArray(list) ? list : []
416
+ const byKey = new Map<string, { item: unknown; idx: number }>()
417
+ for (let i = 0; i < arr.length; i++) {
418
+ const probe: Instance = { [itemName]: arr[i] }
419
+ if (indexName) probe[indexName] = i
420
+ const k = keys.map((p) => String(resolveRaw(probe, p))).join('\x00')
421
+ if (!byKey.has(k)) byKey.set(k, { item: arr[i], idx: i })
422
+ }
423
+ // adopt each seed in place.
424
+ const map = new Map<string, ForEntry>()
425
+ for (let si = 0; si < seeds.length; si++) {
426
+ const node = seeds[si] as HTMLElement
427
+ let key = seedKey(node, keys)
428
+ if (map.has(key)) {
429
+ // mirror the reconcile's dup-key handling: suffix so the entry is tracked
430
+ // (and disposed on the first reconcile, since no client item matches it).
431
+ console.warn(`[brust] duplicate x-for seed key "${key}"`)
432
+ key = `${key}\x00#${si}`
433
+ }
434
+ const match = byKey.get(key)
435
+ const itemSig = signal(match ? match.item : undefined)
436
+ const idxSig = indexName ? signal(match ? match.idx : 0) : undefined
437
+ const childScope: Instance = Object.create(instance)
438
+ childScope[itemName] = itemSig
439
+ if (indexName && idxSig) childScope[indexName] = idxSig
440
+ const entryDisposers: Array<() => void> = []
441
+ bindAdoptedNode(node, childScope, entryDisposers)
442
+ map.set(key, { node, itemSig, idxSig, disposers: entryDisposers })
443
+ }
444
+ installKeyedReconcile(instance, parent, expr, template, anchor, map, disposers)
445
+ }
446
+
303
447
  function bindAttrs(el: HTMLElement, scope: Instance, disposers: Array<() => void>): void {
304
448
  for (const attr of Array.from(el.attributes)) {
305
449
  const name = attr.name