@kudzujs/core 0.4.9 → 0.4.10

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
@@ -160,6 +160,17 @@ Logical state persists across branch switches, while uncontrolled DOM state rese
160
160
 
161
161
  Reactive conditional DOM currently targets the HTML namespace and is rejected inside SVG or MathML.
162
162
 
163
+ Top-level immutable JSX locals can hold static or state-dependent branches:
164
+
165
+ ```tsx
166
+ const menu = open ? <MenuBar /> : <p>Menu dormant</p>
167
+ const content = open && menu
168
+
169
+ return <main>{content}</main>
170
+ ```
171
+
172
+ Kudzu compiles the local initializer to the same bounded DOM ranges as an inline condition. Reassigned, block-scoped, and keyed-list alias locals remain unsupported.
173
+
163
174
  ## Keyed Lists
164
175
 
165
176
  Map local array state directly to one keyed JSX element per item:
@@ -250,11 +261,12 @@ Supported:
250
261
  - Object DOM refs in native event handlers
251
262
  - Controlled `value` and `checked` form properties
252
263
  - Conditional child `&&` and ternary DOM patches
264
+ - Top-level immutable JSX locals
253
265
  - Direct keyed local-state lists
254
266
 
255
267
  Not implemented yet:
256
268
 
257
- - Generalized JSX-valued locals and non-direct list item expressions
269
+ - Block-scoped JSX locals and non-direct list item expressions
258
270
  - Server actions and request-time SSR
259
271
  - Imported client helpers and React package islands
260
272
  - HMR and framework DevTools
@@ -269,13 +281,13 @@ Same counter with initial value `7` and increment/decrement buttons:
269
281
 
270
282
  | Framework | Initial content | Initial JS gzip | Total output | Clean build |
271
283
  |---|---:|---:|---:|---:|
272
- | Kudzu | Yes | 393 B | 1.1 KB | **431 ms** |
273
- | Astro | Yes | **158 B** | **365 B** | 974 ms |
274
- | Svelte CSR | No | 10.5 KB | 26.9 KB | 961 ms |
275
- | Qwik CSR | No | 20.6 KB | 57.8 KB | 660 ms |
276
- | Vue CSR | No | 24.0 KB | 60.3 KB | 859 ms |
277
- | React CSR | No | 59.2 KB | 189.0 KB | 1133 ms |
278
- | Next.js | Yes | 182.1 KB | 652.2 KB | 3269 ms |
284
+ | Kudzu | Yes | 393 B | 1.1 KB | **383 ms** |
285
+ | Astro | Yes | **158 B** | **365 B** | 914 ms |
286
+ | Svelte CSR | No | 10.5 KB | 26.9 KB | 892 ms |
287
+ | Qwik CSR | No | 20.6 KB | 57.8 KB | 606 ms |
288
+ | Vue CSR | No | 24.0 KB | 60.3 KB | 815 ms |
289
+ | React CSR | No | 59.2 KB | 189.0 KB | 1044 ms |
290
+ | Next.js | Yes | 182.1 KB | 652.2 KB | 3030 ms |
279
291
 
280
292
  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.
281
293
 
@@ -285,13 +297,13 @@ Same content and CSS across every fixture:
285
297
 
286
298
  | Framework | Initial content | Initial JS gzip | Total output | Clean build |
287
299
  |---|---:|---:|---:|---:|
288
- | Kudzu | Yes | **0 B** | 3.2 KB | **385 ms** |
289
- | Astro | Yes | **0 B** | **3.0 KB** | 970 ms |
290
- | Svelte CSR | No | 10.2 KB | 27.2 KB | 829 ms |
291
- | Qwik CSR | No | 20.2 KB | 59.6 KB | 634 ms |
292
- | Vue CSR | No | 24.2 KB | 62.3 KB | 767 ms |
293
- | React CSR | No | 59.8 KB | 192.3 KB | 1098 ms |
294
- | Next.js | Yes | 182.6 KB | 663.6 KB | 3217 ms |
300
+ | Kudzu | Yes | **0 B** | 3.2 KB | **409 ms** |
301
+ | Astro | Yes | **0 B** | **3.0 KB** | 1053 ms |
302
+ | Svelte CSR | No | 10.2 KB | 27.2 KB | 863 ms |
303
+ | Qwik CSR | No | 20.2 KB | 59.6 KB | 611 ms |
304
+ | Vue CSR | No | 24.2 KB | 62.3 KB | 782 ms |
305
+ | React CSR | No | 59.8 KB | 192.3 KB | 1062 ms |
306
+ | Next.js | Yes | 182.6 KB | 663.6 KB | 3118 ms |
295
307
 
