@kudzujs/core 0.4.5 → 0.4.6

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 CHANGED
@@ -279,15 +279,15 @@ The list starts with 1,000 keyed items, then updates every label, reverses the o
279
279
 
280
280
  | Framework | Initial content | Initial JS gzip | Total output | Build | Update | Reverse | Remove | Add | Operations total |
281
281
  |---|---:|---:|---:|---:|---:|---:|---:|---:|---:|
282
- | Astro | Yes | **324 B** | **43.6 KB** | 867 ms | **3.5 ms** | **25.4 ms** | 10.7 ms | **18.0 ms** | **57.6 ms** |
283
- | Kudzu | Yes | 5.7 KB | 180.8 KB | **417 ms** | 8.3 ms | 28.5 ms | **10.2 ms** | 20.5 ms | **67.5 ms** |
284
- | Vue CSR | No | 24.3 KB | 61.3 KB | 771 ms | 9.7 ms | 31.3 ms | 9.4 ms | 18.1 ms | 68.5 ms |
285
- | React CSR | No | 59.3 KB | 189.4 KB | 1024 ms | 8.7 ms | 32.3 ms | 12.4 ms | 18.1 ms | 71.5 ms |
286
- | Next.js | Yes | 182.2 KB | 695.2 KB | 3049 ms | 6.7 ms | 35.0 ms | 16.3 ms | 19.4 ms | 77.4 ms |
287
- | Qwik CSR | No | 22.2 KB | 64.1 KB | 602 ms | 14.5 ms | 19.9 ms | 31.3 ms | 18.9 ms | 84.6 ms |
288
- | Svelte CSR | No | 12.9 KB | 33.1 KB | 859 ms | 5.2 ms | 65.4 ms | 10.7 ms | 19.9 ms | 101.2 ms |
289
-
290
- Astro is the hand-authored native DOM baseline in the interactive fixtures. React, Vue, Svelte, and Qwik used client-rendered fixtures, while Kudzu and Astro emitted initial HTML; Qwik therefore did not exercise its SSR resumability advantage. Kudzu's keyed-list operations total 67.5 ms, ahead of the React fixture's 71.5 ms and within 9.9 ms of Astro across all four operations.
282
+ | Astro | Yes | **324 B** | **43.6 KB** | 852 ms | **3.5 ms** | 25.8 ms | 15.8 ms | **17.9 ms** | **63.0 ms** |
283
+ | Kudzu | Yes | 6.3 KB | 69.3 KB | **406 ms** | 6.0 ms | 28.5 ms | 14.7 ms | 19.1 ms | **68.3 ms** |
284
+ | Next.js | Yes | 182.2 KB | 695.2 KB | 2950 ms | 6.8 ms | 34.1 ms | **9.0 ms** | 19.3 ms | 69.2 ms |
285
+ | React CSR | No | 59.3 KB | 189.4 KB | 1028 ms | 9.5 ms | 35.3 ms | 10.1 ms | **17.8 ms** | 72.7 ms |
286
+ | Vue CSR | No | 24.3 KB | 61.3 KB | 759 ms | 10.2 ms | 33.9 ms | 9.3 ms | 19.7 ms | 73.1 ms |
287
+ | Qwik CSR | No | 22.2 KB | 64.1 KB | 604 ms | 15.6 ms | **20.0 ms** | 30.4 ms | 18.1 ms | 84.1 ms |
288
+ | Svelte CSR | No | 12.9 KB | 33.1 KB | 845 ms | 5.4 ms | 64.4 ms | 11.2 ms | 19.1 ms | 100.1 ms |
289
+
290
+ Astro is the hand-authored native DOM baseline in the interactive fixtures. React, Vue, Svelte, and Qwik used client-rendered fixtures, while Kudzu and Astro emitted initial HTML; Qwik therefore did not exercise its SSR resumability advantage. Kudzu's keyed-list operations total 68.3 ms, ahead of the React fixture's 72.7 ms and within 5.3 ms of Astro across all four operations.
291
291
 
292
292
  Benchmark snapshot collected on July 22, 2026 with Node 24.14.0 on an Intel i5-9500. These results compare the selected one-page fixtures, not ecosystem maturity, browser interaction speed beyond the listed operations, or each framework's full rendering options. Build times vary with machine load and filesystem cache.
293
293
 
