@kudzujs/core 0.7.18 → 0.7.20
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 +2 -2
- package/RELEASES.md +48 -0
- package/framework/README.md +4 -2
- package/framework/build.mjs +33 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Kudzu compiles ordinary React-shaped TypeScript and TSX into complete static HTM
|
|
|
10
10
|
|
|
11
11
|
> Experimental `0.7.x`: the compiler API and supported TSX surface may change.
|
|
12
12
|
|
|
13
|
-
**Latest release: 0.7.
|
|
13
|
+
**Latest release: 0.7.20 - Computed child collections.** A block-bodied keyed `map` callback may compute one direct child collection through the supported pure selector pipeline and use it as a nested keyed list source. Read the [release notes](./RELEASES.md#0720---computed-child-collections) or open the [release page](https://kudzujs.cloud/releases/0.7.20).
|
|
14
14
|
|
|
15
15
|
- [Documentation](https://kudzujs.cloud/docs)
|
|
16
16
|
- [Installation guide](https://kudzujs.cloud/docs#install)
|
|
@@ -79,7 +79,7 @@ ordinary React-shaped TSX
|
|
|
79
79
|
|
|
80
80
|
- Function components execute at build time and do not survive as browser components.
|
|
81
81
|
- `useState` and reduced `useReducer`, including directly serializable lazy initialization, compile to synchronous logical state and batched direct DOM writes; top-level `useId` and direct intrinsic `forwardRef` authoring erase to static HTML without a component runtime.
|
|
82
|
-
- Conditions, keyed collections, attributes, events, refs, effects, and supported component boundaries compile to route-specific capabilities.
|
|
82
|
+
- Conditions, keyed collections, attributes, events, refs, effects, and supported component boundaries compile to route-specific capabilities. Inline or simple `const` setter callbacks and object refs may cross one ordinary component boundary into a direct intrinsic root.
|
|
83
83
|
- Build-known data and routes become complete HTML through async components and `getStaticPaths()`.
|
|
84
84
|
- Native document navigation is the default; static routes do not load a client runtime.
|
|
85
85
|
- Unsupported nearby patterns fail during the build with a source location and actionable boundary.
|
package/RELEASES.md
CHANGED
|
@@ -1,5 +1,53 @@
|
|
|
1
1
|
# Kudzu Releases
|
|
2
2
|
|
|
3
|
+
## 0.7.20 - Computed child collections
|
|
4
|
+
|
|
5
|
+
Kudzu 0.7.20 accepts a common block-bodied keyed `map` callback that computes one direct child collection before returning JSX, without executing arbitrary callback code or adding a browser runtime path.
|
|
6
|
+
|
|
7
|
+
### New in 0.7.20
|
|
8
|
+
|
|
9
|
+
- A keyed `map` callback may contain one top-level `const` declaration followed by its final JSX return.
|
|
10
|
+
- The `const` must start from a direct parent-item child property and may use the existing pure `filter`, direct-property `flatMap`, or `Array.from` selector pipeline.
|
|
11
|
+
- The computed alias feeds exactly one nested keyed list source.
|
|
12
|
+
- The compiler substitutes the proven calculation into the returned JSX before nested-list analysis, then reuses existing selector encoding and keyed ownership.
|
|
13
|
+
- Child insertion, reorder, removal, indexes, and DOM identity retain the existing nested-list behavior.
|
|
14
|
+
- Additional statements, multiple or mixed alias reads, parent capture, mutation, arbitrary calls, and asynchronous callbacks fail during compilation.
|
|
15
|
+
- The complete suite passes 107/107 tests with browser coverage for dynamic computed-child insertion and source-diagnostic coverage for mixed alias use.
|
|
16
|
+
|
|
17
|
+
### Boundary
|
|
18
|
+
|
|
19
|
+
This release supports one direct-child collection `const` in a block-bodied keyed-map callback. Multiple calculations, aliases used outside one nested keyed list, imported transforms, parent captures, mutation, and async work remain unsupported.
|
|
20
|
+
|
|
21
|
+
### Upgrade
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install @kudzujs/core@^0.7.20
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## 0.7.19 - Reusable collection aliases
|
|
28
|
+
|
|
29
|
+
Kudzu 0.7.19 lets one immutable local collection alias feed multiple keyed list sites while preserving independent DOM identity, and formalizes setter callback and object-ref ownership across one ordinary component boundary.
|
|
30
|
+
|
|
31
|
+
### New in 0.7.19
|
|
32
|
+
|
|
33
|
+
- A top-level immutable local alias over a supported collection pipeline may be reused by multiple keyed `map` sites.
|
|
34
|
+
- Every alias reference must remain a statically analyzable collection source; mixed reads such as `visible.length` fail with a source-located diagnostic instead of becoming stale build-time values.
|
|
35
|
+
- Each list site retains its own keyed DOM identity through insertion, reorder, and removal while sharing the same state-backed selector.
|
|
36
|
+
- Inline or simple `const` setter callbacks may cross one same-file or relative-imported component boundary to an event on the child's direct intrinsic root.
|
|
37
|
+
- A `null`-initialized object ref may cross the same boundary; conditional removal resolves it to `null`, and remount points it at a fresh element.
|
|
38
|
+
- These paths reuse compiler-owned behavior and ref descriptors without a callback registry, retained component instance, VDOM, or hydration.
|
|
39
|
+
- The complete suite passes 107/107 tests with browser coverage for both reusable list identity and callback/ref conditional ownership.
|
|
40
|
+
|
|
41
|
+
### Boundary
|
|
42
|
+
|
|
43
|
+
Reusable aliases must be top-level immutable locals whose every reference is a supported collection source. Search, slice, pagination, sorting, mutation, imported transforms, arbitrary callbacks, callback refs, and forwarding beyond the proven component boundary remain unsupported.
|
|
44
|
+
|
|
45
|
+
### Upgrade
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm install @kudzujs/core@^0.7.19
|
|
49
|
+
```
|
|
50
|
+
|
|
3
51
|
## 0.7.18 - Primitive child prop dependencies
|
|
4
52
|
|
|
5
53
|
Kudzu 0.7.18 formalizes direct JSON-safe primitive parent state as a reactive child prop across ordinary same-file and relative-imported components, without adding component specialization or a browser component tree.
|
package/framework/README.md
CHANGED
|
@@ -33,12 +33,14 @@ Page `metadata` can emit description, canonical, favicon, manifest, Open Graph,
|
|
|
33
33
|
|
|
34
34
|
Inline SVG rendering normalizes an explicit set of common React presentation aliases before static serialization and binding descriptor creation. Reactive aliases therefore use the existing generic `setAttribute` path; static SVG adds no JavaScript and reactive SVG adds no SVG-specific runtime.
|
|
35
35
|
|
|
36
|
-
Same-file, directly exported 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. A directly exported row may be reused across static and keyed JSX sites; export-list/default aliases and non-JSX references remain rejected. Missing destructured props use directly serializable primitive, plain-object, or array literal defaults during specialization. One final identifier rest binding may be expanded exactly once at the direct intrinsic root. 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.
|
|
36
|
+
Same-file, directly exported 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. A directly exported row may be reused across static and keyed JSX sites; export-list/default aliases and non-JSX references remain rejected. Missing destructured props use directly serializable primitive, plain-object, or array literal defaults during specialization. One final identifier rest binding may be expanded exactly once at the direct intrinsic root. 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. A block-bodied keyed `map` callback may declare one top-level `const` computed from a direct child collection through the supported pure selector pipeline and then return JSX; the alias must feed exactly one nested keyed list source. 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.
|
|
37
37
|
|
|
38
|
-
Rendered collection selectors compile
|
|
38
|
+
Rendered collection selectors compile immutable local aliases and inline `(item)` or `(item, index)` pipelines over local array state or supported static named imports. One alias may feed multiple keyed list sites when every reference is a statically analyzable collection source; mixed non-collection reads fail during compilation. 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. Anonymous zero-argument lazy state initializers that return a directly serializable literal lower to the same ownership path. 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, dynamic row state initializers, non-`null` or callback refs, and recursive component cycles fail during compilation.
|
|
39
39
|
|
|
40
40
|
The reduced `useReducer` form reuses ordinary state slots and React's pure reducer contract. An optional inline, same-file, or relative-imported synchronous one-parameter initializer may derive a directly serializable literal only from its directly serializable initial argument; the compiler substitutes that argument and lowers the call to the ordinary two-argument ownership path. 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 directly serializable literal defaults and direct intrinsic rest props 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.
|
|
41
41
|
|
|
42
|
+
An inline or simple `const` setter callback may cross one same-file or relative-imported component boundary when the child forwards it to an event on its direct intrinsic root. A `null`-initialized object ref may cross the same boundary to that root. Both remain compiler-owned descriptors: conditional removal drops the handler with the element and makes the ref resolve to `null`, while remount creates a fresh element without retaining a component instance.
|
|
43
|
+
|
|
42
44
|
`kudzu.config` may opt one emitted shared-layout group into same-document navigation with legacy `navigation: { routes: ["/product", "/items/[id]"] }`, or multiple groups with `navigation: { groups: [{ routes: [...] }, { routes: [...] }] }`. The forms are mutually exclusive. Identities are globally unique emitted exact paths or `runtimeParams` patterns; each group uses one page-exported layout function identity. Runtime records securely match concrete pathnames under `base`, and their cache-safe parameter initializer runs before route DOM/effects mount on every transition. Each group receives a deterministic route-hashed asset specialized to only its records, pattern decoder, and effect/parameter lifecycle needs. Cross-group and ungrouped anchors remain native and are not prefetched; overlapping path domains across groups fail the build. Route effect entries export cache-safe layout and route mount functions: layout effects, including conditional/keyed DOM-owned effects, persist for the group session; route effects receive a fresh owner registry after each route insertion; and non-persisted page disposal cleans route before layout. Direct primitive state, runtime parameter, and keyed-item property dependencies and cleanup are supported. Fragment payloads and coordinated View Transitions are not implemented.
|
|
43
45
|
|
|
44
46
|
Cross-framework performance tables are historical snapshots from an excluded local benchmark workspace; the current checkout does not contain their competitor fixtures, runners, or raw arrays. The maintained provenance warning and tables live in the web docs. `npm run benchmark` is the tracked reproducible Worker fixture measurement, while the focused Chrome Worker test verifies throughput, cadence, bounded history, stale-write isolation, and route lifecycle behavior.
|
package/framework/build.mjs
CHANGED
|
@@ -2446,7 +2446,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2446
2446
|
const setters = settersByFunction.get(owner) ?? new Map()
|
|
2447
2447
|
for (const [name, entries] of declarations) {
|
|
2448
2448
|
for (const declaration of entries) {
|
|
2449
|
-
const parts = keyedListParts(declaration.initializer, setters, declarations, (target, message) => { throw sourceNodeError(target, sourceFile, message) }, new Set(), importedCollections)
|
|
2449
|
+
const parts = keyedListParts(declaration.initializer, setters, declarations, (target, message) => { throw sourceNodeError(target, sourceFile, message) }, new Set(), importedCollections, factory, context)
|
|
2450
2450
|
if (!parts) continue
|
|
2451
2451
|
const uses = []
|
|
2452
2452
|
const collectUses = node => {
|
|
@@ -2640,7 +2640,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2640
2640
|
return
|
|
2641
2641
|
}
|
|
2642
2642
|
if (ts.isJsxExpression(node) && node.initializer === undefined && node.expression && (ts.isJsxElement(node.parent) || ts.isJsxFragment(node.parent))) {
|
|
2643
|
-
const parts = listLocalUses.get(node) ?? keyedListParts(node.expression, settersForNode(node, settersByFunction), jsxLocalDeclarations.get(nearestFunction(node)), fail, new Set(), importedCollections)
|
|
2643
|
+
const parts = listLocalUses.get(node) ?? keyedListParts(node.expression, settersForNode(node, settersByFunction), jsxLocalDeclarations.get(nearestFunction(node)), fail, new Set(), importedCollections, factory, context)
|
|
2644
2644
|
if (parts) {
|
|
2645
2645
|
for (const declaration of parts.aliasDeclarations ?? []) listLocalDeclarations.add(declaration)
|
|
2646
2646
|
rawRenderedLists.push({ node, parts })
|
|
@@ -2649,6 +2649,13 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2649
2649
|
ts.forEachChild(node, collectRenderedLists)
|
|
2650
2650
|
}
|
|
2651
2651
|
collectRenderedLists(sourceFile)
|
|
2652
|
+
const collectionAliasUses = new WeakSet(rawRenderedLists.flatMap(({ parts }) => parts.aliasUses ?? []))
|
|
2653
|
+
const collectionAliasDeclarations = new Set(rawRenderedLists.flatMap(({ parts }) => parts.aliasDeclarations ?? []))
|
|
2654
|
+
for (const declaration of collectionAliasDeclarations) {
|
|
2655
|
+
const owner = nearestFunction(declaration)
|
|
2656
|
+
const unsupported = identifierReferences(owner.body, declaration.name.text).find(reference => !collectionAliasUses.has(reference))
|
|
2657
|
+
if (unsupported) fail(unsupported, `Rendered collection alias "${declaration.name.text}" may only be used as a rendered collection source`)
|
|
2658
|
+
}
|
|
2652
2659
|
const rejectUnsupportedRenderControl = node => {
|
|
2653
2660
|
if (ts.isIfStatement(node) && containsRenderControl(node, jsxLocalsByFunction.get(nearestFunction(node)) ?? new Set())) {
|
|
2654
2661
|
const setters = settersForNode(node, settersByFunction)
|
|
@@ -3150,16 +3157,29 @@ function containsRenderControl(root, knownLocals) {
|
|
|
3150
3157
|
return found
|
|
3151
3158
|
}
|
|
3152
3159
|
|
|
3153
|
-
function keyedListParts(expression, setters, declarations, fail, aliases = new Set(), importedCollections = new Set()) {
|
|
3160
|
+
function keyedListParts(expression, setters, declarations, fail, aliases = new Set(), importedCollections = new Set(), factory = ts.factory, context) {
|
|
3154
3161
|
const value = unwrapExpression(expression)
|
|
3155
3162
|
const directFrom = isArrayFromCall(value) && value.arguments.length === 2 && containsJsx(value.arguments[1])
|
|
3156
3163
|
if (!directFrom && (!ts.isCallExpression(value) || value.arguments.length !== 1 || !ts.isPropertyAccessExpression(value.expression) || value.expression.name.text !== "map")) return undefined
|
|
3157
3164
|
const collection = renderedCollectionSource(directFrom ? value.arguments[0] : value.expression.expression, setters, declarations, fail, aliases, importedCollections, new Set(setters.values()))
|
|
3158
3165
|
if (!collection?.state) return undefined
|
|
3159
3166
|
if (directFrom) collection.selector.push(["from", undefined])
|
|
3160
|
-
|
|
3167
|
+
let callback = directFrom ? value.arguments[1] : value.arguments[0]
|
|
3161
3168
|
const parameters = collectionParameters(callback, "Keyed list map", fail)
|
|
3162
|
-
|
|
3169
|
+
let root = unwrapExpression(callback.body)
|
|
3170
|
+
if (ts.isBlock(root)) {
|
|
3171
|
+
if (!context || root.statements.length !== 2 || !ts.isVariableStatement(root.statements[0]) || (root.statements[0].declarationList.flags & ts.NodeFlags.Const) === 0 || root.statements[0].declarationList.declarations.length !== 1 || !ts.isReturnStatement(root.statements[1]) || !root.statements[1].expression) fail(root, "Block-bodied keyed list map callbacks require one computed child collection const and a final JSX return")
|
|
3172
|
+
const declaration = root.statements[0].declarationList.declarations[0]
|
|
3173
|
+
if (!ts.isIdentifier(declaration.name) || !declaration.initializer) fail(declaration, "Computed child collections must initialize one const identifier")
|
|
3174
|
+
const computed = renderedCollectionSource(declaration.initializer, new Map(), undefined, fail, new Set())
|
|
3175
|
+
if (!computed?.ownerField || computed.parentItem !== parameters.item) fail(declaration.initializer, `Computed child collections must start from ${parameters.item}.<field>`)
|
|
3176
|
+
const returned = root.statements[1].expression
|
|
3177
|
+
if (identifierReferenceCount(returned, declaration.name.text) !== 1) fail(declaration.name, `Computed child collection alias "${declaration.name.text}" must be used exactly once`)
|
|
3178
|
+
root = unwrapExpression(substituteClone(returned, new Map([[declaration.name.text, declaration.initializer]]), factory, context))
|
|
3179
|
+
callback = factory.updateArrowFunction(callback, callback.modifiers, callback.typeParameters, callback.parameters, callback.type, callback.equalsGreaterThanToken, root)
|
|
3180
|
+
ts.setParentRecursive(callback, false)
|
|
3181
|
+
callback.parent = value
|
|
3182
|
+
}
|
|
3163
3183
|
if (!ts.isJsxElement(root) && !ts.isJsxSelfClosingElement(root)) fail(callback.body, "Keyed list map callback must return one JSX element")
|
|
3164
3184
|
const attributes = ts.isJsxElement(root) ? root.openingElement.attributes : root.attributes
|
|
3165
3185
|
const key = attributes.properties.find(attribute => ts.isJsxAttribute(attribute) && ts.isIdentifier(attribute.name) && attribute.name.text === "key")
|
|
@@ -3196,11 +3216,10 @@ function renderedCollectionSource(expression, setters, declarations, fail, alias
|
|
|
3196
3216
|
const entries = declarations?.get(value.text)
|
|
3197
3217
|
if (!entries) return undefined
|
|
3198
3218
|
if (entries.length !== 1 || aliases.has(value.text) || entries[0].node.parent?.parent?.parent !== nearestFunction(entries[0].node)?.body) fail(value, `Rendered collection alias "${value.text}" must be one top-level immutable local`)
|
|
3199
|
-
if (identifierReferenceCount(nearestFunction(entries[0].node).body, value.text) !== 1) fail(value, `Rendered collection alias "${value.text}" may only be rendered once`)
|
|
3200
3219
|
aliases.add(value.text)
|
|
3201
3220
|
const source = renderedCollectionSource(entries[0].initializer, setters, declarations, fail, aliases, importedCollections, stateNames)
|
|
3202
3221
|
aliases.delete(value.text)
|
|
3203
|
-
return source && { ...source, aliasDeclarations: [...(source.aliasDeclarations ?? []), entries[0].node] }
|
|
3222
|
+
return source && { ...source, aliasDeclarations: [...(source.aliasDeclarations ?? []), entries[0].node], aliasUses: [...(source.aliasUses ?? []), value] }
|
|
3204
3223
|
}
|
|
3205
3224
|
if (ts.isPropertyAccessExpression(value) && ts.isIdentifier(value.expression)) return { state: undefined, ownerField: value.name.text, selector: [], parentItem: value.expression.text }
|
|
3206
3225
|
if (ts.isCallExpression(value) && ts.isPropertyAccessExpression(value.expression)) {
|
|
@@ -4023,13 +4042,17 @@ function referencesIdentifier(root, name) {
|
|
|
4023
4042
|
}
|
|
4024
4043
|
|
|
4025
4044
|
function identifierReferenceCount(root, name) {
|
|
4026
|
-
|
|
4045
|
+
return identifierReferences(root, name).length
|
|
4046
|
+
}
|
|
4047
|
+
|
|
4048
|
+
function identifierReferences(root, name) {
|
|
4049
|
+
const references = []
|
|
4027
4050
|
const visit = node => {
|
|
4028
|
-
if (ts.isIdentifier(node) && node.text === name && isReferenceIdentifier(node) && !ts.isJsxClosingElement(node.parent))
|
|
4051
|
+
if (ts.isIdentifier(node) && node.text === name && isReferenceIdentifier(node) && !ts.isJsxClosingElement(node.parent)) references.push(node)
|
|
4029
4052
|
ts.forEachChild(node, visit)
|
|
4030
4053
|
}
|
|
4031
4054
|
visit(root)
|
|
4032
|
-
return
|
|
4055
|
+
return references
|
|
4033
4056
|
}
|
|
4034
4057
|
|
|
4035
4058
|
function unwrapExpression(node) {
|