@kudzujs/core 0.4.7 → 0.4.8

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
@@ -122,7 +122,22 @@ Regular attributes use the same expressions without an allowlist:
122
122
  />
123
123
  ```
124
124
 
125
- Kudzu compiles derived expressions to external ESM and patches only the bound DOM attribute or property. `aria-*` and `data-*` boolean values serialize as `"true"` or `"false"`; ordinary false values remove the attribute. Object `style` values use React-shaped camelCase properties, add `px` to nonzero dimensional numbers, and preserve unitless properties and CSS custom properties. Reactive `ref` and `dangerouslySetInnerHTML` remain unsupported.
125
+ Kudzu compiles derived expressions to external ESM and patches only the bound DOM attribute or property. `aria-*` and `data-*` boolean values serialize as `"true"` or `"false"`; ordinary false values remove the attribute. Object `style` values use React-shaped camelCase properties, add `px` to nonzero dimensional numbers, and preserve unitless properties and CSS custom properties. Reactive `dangerouslySetInnerHTML` remains unsupported.
126
+
127
+ ## DOM Refs
128
+
129
+ Use an object ref to access an element from a normal event handler:
130
+
131
+ ```tsx
132
+ const inputRef = useRef<HTMLInputElement>(null)
133
+
134
+ return <>
135
+ <input ref={inputRef} />
136
+ <button onClick={() => inputRef.current?.focus()}>Focus</button>
137
+ </>
138
+ ```
139
+
140
+ Kudzu resolves `current` when the handler reads it, so removed conditional elements return `null` without a component runtime. Refs must initialize with `null`; callback refs, mutable value refs, and refs inside keyed lists are not supported.
126
141
 
127
142
  ## Conditional DOM
128
143
 
@@ -169,7 +184,7 @@ const [items, setItems] = useState([
169
184
 
170
185
  Kudzu emits initial items as static HTML, then adds, removes, updates, and moves keyed elements directly. Existing keys move without remounting, preserving uncontrolled descendant state. Direct `item.<field>` reads use compact markers; derived item expressions compile to external ESM evaluators. Item-local handlers use direct DOM listeners and receive the latest JSON-safe item for their key, including after updates, additions, and reorders. The item remains stored once in shared list state; handler descriptors carry a placeholder that the list runtime fills when mounting or updating the keyed root.
171
186
 
172
- Each item must be an ordinary plain object with a unique string or finite-number key; nested data may contain only JSON-safe arrays, ordinary plain objects, and primitive values. Null-prototype objects are rejected to preserve JSON round-trip parity. The current syntax requires a direct local-state `.map`, one identifier callback parameter, one intrinsic JSX root, and `key={item.<field>}`. Derived expressions must be pure and synchronous: item reads, literals, operators, templates, approved read-only string/array methods, deterministic `Math` methods, and `String`/`Number`/`Boolean` conversion are supported. Component state, locals, imported helpers, browser globals, Promise values, mutation, arbitrary calls, and prototype-sensitive properties are rejected. Nested conditions or lists, item spreads, component tags, fragments, and reactive `style`, `ref`, or `dangerouslySetInnerHTML` remain unsupported. Keyed rows must be placed inside an explicit `<tbody>`, `<thead>`, or `<tfoot>`.
187
+ Each item must be an ordinary plain object with a unique string or finite-number key; nested data may contain only JSON-safe arrays, ordinary plain objects, and primitive values. Null-prototype objects are rejected to preserve JSON round-trip parity. The current syntax requires a direct local-state `.map`, one identifier callback parameter, one intrinsic JSX root, and `key={item.<field>}`. Derived expressions must be pure and synchronous: item reads, literals, operators, templates, approved read-only string/array methods, deterministic `Math` methods, and `String`/`Number`/`Boolean` conversion are supported. Component state, locals, imported helpers, browser globals, Promise values, mutation, arbitrary calls, and prototype-sensitive properties are rejected. Nested conditions or lists, item spreads, component tags, dynamic styles, refs, and `dangerouslySetInnerHTML` remain unsupported. Keyed rows must be placed inside an explicit `<tbody>`, `<thead>`, or `<tfoot>`.
173
188
 
174
189
  ## Normal JavaScript
175
190
 
@@ -206,6 +221,7 @@ TSX
206
221
 
207
222
  - Static pages ship no client JavaScript.
208
223
  - Interactive pages receive only the runtime capabilities they use.
224
+ - Production JavaScript is minified; development output stays readable.
209
225
  - Components are authoring units; no component tree is retained in the browser.
210
226
  - There is no VDOM, hydration pass, router, or client application runtime.
211
227
 
@@ -230,6 +246,7 @@ Supported:
230
246
  - Direct text DOM patches
231
247
  - Reactive standard, `aria-*`, and `data-*` attributes
232
248
  - Reactive object `style` attributes
249
+ - Object DOM refs in native event handlers
233
250
  - Controlled `value` and `checked` form properties
234
251
  - Conditional child `&&` and ternary DOM patches
235
252
  - Direct keyed local-state lists
@@ -251,13 +268,13 @@ Same counter with initial value `7` and increment/decrement buttons:
251
268
 
252
269
  | Framework | Initial content | Initial JS gzip | Total output | Clean build |
253
270
  |---|---:|---:|---:|---:|
254
- | Kudzu | Yes | 487 B | 1.4 KB | **363 ms** |
255
- | Astro | Yes | **158 B** | **365 B** | 851 ms |
256
- | Svelte CSR | No | 10.5 KB | 26.9 KB | 827 ms |
257
- | Qwik CSR | No | 20.6 KB | 57.8 KB | 598 ms |
258
- | Vue CSR | No | 24.0 KB | 60.3 KB | 762 ms |
259
- | React CSR | No | 59.2 KB | 189.0 KB | 1018 ms |
260
- | Next.js | Yes | 182.1 KB | 652.2 KB | 2989 ms |
271
+ | Kudzu | Yes | 393 B | 1.1 KB | **431 ms** |
272
+ | Astro | Yes | **158 B** | **365 B** | 974 ms |
273
+ | Svelte CSR | No | 10.5 KB | 26.9 KB | 961 ms |
274
+ | Qwik CSR | No | 20.6 KB | 57.8 KB | 660 ms |
275
+ | Vue CSR | No | 24.0 KB | 60.3 KB | 859 ms |
276
+ | React CSR | No | 59.2 KB | 189.0 KB | 1133 ms |
277
+ | Next.js | Yes | 182.1 KB | 652.2 KB | 3269 ms |
261
278
 
262
279
  Astro produces the smallest hand-authored counter. Kudzu's advantage in this fixture is React-shaped state code with a sub-1 KB runtime, not the smallest possible JavaScript.
263
280
 
@@ -267,13 +284,13 @@ Same content and CSS across every fixture:
267
284
 
268
285
  | Framework | Initial content | Initial JS gzip | Total output | Clean build |
269
286
  |---|---:|---:|---:|---:|
270
- | Kudzu | Yes | **0 B** | 3.2 KB | **380 ms** |
271
- | Astro | Yes | **0 B** | **3.0 KB** | 965 ms |
272
- | Svelte CSR | No | 10.2 KB | 27.2 KB | 824 ms |
273
- | Qwik CSR | No | 20.2 KB | 59.6 KB | 594 ms |
274
- | Vue CSR | No | 24.2 KB | 62.3 KB | 755 ms |
275
- | React CSR | No | 59.8 KB | 192.3 KB | 1041 ms |
276
- | Next.js | Yes | 182.6 KB | 663.6 KB | 2975 ms |
287
+ | Kudzu | Yes | **0 B** | 3.2 KB | **385 ms** |
288
+ | Astro | Yes | **0 B** | **3.0 KB** | 970 ms |
289
+ | Svelte CSR | No | 10.2 KB | 27.2 KB | 829 ms |
290
+ | Qwik CSR | No | 20.2 KB | 59.6 KB | 634 ms |
291
+ | Vue CSR | No | 24.2 KB | 62.3 KB | 767 ms |
292
+ | React CSR | No | 59.8 KB | 192.3 KB | 1098 ms |
293
+ | Next.js | Yes | 182.6 KB | 663.6 KB | 3217 ms |
277
294
 
278
295
  ### 1,000-item Keyed List
279
296
 
@@ -281,15 +298,15 @@ The list starts with 1,000 keyed items, then updates every label, reverses the o
281
298
 
282
299
  | Framework | Initial content | Initial JS gzip | Total output | Build | Update | Reverse | Remove | Add | Operations total |
283
300
  |---|---:|---:|---:|---:|---:|---:|---:|---:|---:|
284
- | Astro | Yes | **324 B** | **43.6 KB** | 852 ms | **3.5 ms** | 25.8 ms | 15.8 ms | **17.9 ms** | **63.0 ms** |
285
- | 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** |
286
- | 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 |
287
- | 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 |
288
- | 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 |
289
- | 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 |
290
- | 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 |
291
-
292
- 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.
301
+ | Astro | Yes | **324 B** | **43.6 KB** | 869 ms | **3.9 ms** | 29.4 ms | 13.9 ms | **18.3 ms** | **65.5 ms** |
302
+ | Kudzu | Yes | 5.0 KB | 60.3 KB | **437 ms** | 7.0 ms | 31.7 ms | **6.7 ms** | 22.0 ms | 67.4 ms |
303
+ | Vue CSR | No | 24.3 KB | 61.3 KB | 813 ms | 11.3 ms | 37.5 ms | 10.1 ms | 21.4 ms | 80.3 ms |
304
+ | Next.js | Yes | 182.2 KB | 695.2 KB | 3069 ms | 7.8 ms | 40.6 ms | 9.8 ms | 22.2 ms | 80.4 ms |
305
+ | React CSR | No | 59.3 KB | 189.4 KB | 1073 ms | 10.5 ms | 40.2 ms | 9.9 ms | 21.4 ms | 82.0 ms |
306
+ | Qwik CSR | No | 22.2 KB | 64.1 KB | 625 ms | 13.3 ms | **25.4 ms** | 37.1 ms | 24.4 ms | 100.2 ms |
307
+ | Svelte CSR | No | 12.9 KB | 33.1 KB | 905 ms | 6.5 ms | 72.3 ms | 9.2 ms | 22.3 ms | 110.3 ms |
308
+
309
+ 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. In this run Kudzu's keyed-list operations total 67.4 ms, 1.9 ms behind Astro and 14.6 ms ahead of React across all four operations.
293
310
 
294
311
  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.
295
312
 
@@ -3,6 +3,7 @@ import { randomUUID } from "node:crypto"
3
3
  import { cp, mkdir, readFile, readdir, rm, stat, watch, writeFile } from "node:fs/promises"
4
4
  import { extname, join, relative, resolve, sep } from "node:path"
5
5
  import { pathToFileURL } from "node:url"
6
+ import { transform } from "esbuild"
6
7
  import ts from "typescript"
7
8
  import { renderPage } from "./core.mjs"
8
9
  import { stateSchema } from "./dev-state.js"
@@ -15,7 +16,7 @@ const outputDirectory = join(root, "dist")
15
16
 
16
17
  const devClient = (session, revision, schema) => `<script>(()=>{const show=event=>{let box=document.getElementById("__kudzu_error");if(!box){box=document.createElement("div");box.id="__kudzu_error";box.setAttribute("role","alert");box.setAttribute("aria-live","assertive");box.style.cssText="position:fixed;inset:0;z-index:2147483647;overflow:auto;padding:2rem;background:#200;color:#fff;font:16px/1.5 ui-monospace,monospace";const title=document.createElement("strong"),text=document.createElement("pre");title.textContent="Kudzu build error";text.style.whiteSpace="pre-wrap";box.append(title,text);document.body.append(box)}box.querySelector("pre").textContent=event.data};const schema=${inlineJson(schema)},route=location.pathname+location.search+location.hash,urls=[...document.querySelectorAll('script[type="module"][src]')].map(node=>node.src).filter(url=>/\\/assets\\/kudzu(?:-(?:binding|list|native))?\\.js$/.test(new URL(url).pathname));const devImport=import("/__kudzu_dev.js"),runtimeImports=Promise.allSettled(urls.map(url=>import(url)));const ready=(async()=>{const dev=await devImport,modules=await runtimeImports,runtime=modules.find(result=>result.status==="fulfilled"&&result.value.browserState instanceof Map&&typeof result.value.commitDom==="function")?.value;try{dev.restoreState(sessionStorage,route,runtime?.browserState,schema,runtime?.commitDom)}catch{}return{dev,runtime}})().catch(()=>({}));const events=new EventSource("/__kudzu_reload?session=${session}&revision=${revision}");let reloading=false;events.addEventListener("reload",async()=>{if(reloading)return;reloading=true;try{const{dev,runtime}=await ready;dev?.snapshotState(sessionStorage,route,runtime?.browserState,schema)}catch{}location.reload()});events.addEventListener("build-error",show)})()</script>`
17
18
 
18
- export async function build({ quiet = false } = {}) {
19
+ export async function build({ quiet = false, minify = true } = {}) {
19
20
  await rm(workDirectory, { recursive: true, force: true })
20
21
  await rm(outputDirectory, { recursive: true, force: true })
21
22
  await mkdir(workDirectory, { recursive: true })
@@ -69,32 +70,32 @@ export async function build({ quiet = false } = {}) {
69
70
  if (behaviorCount) {
70
71
  const runtimeFile = bindingCount || listCount || hasNativeHandlers ? "./shared-runtime.js" : "./runtime.js"
71
72
  const runtime = specializeRuntime(await readFile(new URL(runtimeFile, import.meta.url), "utf8"), commandEvents, stateSeedCount > 0)
72
- await writeFile(join(assetsDirectory, "kudzu.js"), runtime)
73
+ await writeJavaScript(join(assetsDirectory, "kudzu.js"), runtime, minify)
73
74
  }
74
- if (bindingCount || hasNativeHandlers) await cp(new URL("./serialization.js", import.meta.url), join(assetsDirectory, "kudzu-serialization.js"))
75
+ if (bindingCount || hasNativeHandlers) await writeJavaScript(join(assetsDirectory, "kudzu-serialization.js"), await readFile(new URL("./serialization.js", import.meta.url), "utf8"), minify)
75
76
  if (bindingCount) {
76
- await cp(new URL("./style.js", import.meta.url), join(assetsDirectory, "kudzu-style.js"))
77
+ await writeJavaScript(join(assetsDirectory, "kudzu-style.js"), await readFile(new URL("./style.js", import.meta.url), "utf8"), minify)
77
78
  const bindingRuntime = (await readFile(new URL("./binding-runtime.js", import.meta.url), "utf8"))
78
79
  .replace('"./shared-runtime.js"', '"./kudzu.js"')
79
80
  .replace('"./serialization.js"', '"./kudzu-serialization.js"')
80
81
  .replace('"./style.js"', '"./kudzu-style.js"')
81
- await writeFile(join(assetsDirectory, "kudzu-binding.js"), bindingRuntime)
82
+ await writeJavaScript(join(assetsDirectory, "kudzu-binding.js"), bindingRuntime, minify)
82
83
  }
83
84
  if (listCount) {
84
85
  const listRuntime = (await readFile(new URL("./list-runtime.js", import.meta.url), "utf8"))
85
86
  .replace('"./shared-runtime.js"', '"./kudzu.js"')
86
- await writeFile(join(assetsDirectory, "kudzu-list.js"), listRuntime)
87
+ await writeJavaScript(join(assetsDirectory, "kudzu-list.js"), listRuntime, minify)
87
88
  }
88
89
  if (hasNativeHandlers) {
89
90
  const nativeRuntime = (await readFile(new URL("./native-runtime.js", import.meta.url), "utf8"))
90
91
  .replace('"./shared-runtime.js"', '"./kudzu.js"')
91
92
  .replace('"./serialization.js"', '"./kudzu-serialization.js"')
92
- await writeFile(join(assetsDirectory, "kudzu-native.js"), specializeNativeRuntime(nativeRuntime, nativeEvents, nativeModules))
93
+ await writeJavaScript(join(assetsDirectory, "kudzu-native.js"), specializeNativeRuntime(nativeRuntime, nativeEvents, nativeModules), minify)
93
94
  }
94
95
  for (const handlerModule of handlerModules) {
95
96
  const output = join(assetsDirectory, handlerModule.path)
96
97
  await mkdir(resolve(output, ".."), { recursive: true })
97
- await writeFile(output, handlerModule.code)
98
+ await writeJavaScript(output, handlerModule.code, minify)
98
99
  }
99
100
  await writeFile(join(workDirectory, "kudzu-plan.json"), JSON.stringify({ routes: plans }, null, 2))
100
101
  if (hasStyles) await cp(join(sourceDirectory, "style.css"), join(assetsDirectory, "style.css"))
@@ -113,12 +114,17 @@ function specializeNativeRuntime(source, events, modules) {
113
114
  return `${imports}\n${specializeEvents(source, events).replace(/const modules = new Map\(\[[^\n]*\]\)/, `const modules = new Map([${entries}])`)}`
114
115
  }
115
116
 
116
- function specializeRuntime(source, events, hasStateSeed) {
117
+ export function specializeRuntime(source, events, hasStateSeed) {
117
118
  const specialized = specializeEvents(source, events)
118
119
  if (hasStateSeed) return specialized
119
120
  return specialized
120
121
  .replace(" const initialState = document.body.dataset.kState\n", "")
121
- .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", "")
122
+ .replace(/^ if \(initialState\).*\n/m, "")
123
+ }
124
+
125
+ async function writeJavaScript(file, source, minify) {
126
+ const code = minify ? (await transform(source, { format: "esm", legalComments: "none", minify: true, target: "es2022" })).code : source
127
+ await writeFile(file, code)
122
128
  }
123
129
 
124
130
  export function parseDevPort(value) {
@@ -136,7 +142,7 @@ export async function dev({ port = parseDevPort(process.env.PORT) } = {}) {
136
142
  let revision = 0
137
143
  const session = randomUUID()
138
144
  try {
139
- await build()
145
+ await build({ minify: false })
140
146
  revision++
141
147
  } catch (error) {
142
148
  buildError = errorText(error)
@@ -204,7 +210,7 @@ export async function dev({ port = parseDevPort(process.env.PORT) } = {}) {
204
210
  do {
205
211
  pending = false
206
212
  try {
207
- await build({ quiet: true })
213
+ await build({ quiet: true, minify: false })
208
214
  buildError = undefined
209
215
  revision++
210
216
  console.log(`Rebuilt after ${changedFile ?? "source change"}`)
@@ -2,6 +2,12 @@ export type StateSetter<T> = (value: T | ((previous: T) => T)) => void
2
2
 
3
3
  export function useState<T>(initialValue: T): [T, StateSetter<T>]
4
4
 
5
+ export interface RefObject<T> {
6
+ readonly current: T | null
7
+ }
8
+
9
+ export function useRef<T>(initialValue: null): RefObject<T>
10
+
5
11
  export function behavior(commands: Array<["add" | "set" | "log", unknown, unknown]>): unknown
6
12
  export function nativeBehavior(module: string, handler: string, states: Array<[string, unknown]>, scope: Array<[string, unknown]>): unknown
7
13
  export function binding(value: unknown, module: string, handler: string, states: Array<[string, unknown]>, scope: Array<[string, unknown]>): unknown
@@ -9,6 +9,7 @@ const listMarker = Symbol("kudzu.list")
9
9
  const listFieldMarker = Symbol("kudzu.listField")
10
10
  const listExpressionMarker = Symbol("kudzu.listExpression")
11
11
  const listItemMarker = Symbol("kudzu.listItem")
12
+ const refMarker = Symbol("kudzu.ref")
12
13
  const noSelectValue = Symbol("kudzu.no-select-value")
13
14
 
14
15
  let renderContext
@@ -37,6 +38,12 @@ export function useState(initialValue, name) {
37
38
  }]
38
39
  }
39
40
 
41
+ export function useRef(initialValue) {
42
+ if (!renderContext) throw new Error("useRef() can only run while rendering a Kudzu component")
43
+ if (initialValue !== null) throw new Error("Kudzu DOM refs must initialize with null")
44
+ return { [refMarker]: true, id: `r${renderContext.nextRef++}`, current: null }
45
+ }
46
+
40
47
  export function behavior(commands) {
41
48
  return {
42
49
  [behaviorMarker]: true,
@@ -158,6 +165,7 @@ function bindingDescriptor(value) {
158
165
 
159
166
  function serializeCapture(name, value, seen) {
160
167
  if (value?.[listItemMarker]) return { type: "list-item" }
168
+ if (value?.[refMarker]) return { type: "ref", id: value.id }
161
169
  if (value === null || typeof value === "string" || typeof value === "boolean") return value
162
170
  if (typeof value === "number") {
163
171
  return Number.isFinite(value) && !Object.is(value, -0) ? value : { type: "number", value: String(value) }
@@ -189,7 +197,7 @@ function serializeCapture(name, value, seen) {
189
197
  }
190
198
 
191
199
  export async function renderPage(component, metadata = {}) {
192
- 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 }
200
+ renderContext = { nextState: 0, nextRef: 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 }
193
201
 
194
202
  try {
195
203
  const body = await renderNode({ type: component, props: {} })
@@ -351,6 +359,12 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
351
359
 
352
360
  for (const [rawName, value] of Object.entries(props)) {
353
361
  if (rawName === "children" || rawName === "key") continue
362
+ if (rawName === "ref") {
363
+ if (!value?.[refMarker]) throw new Error("ref must be created by useRef(null)")
364
+ if (renderContext.listDepth) throw new Error("Refs are not supported in keyed lists")
365
+ attributes += ` data-k-ref="${value.id}"`
366
+ continue
367
+ }
354
368
  if (rawName === "selected" && selectValue !== noSelectValue) continue
355
369
  if (/^on/i.test(rawName) && !/^on[A-Z]/.test(rawName)) throw new Error(`${rawName} must use a camelCase event handler`)
356
370
  if (rawName.toLowerCase().startsWith("data-k-")) throw new Error(`${rawName} uses Kudzu's reserved data-k-* prefix`)
@@ -2,6 +2,7 @@ export function deserialize(value) {
2
2
  if (!value || typeof value !== "object") return value
3
3
  if (value.type === "undefined") return undefined
4
4
  if (value.type === "number") return value.value === "NaN" ? NaN : value.value === "Infinity" ? Infinity : value.value === "-Infinity" ? -Infinity : -0
5
+ if (value.type === "ref") return { get current() { return typeof document === "undefined" ? null : document.querySelector(`[data-k-ref="${value.id}"]`) } }
5
6
  if (value.type === "array") return value.value.map(deserialize)
6
7
  if (value.type === "object") {
7
8
  const object = value.nullPrototype ? Object.create(null) : {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kudzujs/core",
3
- "version": "0.4.7",
3
+ "version": "0.4.8",
4
4
  "description": "HTML-first TSX framework with synchronous state semantics and no virtual DOM",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -53,6 +53,7 @@
53
53
  "preview": "wrangler dev"
54
54
  },
55
55
  "dependencies": {
56
+ "esbuild": "^0.28.1",
56
57
  "typescript": "^5.9.2"
57
58
  },
58
59
  "devDependencies": {