@@ -116,7 +116,7 @@ function specializeRuntime(source, events, hasStateSeed) {
116
116
  if (hasStateSeed) return specialized
117
117
  return specialized
118
118
  .replace(" const initialState = document.body.dataset.kState\n", "")
119
- .replace(" if (initialState) for (const [id, value] of JSON.parse(initialState)) browserState.set(id, value)\n", "")
119
+ .replace(" if (initialState) for (const [id, value, compact] of JSON.parse(initialState)) browserState.set(id, compact ? value[1].map(row => Object.fromEntries(value[0].map((field, index) => [field, row[index]]))) : value)\n", "")
120
120
  }
121
121
 
122
122
  export function parseDevPort(value) {
@@ -187,7 +187,7 @@ function serializeCapture(name, value, seen) {
187
187
  }
188
188
 
189
189
  export async function renderPage(component, metadata = {}) {
190
- renderContext = { nextState: 0, nextCondition: 0, nextList: 0, conditionDepth: 0, listDepth: 0, listRoot: undefined, listTemplate: false, states: {}, textStates: new Set(), conditionStates: new Set(), events: [], bindings: [], conditions: [], lists: [], hasBehaviors: false, hasNativeBehaviors: false, hasBindings: false, hasLists: false }
190
+ renderContext = { nextState: 0, nextCondition: 0, nextList: 0, conditionDepth: 0, listDepth: 0, listRoot: undefined, listTemplate: false, listFields: undefined, states: {}, textStates: new Set(), conditionStates: new Set(), events: [], bindings: [], conditions: [], lists: [], hasBehaviors: false, hasNativeBehaviors: false, hasBindings: false, hasLists: false }
191
191
 
192
192
  try {
193
193
  const body = await renderNode({ type: component, props: {} })
@@ -208,8 +208,13 @@ export async function renderPage(component, metadata = {}) {
208
208
  const listRuntime = renderContext.hasLists
209
209
  ? '<script type="module" src="/assets/kudzu-list.js"></script>'
210
210
  : ""
211
+ const listStates = new Set(renderContext.lists.map(list => list.state))
212
+ const seededListStates = new Set(renderContext.lists.filter(list => list.seed && !renderContext.textStates.has(list.state) && !renderContext.conditionStates.has(list.state)).map(list => list.state))
211
213
  const initialState = renderContext.hasBehaviors
212
- ? Object.entries(renderContext.states).filter(([id]) => !renderContext.textStates.has(id) || renderContext.conditionStates.has(id)).map(([id, entry]) => [id, entry.initialValue])
214
+ ? Object.entries(renderContext.states).filter(([id]) => (!renderContext.textStates.has(id) || renderContext.conditionStates.has(id)) && !seededListStates.has(id)).map(([id, entry]) => {
215
+ const compact = listStates.has(id) && compactListState(entry.initialValue)
216
+ return compact ? [id, compact, 1] : [id, entry.initialValue]
217
+ })
213
218
  : []
214
219
  const state = initialState.length
215
220
  ? ` data-k-state='${escapeJsonAttribute(initialState)}'`
@@ -305,11 +310,14 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
305
310
  }
306
311
  if (node?.[listMarker]) return renderList(node, namespace, selectValue)
307
312
  if (node?.[listFieldMarker]) {
308
- return `<template data-k-list-text="${escapeAttribute(node.field)}"></template>${escapeHtml(node.value ?? "")}<template data-k-list-text-end></template>`
313
+ if (renderContext.listTemplate) renderContext.listFields?.add(node.field)
314
+ const marker = renderContext.listTemplate ? ` data-k-list-text="${escapeAttribute(node.field)}"` : ""
315
+ return `<template${marker}></template>${escapeHtml(node.value ?? "")}<template data-k-list-text-end></template>`
309
316
  }
310
317
  if (node?.[listExpressionMarker]) {
311
318
  const descriptor = { module: node.module, handler: node.handler }
312
- return `<template data-k-list-expression='${escapeJsonAttribute(descriptor)}'></template>${escapeHtml(node.value ?? "")}<template data-k-list-expression-end></template>`
319
+ const marker = renderContext.listTemplate ? ` data-k-list-expression='${escapeJsonAttribute(descriptor)}'` : ""
320
+ return `<template${marker}></template>${escapeHtml(node.value ?? "")}<template data-k-list-expression-end></template>`
313
321
  }
314
322
  if (!node || typeof node !== "object" || !("type" in node)) {
315
323
  throw new Error(`Cannot render ${String(node)}`)
@@ -336,9 +344,7 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
336
344
  if (renderContext.listRoot) {
337
345
  const root = renderContext.listRoot
338
346
  renderContext.listRoot = undefined
339
- attributes += root.template
340
- ? ` data-k-list-root="${root.id}"`
341
- : ` data-k-list-item='${escapeJsonAttribute([root.id, root.key])}'`
347
+ if (root.template) attributes += ` data-k-list-root="${root.id}"`
342
348
  }
343
349
 
344
350
  for (const [rawName, value] of Object.entries(props)) {
@@ -412,10 +418,13 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
412
418
  }
413
419
 
414
420
  if (attributeBindings.length) attributes += ` data-k-bind-attrs='${escapeJsonAttribute(attributeBindings)}'`
415
- if (listAttributes.length) attributes += ` data-k-list-attrs='${escapeJsonAttribute(listAttributes)}'`
416
- if (listExpressionAttributes.length) attributes += ` data-k-list-expression-attrs='${escapeJsonAttribute(listExpressionAttributes)}'`
417
- if (listEvents.length) attributes += ` data-k-list-events='${escapeJsonAttribute(listEvents)}'`
418
- if (directListText) attributes += ` data-k-list-text="${escapeAttribute(directListText.field)}"`
421
+ if (renderContext.listTemplate && listAttributes.length) attributes += ` data-k-list-attrs='${escapeJsonAttribute(listAttributes)}'`
422
+ if (renderContext.listTemplate && listExpressionAttributes.length) attributes += ` data-k-list-expression-attrs='${escapeJsonAttribute(listExpressionAttributes)}'`
423
+ if (renderContext.listTemplate && listEvents.length) attributes += ` data-k-list-events='${escapeJsonAttribute(listEvents)}'`
424
+ if (renderContext.listTemplate && directListText) {
425
+ renderContext.listFields?.add(directListText.field)
426
+ attributes += ` data-k-list-text="${escapeAttribute(directListText.field)}"`
427
+ }
419
428
 
420
429
  if (tag === "option" && selectValue !== noSelectValue && String(optionValue(props)) === (selectValue == null ? "" : String(selectValue))) attributes += " selected"
421
430
 
@@ -428,12 +437,17 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
428
437
  async function renderList(node, namespace, selectValue) {
429
438
  if (namespace) throw new Error(`Reactive keyed lists are not supported inside ${namespace}`)
430
439
  const id = `l${renderContext.nextList++}`
431
- const descriptor = { id, state: node.items.id, key: node.keyField }
440
+ const descriptor = { id, state: node.items.id, key: node.keyField, keys: node.items.value.map(item => item[node.keyField]) }
432
441
  renderContext.listDepth++
442
+ const previousListFields = renderContext.listFields
433
443
  try {
434
444
  renderContext.listTemplate = true
445
+ renderContext.listFields = new Set([node.keyField])
435
446
  renderContext.listRoot = { id, template: true }
436
447
  const template = await renderNode(node.render({}), namespace, selectValue)
448
+ if (template.includes("data-k-native-")) descriptor.mount = true
449
+ const seed = listSeed(node.items.value, renderContext.listFields)
450
+ if (seed) descriptor.seed = seed
437
451
  let current = ""
438
452
  renderContext.listTemplate = false
439
453
  for (const item of node.items.value) {
@@ -447,6 +461,7 @@ async function renderList(node, namespace, selectValue) {
447
461
  } finally {
448
462
  renderContext.listRoot = undefined
449
463
  renderContext.listTemplate = false
464
+ renderContext.listFields = previousListFields
450
465
  renderContext.listDepth--
451
466
  }
452
467
  }
@@ -493,6 +508,24 @@ function escapeJsonAttribute(value) {
493
508
  return JSON.stringify(value).replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll("'", "&#39;")
494
509
  }
495
510
 
511
+ function compactListState(value) {
512
+ if (!Array.isArray(value) || !value.length) return undefined
513
+ const fields = Object.keys(value[0])
514
+ if (!fields.length || !value.every(item => Object.keys(item).length === fields.length && fields.every(field => Object.hasOwn(item, field)))) return undefined
515
+ return [fields, value.map(item => fields.map(field => item[field]))]
516
+ }
517
+
518
+ function listSeed(items, fields) {
519
+ if (!items.length || !items.every(item => Object.keys(item).length === fields.size && Object.keys(item).every(field => fields.has(field)))) return undefined
520
+ const seed = {}
521
+ for (const field of fields) {
522
+ const types = new Set(items.map(item => item[field] === null ? "null" : typeof item[field]))
523
+ if (types.size !== 1 || !["string", "number", "boolean", "null"].includes([...types][0])) return undefined
524
+ seed[field] = [...types][0]
525
+ }
526
+ return seed
527
+ }
528
+
496
529
  function toKebabCase(value) {
497
530
  return value.replace(/[A-Z]/g, character => `-${character.toLowerCase()}`)
498
531
  }
@@ -28,16 +28,22 @@ function mountLists(root) {
28
28
  if (mountedLists.has(start)) continue
29
29
  mountedLists.add(start)
30
30
  const descriptor = JSON.parse(start.dataset.kList)
31
- const roots = [...start.ownerDocument.querySelectorAll("[data-k-list-item]")].filter(node => JSON.parse(node.dataset.kListItem)[0] === descriptor.id)
31
+ const end = findEnd(start, descriptor.id)
32
+ const roots = listRoots(start, end)
33
+ const templateRoot = start.content.firstElementChild
34
+ const parts = listItemPartPlan(templateRoot)
35
+ for (const root of roots) mapListItemParts(parts, root)
36
+ if (descriptor.seed && !browserState.has(descriptor.state)) browserState.set(descriptor.state, roots.map((root, index) => seedListItem(root, descriptor, index)))
32
37
  const items = browserState.get(descriptor.state)
33
- for (const root of roots) listItemParts(root)
34
38
  const list = {
35
39
  start,
36
40
  descriptor,
37
- roots: new Map(roots.map(node => [keyToken(JSON.parse(node.dataset.kListItem)[1]), node])),
38
- values: new Map(items.map(item => [keyToken(item[descriptor.key]), JSON.stringify(item)])),
41
+ parts,
42
+ seedFields: descriptor.seed && Object.keys(descriptor.seed),
43
+ roots: new Map(roots.map((node, index) => [keyToken(descriptor.keys[index]), node])),
44
+ values: new Map(),
39
45
  container: roots[0]?.parentNode,
40
- boundary: roots.length ? roots.at(-1).nextSibling : findEnd(start, descriptor.id)
46
+ boundary: end
41
47
  }
42
48
  register(listTargets, descriptor.state, list)
43
49
  listRegistrations.set(start, { state: descriptor.state, list })
@@ -70,11 +76,12 @@ function updateList(list) {
70
76
  const key = item?.[list.descriptor.key]
71
77
  if (!validListKey(key)) throw new Error(`Keyed list key "${list.descriptor.key}" must be a string or finite number`)
72
78
  assertListItem(item)
73
- assertListValue(item, seen, true)
79
+ const seededValue = list.seedFields && seededListValue(item, list.seedFields, list.descriptor.seed)
80
+ if (seededValue === undefined) assertListValue(item, seen, true)
74
81
  const token = keyToken(key)
75
82
  if (keys.has(token)) throw new Error(`Duplicate keyed list key: ${String(key)}`)
76
83
  keys.add(token)
77
- entries.push({ item, key, token, value: JSON.stringify(item) })
84
+ entries.push({ item, key, token, value: seededValue ?? JSON.stringify(item) })
78
85
  }
79
86
  const next = []
80
87
  const values = new Map()
@@ -88,7 +95,7 @@ function updateList(list) {
88
95
  if (node?.dataset.kListRoot !== list.descriptor.id) node = undefined
89
96
  if (!node) throw new Error("Keyed list template has no root element")
90
97
  node.removeAttribute("data-k-list-root")
91
- node.dataset.kListItem = JSON.stringify([list.descriptor.id, key])
98
+ mapListItemParts(list.parts, node)
92
99
  fillListItem(node, item)
93
100
  additions.append(node)
94
101
  added = true
@@ -98,13 +105,18 @@ function updateList(list) {
98
105
  next.push([token, node])
99
106
  values.set(token, value)
100
107
  }
108
+ const removals = parent.ownerDocument.createDocumentFragment()
101
109
  for (const [token, node] of list.roots) {
102
110
  if (keys.has(token)) continue
103
- unmountDom(node)
104
- node.remove()
111
+ if (list.descriptor.mount) {
112
+ unmountDom(node)
113
+ node.remove()
114
+ } else {
115
+ removals.append(node)
116
+ }
105
117
  }
106
118
  if (added) {
107
- mountDom(additions)
119
+ if (list.descriptor.mount) mountDom(additions)
108
120
  parent.insertBefore(additions, list.boundary)
109
121
  list.container ??= parent
110
122
  }
@@ -135,8 +147,8 @@ function fillListItem(root, item) {
135
147
  const revision = (revisions.get(root) ?? 0) + 1
136
148
  revisions.set(root, revision)
137
149
  const parts = listItemParts(root)
138
- for (const node of parts.directTexts) {
139
- const text = item?.[node.dataset.kListText]
150
+ for (const [node, field] of parts.directTexts) {
151
+ const text = item?.[field]
140
152
  const value = text == null ? "" : String(text)
141
153
  const current = node.firstChild
142
154
  if (current?.nodeType === Node.TEXT_NODE && !current.nextSibling) {
@@ -145,14 +157,14 @@ function fillListItem(root, item) {
145
157
  node.textContent = value
146
158
  }
147
159
  }
148
- for (const marker of parts.texts) {
149
- patchListText(marker, "template[data-k-list-text-end]", item?.[marker.dataset.kListText])
160
+ for (const [marker, field] of parts.texts) {
161
+ patchListText(marker, "template[data-k-list-text-end]", item?.[field])
150
162
  }
151
163
  for (const [node, attributes] of parts.attributes) {
152
164
  for (const [target, field] of attributes) patchBinding(node, target, item?.[field])
153
165
  }
154
- for (const node of parts.events) {
155
- for (const [event, native] of JSON.parse(node.dataset.kListEvents)) {
166
+ for (const [node, events] of parts.events) {
167
+ for (const [event, native] of JSON.parse(events)) {
156
168
  native.scope = Object.fromEntries(Object.entries(native.scope).map(([name, value]) => [name, value?.type === "list-item" ? serializeItem(item) : value]))
157
169
  node.dataset[`kNative${capitalize(event)}`] = JSON.stringify(native)
158
170
  }
@@ -176,9 +188,9 @@ function listItemParts(root) {
176
188
  if (parts) return parts
177
189
  parts = { directTexts: [], texts: [], attributes: [], events: [], expressions: [], expressionAttributes: [] }
178
190
  for (const node of matching(root, itemPartsSelector)) {
179
- if (node.hasAttribute("data-k-list-text")) (node.tagName === "TEMPLATE" ? parts.texts : parts.directTexts).push(node)
191
+ if (node.hasAttribute("data-k-list-text")) (node.tagName === "TEMPLATE" ? parts.texts : parts.directTexts).push([node, node.dataset.kListText])
180
192
  if (node.hasAttribute("data-k-list-attrs")) parts.attributes.push([node, JSON.parse(node.dataset.kListAttrs)])
181
- if (node.hasAttribute("data-k-list-events")) parts.events.push(node)
193
+ if (node.hasAttribute("data-k-list-events")) parts.events.push([node, node.dataset.kListEvents])
182
194
  if (node.hasAttribute("data-k-list-expression")) parts.expressions.push([node, JSON.parse(node.dataset.kListExpression)])
183
195
  if (node.hasAttribute("data-k-list-expression-attrs")) parts.expressionAttributes.push([node, JSON.parse(node.dataset.kListExpressionAttrs)])
184
196
  }
@@ -186,6 +198,61 @@ function listItemParts(root) {
186
198
  return parts
187
199
  }
188
200
 
201
+ function listItemPartPlan(template) {
202
+ const source = [template, ...template.querySelectorAll("*")]
203
+ const indexes = new Map(source.map((node, index) => [node, index]))
204
+ const parts = listItemParts(template)
205
+ return {
206
+ directTexts: parts.directTexts.map(([node, field]) => [indexes.get(node), field]),
207
+ texts: parts.texts.map(([node, field]) => [indexes.get(node), field]),
208
+ attributes: parts.attributes.map(([node, attributes]) => [indexes.get(node), attributes]),
209
+ events: parts.events.map(([node, events]) => [indexes.get(node), events]),
210
+ expressions: parts.expressions.map(([node, descriptor]) => [indexes.get(node), descriptor]),
211
+ expressionAttributes: parts.expressionAttributes.map(([node, attributes]) => [indexes.get(node), attributes])
212
+ }
213
+ }
214
+
215
+ function mapListItemParts(parts, root) {
216
+ const target = [root, ...root.querySelectorAll("*")]
217
+ itemParts.set(root, {
218
+ directTexts: parts.directTexts.map(([index, field]) => [target[index], field]),
219
+ texts: parts.texts.map(([index, field]) => [target[index], field]),
220
+ attributes: parts.attributes.map(([index, attributes]) => [target[index], attributes]),
221
+ events: parts.events.map(([index, events]) => [target[index], events]),
222
+ expressions: parts.expressions.map(([index, descriptor]) => [target[index], descriptor]),
223
+ expressionAttributes: parts.expressionAttributes.map(([index, attributes]) => [target[index], attributes])
224
+ })
225
+ }
226
+
227
+ function listRoots(start, end) {
228
+ const roots = []
229
+ for (let node = start.nextSibling; node && node !== end; node = node.nextSibling) {
230
+ if (node.nodeType === Node.ELEMENT_NODE) roots.push(node)
231
+ }
232
+ return roots
233
+ }
234
+
235
+ function seedListItem(root, descriptor, index) {
236
+ const item = { [descriptor.key]: descriptor.keys[index] }
237
+ const parts = itemParts.get(root)
238
+ for (const [node, field] of parts.directTexts) item[field] = seedValue(node.textContent, descriptor.seed[field])
239
+ for (const [marker, field] of parts.texts) item[field] = seedValue(rangeText(marker, "template[data-k-list-text-end]"), descriptor.seed[field])
240
+ return item
241
+ }
242
+
243
+ function seedValue(value, type) {
244
+ if (type === "number") return Number(value)
245
+ if (type === "boolean") return value === "true"
246
+ if (type === "null") return null
247
+ return value
248
+ }
249
+
250
+ function rangeText(marker, endSelector) {
251
+ let value = ""
252
+ for (let node = marker.nextSibling; node && !(node.nodeType === Node.ELEMENT_NODE && node.matches(endSelector)); node = node.nextSibling) value += node.textContent
253
+ return value
254
+ }
255
+
189
256
  function patchListText(marker, endSelector, value) {
190
257
  let end = marker.nextSibling
191
258
  while (end && !(end.nodeType === Node.ELEMENT_NODE && end.matches(endSelector))) end = end.nextSibling
@@ -254,6 +321,22 @@ function assertListItem(item) {
254
321
  if (!item || Array.isArray(item) || prototype !== Object.prototype) throw new Error("Keyed list items must be ordinary plain objects")
255
322
  }
256
323
 
324
+ function seededListValue(item, fields, seed) {
325
+ const keys = Reflect.ownKeys(item)
326
+ if (keys.length !== fields.length || keys.some(key => typeof key !== "string" || !fields.includes(key))) return undefined
327
+ const values = []
328
+ for (const field of fields) {
329
+ const descriptor = Object.getOwnPropertyDescriptor(item, field)
330
+ if (!descriptor.enumerable) throw new Error("Keyed list items must not contain non-enumerable properties")
331
+ if (!("value" in descriptor)) throw new Error("Keyed list items must not contain accessors")
332
+ const value = descriptor.value
333
+ const type = value === null ? "null" : typeof value
334
+ if (type !== seed[field] || type === "number" && (!Number.isFinite(value) || Object.is(value, -0))) return undefined
335
+ values.push(value)
336
+ }
337
+ return JSON.stringify(values)
338
+ }
339
+
257
340
  function assertListValue(value, seen, root = false) {
258
341
  if (value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value) && !Object.is(value, -0)) return
259
342
  if (!value || typeof value !== "object") throw new Error("Keyed list items must contain only JSON-safe values")
@@ -55,7 +55,7 @@ export function unmountDom(root) {
55
55
 
56
56
  if (typeof document !== "undefined") {
57
57
  const initialState = document.body.dataset.kState
58
- if (initialState) for (const [id, value] of JSON.parse(initialState)) browserState.set(id, value)
58
+ if (initialState) for (const [id, value, compact] of JSON.parse(initialState)) browserState.set(id, compact ? value[1].map(row => Object.fromEntries(value[0].map((field, index) => [field, row[index]]))) : value)
59
59
  mountText(document)
60
60
 
61
61
  const eventNames = ["click", "input", "change"]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kudzujs/core",
3
- "version": "0.4.5",
3
+ "version": "0.4.6",
4
4
  "description": "HTML-first TSX framework with synchronous state semantics and no virtual DOM",
5
5
  "type": "module",
6
6
  "license": "MIT",