296
308
  ### 1,000-item Keyed List
297
309
 
@@ -299,15 +311,15 @@ The list starts with 1,000 keyed items, then updates every label, reverses the o
299
311
 
300
312
  | Framework | Initial content | Initial JS gzip | Total output | Build | Update | Reverse | Remove | Add | Operations total |
301
313
  |---|---:|---:|---:|---:|---:|---:|---:|---:|---:|
302
- | Astro | Yes | **324 B** | **43.6 KB** | 889 ms | **3.9 ms** | 27.6 ms | 7.9 ms | **18.1 ms** | **57.5 ms** |
303
- | Kudzu | Yes | 5.0 KB | 60.3 KB | **447 ms** | 6.3 ms | 31.0 ms | **7.3 ms** | 20.7 ms | 65.3 ms |
304
- | Vue CSR | No | 24.3 KB | 61.3 KB | 805 ms | 10.9 ms | 36.4 ms | 12.2 ms | 21.0 ms | 80.5 ms |
305
- | Next.js | Yes | 182.2 KB | 695.2 KB | 3081 ms | 7.9 ms | 40.2 ms | 9.4 ms | 23.5 ms | 81.0 ms |
306
- | React CSR | No | 59.3 KB | 189.4 KB | 1075 ms | 10.8 ms | 40.6 ms | 11.0 ms | 21.0 ms | 83.4 ms |
307
- | Qwik CSR | No | 22.2 KB | 64.1 KB | 655 ms | 11.9 ms | **25.1 ms** | 40.7 ms | 22.6 ms | 100.3 ms |
308
- | Svelte CSR | No | 12.9 KB | 33.1 KB | 895 ms | 6.1 ms | 69.9 ms | 11.2 ms | 20.8 ms | 108.0 ms |
309
-
310
- 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 65.3 ms, 7.8 ms behind Astro and 18.1 ms ahead of React across all four operations.
314
+ | Astro | Yes | **324 B** | **43.6 KB** | 862 ms | **3.5 ms** | 24.8 ms | **8.4 ms** | **17.9 ms** | **54.6 ms** |
315
+ | Kudzu | Yes | 5.0 KB | 60.3 KB | **435 ms** | 6.1 ms | 29.0 ms | 12.8 ms | 19.5 ms | 67.4 ms |
316
+ | Vue CSR | No | 24.3 KB | 61.3 KB | 789 ms | 9.3 ms | 32.1 ms | 10.6 ms | 18.6 ms | 70.6 ms |
317
+ | React CSR | No | 59.3 KB | 189.4 KB | 1049 ms | 8.8 ms | 34.7 ms | 13.5 ms | 18.2 ms | 75.2 ms |
318
+ | Next.js | Yes | 182.2 KB | 695.2 KB | 3029 ms | 7.0 ms | 37.2 ms | 12.9 ms | 21.9 ms | 79.0 ms |
319
+ | Qwik CSR | No | 22.2 KB | 64.1 KB | 623 ms | 13.4 ms | **21.7 ms** | 33.6 ms | 28.9 ms | 97.6 ms |
320
+ | Svelte CSR | No | 12.9 KB | 33.1 KB | 870 ms | 5.6 ms | 61.3 ms | 15.6 ms | 18.3 ms | 100.8 ms |
321
+
322
+ 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, 12.8 ms behind Astro and 7.8 ms ahead of React across all four operations.
311
323
 
312
324
  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.
313
325
 
@@ -322,6 +322,8 @@ function createKudzuTransformer(nativeHandlers, reactiveBindings, listExpression
322
322
  const factory = context.factory
323
323
  const settersByFunction = new Map()
324
324
  const functions = new Map()
325
+ const jsxLocalDeclarations = new Map()
326
+ const jsxLocalsByFunction = new Map()
325
327
  const listValues = new WeakMap()
326
328
  const listEventItems = new WeakMap()
327
329
  let usesBehavior = false
@@ -346,9 +348,29 @@ function createKudzuTransformer(nativeHandlers, reactiveBindings, listExpression
346
348
  if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && (ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer))) {
347
349
  functions.set(node.name.text, node.initializer)
348
350
  }
