@kudzujs/core 0.7.16 → 0.7.18
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 +2 -2
- package/framework/build.mjs +42 -4
- package/framework/core.d.ts +1 -0
- 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.18 - Primitive child prop dependencies.** Direct JSON-safe primitive parent state remains reactive when passed to destructured same-file or imported child props, including child DOM bindings and conditionally owned effects. Read the [release notes](./RELEASES.md#0718---primitive-child-prop-dependencies) or open the [release page](https://kudzujs.cloud/releases/0.7.18).
|
|
14
14
|
|
|
15
15
|
- [Documentation](https://kudzujs.cloud/docs)
|
|
16
16
|
- [Installation guide](https://kudzujs.cloud/docs#install)
|
|
@@ -78,7 +78,7 @@ ordinary React-shaped TSX
|
|
|
78
78
|
```
|
|
79
79
|
|
|
80
80
|
- Function components execute at build time and do not survive as browser components.
|
|
81
|
-
- `useState` and reduced `useReducer
|
|
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
82
|
- Conditions, keyed collections, attributes, events, refs, effects, and supported component boundaries compile to route-specific capabilities.
|
|
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.
|
package/RELEASES.md
CHANGED
|
@@ -1,5 +1,53 @@
|
|
|
1
1
|
# Kudzu Releases
|
|
2
2
|
|
|
3
|
+
## 0.7.18 - Primitive child prop dependencies
|
|
4
|
+
|
|
5
|
+
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.
|
|
6
|
+
|
|
7
|
+
### New in 0.7.18
|
|
8
|
+
|
|
9
|
+
- A direct parent state identifier passed to a destructured child prop remains the same compiler-owned signal.
|
|
10
|
+
- Child text and attribute bindings update directly when the parent primitive changes.
|
|
11
|
+
- Child `useEffect` dependency arrays may reference that destructured prop and reuse the parent state ID.
|
|
12
|
+
- Repeated child calls share the parent signal while retaining independent effect records and captures.
|
|
13
|
+
- Conditional child insertion mounts its effect, parent updates run cleanup before setup, and removal cleans up exactly once.
|
|
14
|
+
- Same-file and relative-imported children receive matching behavior; React and component functions remain absent from browser output.
|
|
15
|
+
- The complete suite passes 106/106 tests with browser coverage for binding updates, dependency order, conditional mount, rerun, and removal.
|
|
16
|
+
|
|
17
|
+
### Boundary
|
|
18
|
+
|
|
19
|
+
The prop must be a direct JSON-safe primitive parent state identifier. Derived expressions, object or array dependencies, computed prop forwarding, and broader callback/ref ownership remain unsupported.
|
|
20
|
+
|
|
21
|
+
### Upgrade
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install @kudzujs/core@^0.7.18
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## 0.7.17 - Lazy reducer initialization
|
|
28
|
+
|
|
29
|
+
Kudzu 0.7.17 lowers React-shaped `useReducer(reducer, initialArg, initializer)` calls into existing compiler-owned reducer state when the initializer is statically analyzable, and fixes capability-specialized keyed-list fallback paths.
|
|
30
|
+
|
|
31
|
+
### New in 0.7.17
|
|
32
|
+
|
|
33
|
+
- A synchronous one-parameter initializer may be inline, same-file, or imported from a relative TypeScript module.
|
|
34
|
+
- The initial argument must be directly serializable, and the initializer result must be a primitive, plain-object, or array literal derived only from that argument.
|
|
35
|
+
- The compiler substitutes the argument, emits fresh literal AST, and lowers the call to the existing two-argument reducer ownership path.
|
|
36
|
+
- React migration imports and direct `@kudzujs/core` imports receive matching TypeScript overloads without adding a reducer runtime.
|
|
37
|
+
- Dynamic calls, captures, defaults, rest parameters, async or generator functions, and non-literal results fail with source-located diagnostics.
|
|
38
|
+
- Keyed-list builds now use the template fallback when static collection support is absent and preserve a zero index when index capability code is removed.
|
|
39
|
+
- The complete suite passes 105/105 tests, including browser coverage for ordinary, nested, reducer-owned, effect-owned, and conditional keyed lists.
|
|
40
|
+
|
|
41
|
+
### Boundary
|
|
42
|
+
|
|
43
|
+
Kudzu does not execute initializer functions during compilation. It only substitutes a directly serializable argument into a directly serializable return expression. Reducer factories, package initializers, arbitrary computation, and captured values remain unsupported.
|
|
44
|
+
|
|
45
|
+
### Upgrade
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm install @kudzujs/core@^0.7.17
|
|
49
|
+
```
|
|
50
|
+
|
|
3
51
|
## 0.7.16 - Build-time lazy state
|
|
4
52
|
|
|
5
53
|
Kudzu 0.7.16 accepts the common React-shaped `useState(() => initialValue)` form when the initializer directly returns a serializable literal, lowering it into existing compiler-owned state with no browser component runtime.
|
package/framework/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Migration source may retain conventional `react` imports for supported named or
|
|
|
6
6
|
|
|
7
7
|
Direct `clsx` calls over literal strings, numbers, arrays, object conditions, and conditional expressions are similarly lowered to ordinary concatenation and conditional expressions. The package import is erased, and dynamic classes continue through the existing binding compiler without serializing or shipping the `clsx` function.
|
|
8
8
|
|
|
9
|
-
Repeated ordinary same-file and relative-imported child components execute independently at build time, so each `useState` call receives a distinct concrete state ID while shared native handler modules retain per-element state maps and captures. Reactive conditional descriptors own state created by their direct branch: initial visible output reuses the rendered template IDs, removal deletes those slots, and remount recreates them from serialized initial values. Static sibling routes and branches without local state add no ownership metadata, component function, hook dispatcher, or rerender loop.
|
|
9
|
+
Repeated ordinary same-file and relative-imported child components execute independently at build time, so each `useState` call receives a distinct concrete state ID while shared native handler modules retain per-element state maps and captures. A direct JSON-safe primitive parent state passed to a destructured child prop remains the same signal for child DOM bindings and effect dependencies; repeated calls own independent effect records, and conditional removal cleans up before remount recreates the effect. Reactive conditional descriptors own state created by their direct branch: initial visible output reuses the rendered template IDs, removal deletes those slots, and remount recreates them from serialized initial values. Static sibling routes and branches without local state add no ownership metadata, component function, hook dispatcher, or rerender loop.
|
|
10
10
|
|
|
11
11
|
Reduced Zustand migration stores lower to one ordinary layout-lifetime state slot. The compiler accepts one exported `create(set => ({ data, ...actions }))` store with one serializable data property, direct property selectors, and synchronous capture-free actions using one-argument merge-form `set`; selected actions reuse the reducer-style functional update compiler, so same-turn calls observe current logical state and DOM writes still batch. The shared layout must initialize the store before route consumers, outside keyed rows. No Zustand import, store subscription runtime, React hook, or generic external-store capability is emitted.
|
|
12
12
|
|
|
@@ -37,7 +37,7 @@ Same-file, directly exported same-file, and relative-imported component chains r
|
|
|
37
37
|
|
|
38
38
|
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. 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
|
-
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 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.
|
|
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
42
|
`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
43
|
|
package/framework/build.mjs
CHANGED
|
@@ -2148,17 +2148,40 @@ function validateUseIdSyntax(sourceFile) {
|
|
|
2148
2148
|
visit(sourceFile)
|
|
2149
2149
|
}
|
|
2150
2150
|
|
|
2151
|
-
function normalizeLazyStateInitializers(sourceFile, factory, context) {
|
|
2151
|
+
function normalizeLazyStateInitializers(sourceFile, factory, context, file, sourceFiles, sourceIndex) {
|
|
2152
2152
|
const bindings = new Set()
|
|
2153
2153
|
for (const statement of sourceFile.statements) {
|
|
2154
2154
|
if (!ts.isImportDeclaration(statement) || statement.importClause?.isTypeOnly || !ts.isStringLiteral(statement.moduleSpecifier) || !["react", "@kudzujs/core"].includes(statement.moduleSpecifier.text)) continue
|
|
2155
2155
|
const named = statement.importClause?.namedBindings
|
|
2156
2156
|
if (named && ts.isNamedImports(named)) for (const entry of named.elements) {
|
|
2157
|
-
|
|
2157
|
+
const imported = (entry.propertyName ?? entry.name).text
|
|
2158
|
+
if (!entry.isTypeOnly && ["useReducer", "useState"].includes(imported) && entry.name.text === imported) bindings.add(imported)
|
|
2158
2159
|
}
|
|
2159
2160
|
}
|
|
2160
2161
|
if (!bindings.size) return sourceFile
|
|
2162
|
+
const imports = clientImportBindings(sourceFile, file, sourceFiles)
|
|
2161
2163
|
const visitor = node => {
|
|
2164
|
+
if (bindings.has("useReducer") && ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "useReducer" && !isShadowedIdentifier(node.expression, sourceFile) && node.arguments.length === 3) {
|
|
2165
|
+
const initialArg = node.arguments[1]
|
|
2166
|
+
const initializer = node.arguments[2]
|
|
2167
|
+
let declaration
|
|
2168
|
+
if (ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer)) declaration = initializer
|
|
2169
|
+
else if (ts.isIdentifier(initializer)) {
|
|
2170
|
+
declaration = localComponentDeclaration(sourceFile, initializer.text)
|
|
2171
|
+
const binding = imports.get(initializer.text)
|
|
2172
|
+
if (!declaration && binding && binding.kind !== "namespace") {
|
|
2173
|
+
try {
|
|
2174
|
+
declaration = resolveComponentExport(binding.target, binding.kind === "default" ? "default" : binding.imported, target => parseSourceFile(target, sourceIndex.get(target)), sourceFiles)
|
|
2175
|
+
} catch {}
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
if (!declaration || declaration.parameters.length !== 1 || !ts.isIdentifier(declaration.parameters[0].name) || declaration.parameters[0].initializer || declaration.parameters[0].dotDotDotToken || declaration.asteriskToken || declaration.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword) || ts.isFunctionExpression(initializer) && initializer.name) throw sourceNodeError(initializer, sourceFile, "Lazy useReducer() requires one inline, same-file, or relative-imported synchronous one-parameter initializer")
|
|
2179
|
+
if (!isSerializableStateLiteral(initialArg)) throw sourceNodeError(initialArg, sourceFile, "Lazy useReducer() initial argument must be directly serializable")
|
|
2180
|
+
const expression = reactMemoExpression(declaration)
|
|
2181
|
+
const lowered = expression && substituteClone(expression, new Map([[declaration.parameters[0].name.text, initialArg]]), factory, context)
|
|
2182
|
+
if (!lowered || !isSerializableStateLiteral(lowered)) throw sourceNodeError(initializer, sourceFile, "Lazy useReducer() initializer must directly return a serializable primitive, plain-object, or array literal derived only from its initial argument")
|
|
2183
|
+
return factory.updateCallExpression(node, node.expression, node.typeArguments, [node.arguments[0], synthesizeSerializableStateLiteral(lowered, factory)])
|
|
2184
|
+
}
|
|
2162
2185
|
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && bindings.has(node.expression.text) && !isShadowedIdentifier(node.expression, sourceFile) && node.arguments[0] && (ts.isArrowFunction(node.arguments[0]) || ts.isFunctionExpression(node.arguments[0]))) {
|
|
2163
2186
|
const initializer = node.arguments[0]
|
|
2164
2187
|
if (node.arguments.length !== 1 || initializer.parameters.length || initializer.asteriskToken || initializer.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword) || ts.isFunctionExpression(initializer) && initializer.name) throw sourceNodeError(initializer, sourceFile, "Lazy useState() requires one anonymous synchronous zero-parameter initializer")
|
|
@@ -2258,7 +2281,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2258
2281
|
sourceFile = normalizeReactMigrationSyntax(sourceFile, factory, context, importedCollections)
|
|
2259
2282
|
ts.setParentRecursive(sourceFile, false)
|
|
2260
2283
|
validateUseIdSyntax(sourceFile)
|
|
2261
|
-
sourceFile = normalizeLazyStateInitializers(sourceFile, factory, context)
|
|
2284
|
+
sourceFile = normalizeLazyStateInitializers(sourceFile, factory, context, file, sourceFiles, sourceIndex)
|
|
2262
2285
|
ts.setParentRecursive(sourceFile, false)
|
|
2263
2286
|
sourceFile = normalizeZustandMigrationSyntax(sourceFile, factory, context)
|
|
2264
2287
|
ts.setParentRecursive(sourceFile, false)
|
|
@@ -2276,7 +2299,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2276
2299
|
imported = normalizeReactMigrationSyntax(imported, factory, context, importedSerializableCollectionNames(imported, target, sourceFiles, sourceIndex))
|
|
2277
2300
|
ts.setParentRecursive(imported, false)
|
|
2278
2301
|
validateUseIdSyntax(imported)
|
|
2279
|
-
imported = normalizeLazyStateInitializers(imported, factory, context)
|
|
2302
|
+
imported = normalizeLazyStateInitializers(imported, factory, context, target, sourceFiles, sourceIndex)
|
|
2280
2303
|
ts.setParentRecursive(imported, false)
|
|
2281
2304
|
imported = normalizeZustandMigrationSyntax(imported, factory, context)
|
|
2282
2305
|
ts.setParentRecursive(imported, false)
|
|
@@ -3740,6 +3763,21 @@ function isSerializableStateLiteral(node) {
|
|
|
3740
3763
|
return value.properties.every(property => ts.isPropertyAssignment(property) && !ts.isComputedPropertyName(property.name) && property.name.text !== "__proto__" && isSerializableStateLiteral(property.initializer))
|
|
3741
3764
|
}
|
|
3742
3765
|
|
|
3766
|
+
function synthesizeSerializableStateLiteral(node, factory) {
|
|
3767
|
+
node = unwrapExpression(node)
|
|
3768
|
+
if (ts.isStringLiteral(node)) return factory.createStringLiteral(node.text)
|
|
3769
|
+
if (ts.isNumericLiteral(node)) return factory.createNumericLiteral(node.text)
|
|
3770
|
+
if (node.kind === ts.SyntaxKind.TrueKeyword) return factory.createTrue()
|
|
3771
|
+
if (node.kind === ts.SyntaxKind.FalseKeyword) return factory.createFalse()
|
|
3772
|
+
if (node.kind === ts.SyntaxKind.NullKeyword) return factory.createNull()
|
|
3773
|
+
if (ts.isPrefixUnaryExpression(node)) return factory.createPrefixUnaryExpression(node.operator, synthesizeSerializableStateLiteral(node.operand, factory))
|
|
3774
|
+
if (ts.isArrayLiteralExpression(node)) return factory.createArrayLiteralExpression(node.elements.map(element => synthesizeSerializableStateLiteral(element, factory)))
|
|
3775
|
+
return factory.createObjectLiteralExpression(node.properties.map(property => {
|
|
3776
|
+
const name = ts.isIdentifier(property.name) ? factory.createIdentifier(property.name.text) : ts.isNumericLiteral(property.name) ? factory.createNumericLiteral(property.name.text) : factory.createStringLiteral(property.name.text)
|
|
3777
|
+
return factory.createPropertyAssignment(name, synthesizeSerializableStateLiteral(property.initializer, factory))
|
|
3778
|
+
}))
|
|
3779
|
+
}
|
|
3780
|
+
|
|
3743
3781
|
function isPrimitiveDefaultLiteral(node) {
|
|
3744
3782
|
return ts.isStringLiteral(node) || ts.isNumericLiteral(node) ||
|
|
3745
3783
|
(ts.isPrefixUnaryExpression(node) && (node.operator === ts.SyntaxKind.PlusToken || node.operator === ts.SyntaxKind.MinusToken) && ts.isNumericLiteral(node.operand)) ||
|
package/framework/core.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export const Fragment: unique symbol
|
|
|
8
8
|
export function useId(): string
|
|
9
9
|
export function useState<T>(initialValue: () => T): [T, StateSetter<T>]
|
|
10
10
|
export function useState<T>(initialValue: T): [T, StateSetter<T>]
|
|
11
|
+
export function useReducer<State, Action, InitialArg>(reducer: Reducer<State, Action>, initialArg: InitialArg, initializer: (initialArg: InitialArg) => State): [State, Dispatch<Action>]
|
|
11
12
|
export function useReducer<State, Action>(reducer: Reducer<State, Action>, initialValue: State): [State, Dispatch<Action>]
|
|
12
13
|
export function useEffect(effect: () => void | EffectCleanup | Promise<void>, dependencies: readonly EffectDependency[]): void
|
|
13
14
|
export function useParams<Params extends Record<string, string> = Record<string, string>>(): Readonly<Params>
|