@kudzujs/core 0.4.8 → 0.4.9
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 +12 -11
- package/framework/README.md +1 -1
- package/framework/build.mjs +14 -4
- package/framework/core.d.ts +1 -0
- package/framework/core.mjs +4 -1
- package/framework/list-runtime.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -175,6 +175,7 @@ const [items, setItems] = useState([
|
|
|
175
175
|
key={item.id}
|
|
176
176
|
className={item.done ? "done" : "active"}
|
|
177
177
|
aria-label={`${item.name} item`}
|
|
178
|
+
style={{ opacity: item.done ? 0.5 : 1 }}
|
|
178
179
|
>
|
|
179
180
|
{item.name.toUpperCase()}
|
|
180
181
|
<button onClick={() => setItems(items.filter(entry => entry.id !== item.id))}>Remove</button>
|
|
@@ -182,9 +183,9 @@ const [items, setItems] = useState([
|
|
|
182
183
|
)}</ul>
|
|
183
184
|
```
|
|
184
185
|
|
|
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.
|
|
186
|
+
Kudzu emits initial items as static HTML, then adds, removes, updates, styles, 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.
|
|
186
187
|
|
|
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,
|
|
188
|
+
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, refs, and `dangerouslySetInnerHTML` remain unsupported. Keyed rows must be placed inside an explicit `<tbody>`, `<thead>`, or `<tfoot>`.
|
|
188
189
|
|
|
189
190
|
## Normal JavaScript
|
|
190
191
|
|
|
@@ -298,15 +299,15 @@ The list starts with 1,000 keyed items, then updates every label, reverses the o
|
|
|
298
299
|
|
|
299
300
|
| Framework | Initial content | Initial JS gzip | Total output | Build | Update | Reverse | Remove | Add | Operations total |
|
|
300
301
|
|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|
|
|
301
|
-
| Astro | Yes | **324 B** | **43.6 KB** |
|
|
302
|
-
| Kudzu | Yes | 5.0 KB | 60.3 KB | **
|
|
303
|
-
| Vue CSR | No | 24.3 KB | 61.3 KB |
|
|
304
|
-
| Next.js | Yes | 182.2 KB | 695.2 KB |
|
|
305
|
-
| React CSR | No | 59.3 KB | 189.4 KB |
|
|
306
|
-
| Qwik CSR | No | 22.2 KB | 64.1 KB |
|
|
307
|
-
| Svelte CSR | No | 12.9 KB | 33.1 KB |
|
|
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
|
|
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.
|
|
310
311
|
|
|
311
312
|
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.
|
|
312
313
|
|
package/framework/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
- `runtime.js`: command-only runtime for direct state-to-text patches.
|
|
7
7
|
- `shared-runtime.js`: command runtime with capability commit and DOM lifecycle hooks, emitted only when needed.
|
|
8
8
|
- `binding-runtime.js`: optional generic attributes, form properties, and conditional range patches.
|
|
9
|
-
- `list-runtime.js`: optional keyed list validation, external item-expression evaluation, dynamic item-handler scopes, moves, and cleanup.
|
|
9
|
+
- `list-runtime.js`: optional keyed list validation, external item-expression evaluation, dynamic styles and item-handler scopes, moves, and cleanup.
|
|
10
10
|
- `serialization.js`: capture deserialization shared by binding and native handlers.
|
|
11
11
|
- `native-runtime.js`: optional runtime for normal synchronous and asynchronous ESM handlers.
|
|
12
12
|
- `dev-state.js`: dev-only, short-lived logical-state snapshot validation and restoration.
|
package/framework/build.mjs
CHANGED
|
@@ -37,6 +37,7 @@ export async function build({ quiet = false, minify = true } = {}) {
|
|
|
37
37
|
let behaviorCount = 0
|
|
38
38
|
let bindingCount = 0
|
|
39
39
|
let listCount = 0
|
|
40
|
+
let listStyleCount = 0
|
|
40
41
|
let stateSeedCount = 0
|
|
41
42
|
const plans = []
|
|
42
43
|
const hasStyles = await exists(join(sourceDirectory, "style.css"))
|
|
@@ -58,6 +59,7 @@ export async function build({ quiet = false, minify = true } = {}) {
|
|
|
58
59
|
if (result.hasBehaviors) behaviorCount++
|
|
59
60
|
if (result.hasBindings) bindingCount++
|
|
60
61
|
if (result.hasLists) listCount++
|
|
62
|
+
if (result.hasListStyles) listStyleCount++
|
|
61
63
|
if (result.hasStateSeed) stateSeedCount++
|
|
62
64
|
}
|
|
63
65
|
|
|
@@ -73,8 +75,8 @@ export async function build({ quiet = false, minify = true } = {}) {
|
|
|
73
75
|
await writeJavaScript(join(assetsDirectory, "kudzu.js"), runtime, minify)
|
|
74
76
|
}
|
|
75
77
|
if (bindingCount || hasNativeHandlers) await writeJavaScript(join(assetsDirectory, "kudzu-serialization.js"), await readFile(new URL("./serialization.js", import.meta.url), "utf8"), minify)
|
|
78
|
+
if (bindingCount || listStyleCount) await writeJavaScript(join(assetsDirectory, "kudzu-style.js"), await readFile(new URL("./style.js", import.meta.url), "utf8"), minify)
|
|
76
79
|
if (bindingCount) {
|
|
77
|
-
await writeJavaScript(join(assetsDirectory, "kudzu-style.js"), await readFile(new URL("./style.js", import.meta.url), "utf8"), minify)
|
|
78
80
|
const bindingRuntime = (await readFile(new URL("./binding-runtime.js", import.meta.url), "utf8"))
|
|
79
81
|
.replace('"./shared-runtime.js"', '"./kudzu.js"')
|
|
80
82
|
.replace('"./serialization.js"', '"./kudzu-serialization.js"')
|
|
@@ -82,8 +84,16 @@ export async function build({ quiet = false, minify = true } = {}) {
|
|
|
82
84
|
await writeJavaScript(join(assetsDirectory, "kudzu-binding.js"), bindingRuntime, minify)
|
|
83
85
|
}
|
|
84
86
|
if (listCount) {
|
|
85
|
-
|
|
87
|
+
let listRuntime = (await readFile(new URL("./list-runtime.js", import.meta.url), "utf8"))
|
|
86
88
|
.replace('"./shared-runtime.js"', '"./kudzu.js"')
|
|
89
|
+
const stylePatch = ` if (target === "style") {
|
|
90
|
+
const style = serializeStyle(value)
|
|
91
|
+
if (style) node.setAttribute("style", style)
|
|
92
|
+
else node.removeAttribute("style")
|
|
93
|
+
return
|
|
94
|
+
}`
|
|
95
|
+
listRuntime = listRuntime.replace(" /* list-style */", listStyleCount ? stylePatch : "")
|
|
96
|
+
if (listStyleCount) listRuntime = `import { serializeStyle } from "./kudzu-style.js"\n${listRuntime}`
|
|
87
97
|
await writeJavaScript(join(assetsDirectory, "kudzu-list.js"), listRuntime, minify)
|
|
88
98
|
}
|
|
89
99
|
if (hasNativeHandlers) {
|
|
@@ -488,7 +498,7 @@ function validateKeyedList(parts, sourceFile, setters, listValues, listEventItem
|
|
|
488
498
|
const field = directProperty(expression, parts.item)
|
|
489
499
|
const isRootKey = ts.isJsxAttribute(node.parent) && node.parent.name.getText() === "key"
|
|
490
500
|
if (field && ["__proto__", "constructor", "prototype"].includes(field)) fail(node, `Keyed list item property "${field}" is not supported`)
|
|
491
|
-
if (field && ts.isJsxAttribute(node.parent) && ["
|
|
501
|
+
if (field && ts.isJsxAttribute(node.parent) && ["ref", "dangerouslysetinnerhtml"].includes(node.parent.name.getText().toLowerCase())) fail(node, `Keyed list item ${node.parent.name.getText()} is not supported`)
|
|
492
502
|
if (isRootKey) return
|
|
493
503
|
if (field) {
|
|
494
504
|
listValues.set(node.expression, { field })
|
|
@@ -496,7 +506,7 @@ function validateKeyedList(parts, sourceFile, setters, listValues, listEventItem
|
|
|
496
506
|
}
|
|
497
507
|
if (referencesIdentifier(expression, parts.item)) {
|
|
498
508
|
validateListExpression(expression, parts.item, node, fail)
|
|
499
|
-
if (ts.isJsxAttribute(node.parent) && ["
|
|
509
|
+
if (ts.isJsxAttribute(node.parent) && ["ref", "dangerouslysetinnerhtml"].includes(node.parent.name.getText().toLowerCase())) fail(node, `Keyed list item ${node.parent.name.getText()} is not supported`)
|
|
500
510
|
listValues.set(node.expression, { item: parts.item })
|
|
501
511
|
return
|
|
502
512
|
}
|
package/framework/core.d.ts
CHANGED
package/framework/core.mjs
CHANGED
|
@@ -197,7 +197,7 @@ function serializeCapture(name, value, seen) {
|
|
|
197
197
|
}
|
|
198
198
|
|
|
199
199
|
export async function renderPage(component, metadata = {}) {
|
|
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 }
|
|
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, hasListStyles: false }
|
|
201
201
|
|
|
202
202
|
try {
|
|
203
203
|
const body = await renderNode({ type: component, props: {} })
|
|
@@ -235,6 +235,7 @@ export async function renderPage(component, metadata = {}) {
|
|
|
235
235
|
hasBehaviors: renderContext.hasBehaviors,
|
|
236
236
|
hasBindings: renderContext.hasBindings,
|
|
237
237
|
hasLists: renderContext.hasLists,
|
|
238
|
+
hasListStyles: renderContext.hasListStyles,
|
|
238
239
|
hasStateSeed: initialState.length > 0,
|
|
239
240
|
plan: {
|
|
240
241
|
states: Object.entries(renderContext.states).map(([id, state]) => ({ id, ...state })),
|
|
@@ -397,11 +398,13 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
|
|
|
397
398
|
if (value?.[listFieldMarker]) {
|
|
398
399
|
attributes += renderAttribute(name, value.value)
|
|
399
400
|
listAttributes.push([name, value.field])
|
|
401
|
+
if (name === "style") renderContext.hasListStyles = true
|
|
400
402
|
continue
|
|
401
403
|
}
|
|
402
404
|
if (value?.[listExpressionMarker]) {
|
|
403
405
|
attributes += renderAttribute(name, value.value)
|
|
404
406
|
listExpressionAttributes.push([name, value.module, value.handler])
|
|
407
|
+
if (name === "style") renderContext.hasListStyles = true
|
|
405
408
|
continue
|
|
406
409
|
}
|
|
407
410
|
if (value?.[signalMarker] || value?.[bindingMarker]) {
|