351
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && isTopLevelConst(node)) {
352
+ const owner = nearestFunction(node)
353
+ const declarations = jsxLocalDeclarations.get(owner) ?? new Map()
354
+ declarations.set(node.name.text, node.initializer)
355
+ jsxLocalDeclarations.set(owner, declarations)
356
+ }
349
357
  ts.forEachChild(node, collect)
350
358
  }
351
359
  collect(sourceFile)
360
+ for (const [owner, declarations] of jsxLocalDeclarations) {
361
+ const names = new Set()
362
+ let changed = true
363
+ while (changed) {
364
+ changed = false
365
+ for (const [name, initializer] of declarations) {
366
+ if (!names.has(name) && isJsxLocalValue(initializer, names)) {
367
+ names.add(name)
368
+ changed = true
369
+ }
370
+ }
371
+ }
372
+ jsxLocalsByFunction.set(owner, names)
373
+ }
352
374
 
353
375
  const visitor = node => {
354
376
  if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text.startsWith(".")) {
@@ -369,6 +391,21 @@ function createKudzuTransformer(nativeHandlers, reactiveBindings, listExpression
369
391
  return factory.updateVariableDeclaration(node, node.name, node.exclamationToken, node.type, initializer)
370
392
  }
371
393
 
394
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && jsxLocalsByFunction.get(nearestFunction(node))?.has(node.name.text) && referencesIdentifier(nearestFunction(node).body, node.name.text)) {
395
+ const parts = conditionalParts(node.initializer)
396
+ if (parts) {
397
+ const setters = settersForNode(node, settersByFunction)
398
+ const usedStates = referencedStateNames(parts.condition, setters)
399
+ const captures = captureNames(parts.condition, parts.condition, setters)
400
+ if (usedStates.size || captures.size) {
401
+ usesBehavior = true
402
+ usesConditional = true
403
+ const compiled = compileConditional(parts.kind, parts.condition, ts.visitNode(parts.truthy, visitor), ts.visitNode(parts.falsy, visitor), setters, factory, context, reactiveBindings, handlerUrl)
404
+ return factory.updateVariableDeclaration(node, node.name, node.exclamationToken, node.type, compiled)
405
+ }
406
+ }
407
+ }
408
+
372
409
  if (ts.isJsxExpression(node) && node.expression && listValues.has(node.expression)) {
373
410
  return factory.updateJsxExpression(node, compileListValue(node.expression, listValues.get(node.expression), factory, listExpressions, handlerUrl))
374
411
  }
@@ -618,6 +655,21 @@ function unwrapExpression(node) {
618
655
  return ts.isParenthesizedExpression(node) ? unwrapExpression(node.expression) : node
619
656
  }
620
657
 
658
+ function isTopLevelConst(node) {
659
+ const list = node.parent
660
+ const statement = list?.parent
661
+ const owner = nearestFunction(node)
662
+ return ts.isVariableDeclarationList(list) && (list.flags & ts.NodeFlags.Const) !== 0 && ts.isVariableStatement(statement) && statement.parent === owner?.body
663
+ }
664
+
665
+ function isJsxLocalValue(expression, known) {
666
+ const value = unwrapExpression(expression)
667
+ if (ts.isJsxElement(value) || ts.isJsxSelfClosingElement(value) || ts.isJsxFragment(value)) return true
668
+ if (ts.isIdentifier(value)) return known.has(value.text)
669
+ const parts = conditionalParts(value)
670
+ return Boolean(parts && (isJsxLocalValue(parts.truthy, known) || isJsxLocalValue(parts.falsy, known)))
671
+ }
672
+
621
673
  function compileReactiveBinding(expression, setters, factory, context, reactiveBindings, handlerUrl) {
622
674
  return factory.createCallExpression(factory.createIdentifier("__kBinding"), undefined, compileReactiveExpression(expression, setters, factory, context, reactiveBindings, handlerUrl))
623
675
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kudzujs/core",
3
- "version": "0.4.9",
3
+ "version": "0.4.10",
4
4
  "description": "HTML-first TSX framework with synchronous state semantics and no virtual DOM",
5
5
  "type": "module",
6
6
  "license": "MIT",