@kudzujs/core 0.7.9 → 0.7.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 +3 -3
- package/RELEASES.md +23 -0
- package/framework/README.md +1 -1
- package/framework/build.mjs +111 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Kudzu is designed so ordinary common React-shaped TSX can migrate with minimal s
|
|
|
10
10
|
|
|
11
11
|
> Experimental `0.7.x`: the compiler API and supported TSX surface may change.
|
|
12
12
|
|
|
13
|
-
**0.7.
|
|
13
|
+
**0.7.10:** Component composition. Existing specialized collection wrappers and keyed rows now retain direct analyzable prop spreads and forwarded JSX children without adding a browser component runtime. See [release notes](./RELEASES.md#0710---component-composition).
|
|
14
14
|
|
|
15
15
|
Documentation: [kudzujs.cloud/docs](https://kudzujs.cloud/docs)
|
|
16
16
|
|
|
@@ -465,7 +465,7 @@ Initial child rows remain complete HTML. Kudzu stores one child row prototype, i
|
|
|
465
465
|
|
|
466
466
|
In the matched 100-parent/1,000-child fixture, Kudzu measured 1.3/0.4/5.0/0.7 ms for child update and condition change, child reverse, parent reverse, and parent removal. Hand-written Astro/native measured 0.5/0.4/3.9/0.2 ms, Svelte 2.7/1.2/6.7/1.3 ms, Vue 4.9/2.5/6.1/2.2 ms, and React 11.8/5.0/8.2/4.4 ms. Kudzu and Astro emit initial rows while the CSR targets do not, so artifact sizes are not architecture-equivalent.
|
|
467
467
|
|
|
468
|
-
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. Collections must remain anchored to local array state or a supported static named import; inline callbacks accept one or two identifier parameters, and row roots must be intrinsic JSX or supported same-file/relative components with `key={item.<field>}` or `key={index}`. Specialized keyed row components accept missing destructured string, finite-number, boolean, or `null` props with literal defaults. State-backed list wrappers use one destructured props parameter, an intrinsic return root, no effects, and a direct local-state prop. Whole-item, computed, nested, derived, `__proto__`, `prototype`, and `constructor` effect dependencies are rejected. A collection alias may only be rendered once and cannot be read by other JavaScript. Collection callbacks and derived expressions must be pure and synchronous: supported reads, operators, templates, approved read-only methods, deterministic `Math`, and primitive conversion compile; imported callbacks, browser globals, promises, mutation, arbitrary calls, and prototype-sensitive properties fail. Lazy or dynamic keyed-row state initializers, non-`null` refs, callback refs, package/namespace/star row imports, same-file exported rows, reusable aliases, prop spreads
|
|
468
|
+
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. Collections must remain anchored to local array state or a supported static named import; inline callbacks accept one or two identifier parameters, and row roots must be intrinsic JSX or supported same-file/relative components with `key={item.<field>}` or `key={index}`. Specialized wrappers and keyed rows accept forwarded JSX children and source-ordered prop spreads from inline object literals or one direct `const` object literal declared in the calling component. Specialized keyed row components accept missing destructured string, finite-number, boolean, or `null` props with literal defaults. State-backed list wrappers use one destructured props parameter, an intrinsic return root, no effects, and a direct local-state prop. Whole-item, computed, nested, derived, `__proto__`, `prototype`, and `constructor` effect dependencies are rejected. A collection alias may only be rendered once and cannot be read by other JavaScript. Collection callbacks and derived expressions must be pure and synchronous: supported reads, operators, templates, approved read-only methods, deterministic `Math`, and primitive conversion compile; imported callbacks, browser globals, promises, mutation, arbitrary calls, and prototype-sensitive properties fail. Lazy or dynamic keyed-row state initializers, non-`null` refs, callback refs, package/namespace/star row imports, same-file exported rows, reusable aliases, dynamic/computed prop spreads, rest props, non-primitive defaults, fragments, and `dangerouslySetInnerHTML` remain unsupported. Keyed rows must be placed inside an explicit `<tbody>`, `<thead>`, or `<tfoot>`.
|
|
469
469
|
|
|
470
470
|
The focused wrapper fixture emits 1,393 B raw / 500 B gzip HTML and 10,719 B raw / 4,665 B gzip JavaScript across its route capabilities. After one warm-up, seven clean builds measured 314.1, 325.3, 322.3, 327.2, 336.1, 322.4, and 315.0 ms, with a 322.4 ms median.
|
|
471
471
|
|
|
@@ -676,7 +676,7 @@ Supported:
|
|
|
676
676
|
|
|
677
677
|
Selected current migration limits:
|
|
678
678
|
|
|
679
|
-
-
|
|
679
|
+
- Non-primitive prop defaults/rest, exported reusable specialized rows, `forwardRef`, and `useId` still need fixture-driven component specialization. Direct analyzable prop spreads and forwarded JSX children are supported across existing specialized collection boundaries.
|
|
680
680
|
- Hookful non-keyed imported components and pure lazy state/reducer initializers remain narrower than ordinary React.
|
|
681
681
|
- Reusable collection aliases, computed child collections, imported pure transforms, and common immutable pagination/sorting forms need failing fixtures before expansion.
|
|
682
682
|
- Effect dependencies remain limited to directly analyzable primitive signals and supported keyed-item properties.
|
package/RELEASES.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Kudzu Releases
|
|
2
2
|
|
|
3
|
+
## 0.7.10 - Component composition
|
|
4
|
+
|
|
5
|
+
Kudzu 0.7.10 preserves common component composition across existing compiler-specialized collection boundaries.
|
|
6
|
+
|
|
7
|
+
### New in 0.7.10
|
|
8
|
+
|
|
9
|
+
- State-backed collection wrappers, keyed rows, and reducer specializations accept prop spreads from direct inline object literals or one direct `const` object literal declared in the calling component.
|
|
10
|
+
- Spread and explicit props apply in source order, preserving ordinary override behavior.
|
|
11
|
+
- Specialized components accept forwarded JSX children, including mixed text and element children, while still lowering to intrinsic DOM.
|
|
12
|
+
- Non-self-closing specialized component tags are counted as one JSX use instead of treating the closing tag as another reference.
|
|
13
|
+
- A conventional React-shaped landing fixture proves imported layout children, component spreads, keyed row children, interaction output, and a zero-JavaScript static route.
|
|
14
|
+
- Dynamic, computed, circular, prototype-sensitive, and method/accessor spread shapes fail with source-located diagnostics.
|
|
15
|
+
|
|
16
|
+
### Boundary
|
|
17
|
+
|
|
18
|
+
Spread sources must be inline object literals or one direct `const` object literal declared in the calling component. State collections still cross the specialization boundary as direct props. Rest bindings, non-primitive defaults, exported reusable specialized rows, `forwardRef`, and `useId` remain fixture-driven work.
|
|
19
|
+
|
|
20
|
+
### Upgrade
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install @kudzujs/core@^0.7.10
|
|
24
|
+
```
|
|
25
|
+
|
|
3
26
|
## 0.7.9 - Keyed-row prop defaults
|
|
4
27
|
|
|
5
28
|
Kudzu 0.7.9 lets ordinary keyed row components retain destructured primitive literal prop defaults instead of restructuring every call site.
|
package/framework/README.md
CHANGED
|
@@ -33,7 +33,7 @@ Inline SVG rendering normalizes an explicit set of common React presentation ali
|
|
|
33
33
|
|
|
34
34
|
Same-file and relative-imported component chains receiving a direct local-state array or keyed item are recursively specialized to intrinsic JSX before keyed-list analysis, so their component functions are not retained in the browser. Missing destructured string, finite-number, boolean, or `null` props use their literal defaults during specialization. Rows may own multiple direct-property child maps recursively, nested conditions, latest-item handlers, multiple directly serializable state slots, effects, and `null`-initialized object refs. Structural list sites and ancestor key paths scope hooks across updates and reorder and release them on removal. Handler modules are emitted only when a rendered descriptor references them. Direct JSON-safe primitive keyed-item dependencies subscribe each row record to its owning list commit and compare selected fields after `list-runtime.js` synchronously refreshes the row marker. Only changed rows rerun with the complete latest item; reorder compares equal and unrelated fields do nothing. Builds without item dependencies emit no item reader or list-state subscription code.
|
|
35
35
|
|
|
36
|
-
Rendered collection selectors compile one-use aliases and inline `(item)` or `(item, index)` pipelines over local array state or supported static named imports. Supported selectors are pure `filter` with direct local-state reads, direct-property `flatMap`, and `Array.from` before a final keyed `map`; dependency commits re-evaluate the selector against the immutable build-time collection while field keys retain item identity and `key={index}` retains positional identity. Compiler-owned static filters over structural keyed rows validate source references and keys once, retain removed rows as detached prototypes, clone fresh restoration nodes, and insert only new runs without moving retained DOM. This route-specific path is compiled out elsewhere. Compiler-owned collection state is excluded from development snapshot restoration.
|
|
36
|
+
Rendered collection selectors compile one-use aliases and inline `(item)` or `(item, index)` pipelines over local array state or supported static named imports. Supported selectors are pure `filter` with direct local-state reads, direct-property `flatMap`, and `Array.from` before a final keyed `map`; dependency commits re-evaluate the selector against the immutable build-time collection while field keys retain item identity and `key={index}` retains positional identity. Compiler-owned static filters over structural keyed rows validate source references and keys once, retain removed rows as detached prototypes, clone fresh restoration nodes, and insert only new runs without moving retained DOM. Specialized collection wrappers and keyed rows inline direct object-literal or calling-component `const` object prop spreads in source order and forward JSX children into intrinsic output. This route-specific path is compiled out elsewhere. Compiler-owned collection state is excluded from development snapshot restoration. Dynamic/computed prop spreads, arbitrary callbacks, mutation, asynchronous selectors, imported callback functions, prototype-sensitive reads, lazy/dynamic row state initializers, non-`null` or callback refs, and recursive component cycles fail during compilation.
|
|
37
37
|
|
|
38
38
|
The reduced `useReducer` form reuses ordinary state slots and React's pure reducer contract. A direct dispatch in a compiled handler becomes a functional `set` whose reducer is bundled from a relative TypeScript module into that handler graph. Pure reducer-owned keyed lists reuse unchanged item identities for reorder, one removal, and append fast paths; ordinary `useState` lists retain full validation. One direct dispatch prop into a same-file or relative-imported synchronous component, including a direct keyed row, is specialized to intrinsic JSX at the call site, so its handler retains the parent reducer scope and no dispatch capture or child handler asset is emitted. A reducer row reads the latest item through the existing list scope and uses the same multiple serializable state, effect, condition, and object-ref specialization as other keyed rows. Relative TypeScript imports referenced inside that child handler receive collision-free call-site aliases and join the parent handler graph. One nested relative-imported intrinsic child may receive an inline or simple `const` callback containing dispatch; the compiler recursively substitutes that callback once and omits the nested child handler asset. Missing primitive literal defaults in these reducer specializations are substituted at the same call site. Reducer-free routes and shared runtimes are unchanged; no reducer runtime or browser component instance exists.
|
|
39
39
|
|
package/framework/build.mjs
CHANGED
|
@@ -3358,17 +3358,113 @@ function validateKeyedList(parts, sourceFile, listValues, listEventItems, listCo
|
|
|
3358
3358
|
visit(root)
|
|
3359
3359
|
}
|
|
3360
3360
|
|
|
3361
|
+
function directConstObjectLiteral(expression, call, sourceFile) {
|
|
3362
|
+
expression = unwrapExpression(expression)
|
|
3363
|
+
if (ts.isObjectLiteralExpression(expression)) return expression
|
|
3364
|
+
if (!ts.isIdentifier(expression)) return
|
|
3365
|
+
const owner = nearestFunction(call)
|
|
3366
|
+
const scope = owner?.body ?? sourceFile
|
|
3367
|
+
if (!scope || !ts.isBlock(scope) && !ts.isSourceFile(scope)) return
|
|
3368
|
+
const declarations = []
|
|
3369
|
+
for (const statement of scope.statements) {
|
|
3370
|
+
if (!ts.isVariableStatement(statement) || (statement.declarationList.flags & ts.NodeFlags.Const) === 0) continue
|
|
3371
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
3372
|
+
if (ts.isIdentifier(declaration.name) && declaration.name.text === expression.text && declaration.initializer && declaration.end < call.pos) declarations.push(declaration)
|
|
3373
|
+
}
|
|
3374
|
+
}
|
|
3375
|
+
if (declarations.length !== 1) return
|
|
3376
|
+
const initializer = unwrapExpression(declarations[0].initializer)
|
|
3377
|
+
if (ts.isObjectLiteralExpression(initializer)) return initializer
|
|
3378
|
+
}
|
|
3379
|
+
|
|
3380
|
+
function specializedSpreadEntries(expression, call, sourceFile, fail, label, seen = new Set()) {
|
|
3381
|
+
const object = directConstObjectLiteral(expression, call, sourceFile)
|
|
3382
|
+
if (!object) fail(expression, `${label} component prop spreads must use an inline object literal or one direct const object literal declared in the calling component`)
|
|
3383
|
+
if (seen.has(object)) fail(expression, `${label} component prop spreads cannot be circular`)
|
|
3384
|
+
seen.add(object)
|
|
3385
|
+
const entries = []
|
|
3386
|
+
for (const property of object.properties) {
|
|
3387
|
+
if (ts.isSpreadAssignment(property)) {
|
|
3388
|
+
entries.push(...specializedSpreadEntries(property.expression, call, sourceFile, fail, label, seen))
|
|
3389
|
+
continue
|
|
3390
|
+
}
|
|
3391
|
+
if (ts.isShorthandPropertyAssignment(property)) {
|
|
3392
|
+
entries.push([property.name.text, property.name, property])
|
|
3393
|
+
continue
|
|
3394
|
+
}
|
|
3395
|
+
if (!ts.isPropertyAssignment(property) || ts.isComputedPropertyName(property.name) || !ts.isIdentifier(property.name) && !ts.isStringLiteral(property.name) && !ts.isNumericLiteral(property.name)) {
|
|
3396
|
+
fail(property, `${label} component prop spreads must contain only direct properties`)
|
|
3397
|
+
}
|
|
3398
|
+
entries.push([property.name.text, property.initializer, property])
|
|
3399
|
+
}
|
|
3400
|
+
seen.delete(object)
|
|
3401
|
+
return entries
|
|
3402
|
+
}
|
|
3403
|
+
|
|
3404
|
+
function specializedCallChildren(call, factory) {
|
|
3405
|
+
if (!ts.isJsxElement(call)) return []
|
|
3406
|
+
return call.children.flatMap(child => {
|
|
3407
|
+
if (ts.isJsxText(child)) {
|
|
3408
|
+
const lines = child.text.split(/\r\n|\n|\r/)
|
|
3409
|
+
const text = lines.length === 1
|
|
3410
|
+
? child.text
|
|
3411
|
+
: lines.map((line, index) => {
|
|
3412
|
+
let text = line.replace(/\t/g, " ")
|
|
3413
|
+
if (index) text = text.trimStart()
|
|
3414
|
+
if (index < lines.length - 1) text = text.trimEnd()
|
|
3415
|
+
return text
|
|
3416
|
+
}).filter(Boolean).join(" ")
|
|
3417
|
+
return text ? [factory.createStringLiteral(text)] : []
|
|
3418
|
+
}
|
|
3419
|
+
if (ts.isJsxExpression(child)) return child.expression ? [child.expression] : []
|
|
3420
|
+
return [child]
|
|
3421
|
+
})
|
|
3422
|
+
}
|
|
3423
|
+
|
|
3424
|
+
function flattenForwardedComponentChildren(root, factory, context) {
|
|
3425
|
+
const forwarded = expression => {
|
|
3426
|
+
const value = unwrapExpression(expression)
|
|
3427
|
+
if (ts.isJsxElement(value) || ts.isJsxSelfClosingElement(value)) return [value]
|
|
3428
|
+
if (ts.isJsxFragment(value)) return [...value.children]
|
|
3429
|
+
if (ts.isArrayLiteralExpression(value) && !value.elements.some(ts.isSpreadElement)) {
|
|
3430
|
+
return value.elements.flatMap(element => {
|
|
3431
|
+
if (ts.isJsxFragment(element)) return [...element.children]
|
|
3432
|
+
if (ts.isJsxElement(element) || ts.isJsxSelfClosingElement(element)) return [element]
|
|
3433
|
+
return [factory.createJsxExpression(undefined, element)]
|
|
3434
|
+
})
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3437
|
+
const visit = node => {
|
|
3438
|
+
if (ts.isJsxElement(node)) {
|
|
3439
|
+
const children = node.children.flatMap(child => {
|
|
3440
|
+
const values = ts.isJsxExpression(child) && child.expression ? forwarded(child.expression) : undefined
|
|
3441
|
+
return (values ?? [child]).map(entry => ts.visitNode(entry, visit))
|
|
3442
|
+
})
|
|
3443
|
+
return factory.updateJsxElement(node, ts.visitNode(node.openingElement, visit), children, ts.visitNode(node.closingElement, visit))
|
|
3444
|
+
}
|
|
3445
|
+
return ts.visitEachChild(node, visit, context)
|
|
3446
|
+
}
|
|
3447
|
+
return ts.visitNode(root, visit)
|
|
3448
|
+
}
|
|
3449
|
+
|
|
3361
3450
|
function specializeComponentCall(call, component, sourceFile, factory, context, fail, label = "Keyed list", allowComponentRoot = false) {
|
|
3362
3451
|
if (component.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword) || component.asteriskToken) fail(component, `${label} components must be synchronous`)
|
|
3363
3452
|
if (component.parameters.length !== 1 || !ts.isObjectBindingPattern(component.parameters[0].name)) fail(component, `${label} components must use one destructured props parameter`)
|
|
3364
|
-
if (ts.isJsxElement(call) && call.children.some(child => !ts.isJsxText(child) || child.text.trim())) fail(call, `${label} component children are not supported`)
|
|
3365
3453
|
const callAttributes = ts.isJsxElement(call) ? call.openingElement.attributes : call.attributes
|
|
3366
3454
|
const props = new Map()
|
|
3455
|
+
const directProps = new Set()
|
|
3367
3456
|
let key
|
|
3368
3457
|
for (const attribute of callAttributes.properties) {
|
|
3369
|
-
if (ts.isJsxSpreadAttribute(attribute))
|
|
3458
|
+
if (ts.isJsxSpreadAttribute(attribute)) {
|
|
3459
|
+
for (const [name, value, property] of specializedSpreadEntries(attribute.expression, call, sourceFile, fail, label)) {
|
|
3460
|
+
if (["__proto__", "constructor", "prototype"].includes(name)) fail(property, `${label} component prop spread property ${JSON.stringify(name)} is not supported`)
|
|
3461
|
+
if (name === "key") fail(property, `${label} component prop spreads cannot declare key`)
|
|
3462
|
+
props.set(name, value)
|
|
3463
|
+
}
|
|
3464
|
+
continue
|
|
3465
|
+
}
|
|
3370
3466
|
const name = attribute.name.text
|
|
3371
|
-
if (
|
|
3467
|
+
if (directProps.has(name) || name === "key" && key) fail(attribute, `Duplicate ${label.toLowerCase()} component prop "${name}"`)
|
|
3372
3468
|
const value = !attribute.initializer
|
|
3373
3469
|
? factory.createTrue()
|
|
3374
3470
|
: ts.isStringLiteral(attribute.initializer)
|
|
@@ -3377,7 +3473,15 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
|
|
|
3377
3473
|
? attribute.initializer.expression
|
|
3378
3474
|
: factory.createIdentifier("undefined")
|
|
3379
3475
|
if (name === "key") key = attribute
|
|
3380
|
-
else
|
|
3476
|
+
else {
|
|
3477
|
+
props.set(name, value)
|
|
3478
|
+
directProps.add(name)
|
|
3479
|
+
}
|
|
3480
|
+
}
|
|
3481
|
+
const children = specializedCallChildren(call, factory)
|
|
3482
|
+
if (children.length) {
|
|
3483
|
+
if (directProps.has("children")) fail(call, `Duplicate ${label.toLowerCase()} component prop "children"`)
|
|
3484
|
+
props.set("children", children.length === 1 ? children[0] : factory.createArrayLiteralExpression(children))
|
|
3381
3485
|
}
|
|
3382
3486
|
const substitutions = new Map()
|
|
3383
3487
|
const acceptedProps = new Set()
|
|
@@ -3454,7 +3558,7 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
|
|
|
3454
3558
|
findUnsupportedHook(returned)
|
|
3455
3559
|
for (const calculation of calculations) findUnsupportedHook(calculation.expression)
|
|
3456
3560
|
if (unsupportedHook) throw sourceNodeError(unsupportedHook, component.getSourceFile(), `Keyed row ${unsupportedHook.expression.text}() must be one top-level const declaration`)
|
|
3457
|
-
let root = unwrapExpression(substituteClone(returned, substitutions, factory, context))
|
|
3561
|
+
let root = unwrapExpression(flattenForwardedComponentChildren(substituteClone(returned, substitutions, factory, context), factory, context))
|
|
3458
3562
|
if (!ts.isJsxElement(root) && !ts.isJsxSelfClosingElement(root)) fail(returned, `${label} component must return one JSX element`)
|
|
3459
3563
|
const tag = jsxTagName(root)
|
|
3460
3564
|
if (!ts.isIdentifier(tag) || !allowComponentRoot && tag.text[0] !== tag.text[0].toLowerCase()) fail(returned, `${label} component must directly return an intrinsic JSX element`)
|
|
@@ -3663,7 +3767,7 @@ function validateListExpression(expression, item, source, fail, index) {
|
|
|
3663
3767
|
fail(source, "Derived keyed list item expressions cannot call arbitrary functions")
|
|
3664
3768
|
}
|
|
3665
3769
|
}
|
|
3666
|
-
if (ts.isIdentifier(node) && isReferenceIdentifier(node) && node.text !== item && node.text !== index && !pureListGlobals.has(node.text)) {
|
|
3770
|
+
if (ts.isIdentifier(node) && isReferenceIdentifier(node) && !isJsxSyntaxIdentifier(node) && node.text !== item && node.text !== index && !pureListGlobals.has(node.text)) {
|
|
3667
3771
|
fail(source, `Derived keyed list item expression identifier "${node.text}" is not allowed`)
|
|
3668
3772
|
}
|
|
3669
3773
|
ts.forEachChild(node, visit)
|
|
@@ -3731,7 +3835,7 @@ function referencesIdentifier(root, name) {
|
|
|
3731
3835
|
function identifierReferenceCount(root, name) {
|
|
3732
3836
|
let count = 0
|
|
3733
3837
|
const visit = node => {
|
|
3734
|
-
if (ts.isIdentifier(node) && node.text === name && isReferenceIdentifier(node)) count++
|
|
3838
|
+
if (ts.isIdentifier(node) && node.text === name && isReferenceIdentifier(node) && !ts.isJsxClosingElement(node.parent)) count++
|
|
3735
3839
|
ts.forEachChild(node, visit)
|
|
3736
3840
|
}
|
|
3737
3841
|
visit(root)
|