@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 +36 -24
- package/framework/build.mjs +52 -0
- package/package.json +1 -1
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
|
-
-
|
|
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 | **
|
|
273
|
-
| Astro | Yes | **158 B** | **365 B** |
|
|
274
|
-
| Svelte CSR | No | 10.5 KB | 26.9 KB |
|
|
275
|
-
| Qwik CSR | No | 20.6 KB | 57.8 KB |
|
|
276
|
-
| Vue CSR | No | 24.0 KB | 60.3 KB |
|
|
277
|
-
| React CSR | No | 59.2 KB | 189.0 KB |
|
|
278
|
-
| Next.js | Yes | 182.1 KB | 652.2 KB |
|
|
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 | **
|
|
289
|
-
| Astro | Yes | **0 B** | **3.0 KB** |
|
|
290
|
-
| Svelte CSR | No | 10.2 KB | 27.2 KB |
|
|
291
|
-
| Qwik CSR | No | 20.2 KB | 59.6 KB |
|
|
292
|
-
| Vue CSR | No | 24.2 KB | 62.3 KB |
|
|
293
|
-
| React CSR | No | 59.8 KB | 192.3 KB |
|
|
294
|
-
| Next.js | Yes | 182.6 KB | 663.6 KB |
|
|
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** |
|
|
303
|
-
| Kudzu | Yes | 5.0 KB | 60.3 KB | **
|
|
304
|
-
| Vue CSR | No | 24.3 KB | 61.3 KB |
|
|
305
|
-
|
|
|
306
|
-
|
|
|
307
|
-
| Qwik CSR | No | 22.2 KB | 64.1 KB |
|
|
308
|
-
| Svelte CSR | No | 12.9 KB | 33.1 KB |
|
|
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
|
|
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
|
|
package/framework/build.mjs
CHANGED
|
@@ -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
|
}
|