@kudzujs/core 0.7.15 → 0.7.16
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 +1 -1
- package/RELEASES.md +23 -0
- package/framework/README.md +1 -1
- package/framework/build.mjs +31 -1
- package/framework/core.d.ts +1 -0
- package/framework/list-runtime.js +1 -1
- 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.16 - Build-time lazy state.** Anonymous synchronous `useState` initializers may return directly serializable primitive, plain-object, or array literals without adding a browser runtime. Read the [release notes](./RELEASES.md#0716---build-time-lazy-state) or open the [release page](https://kudzujs.cloud/releases/0.7.16).
|
|
14
14
|
|
|
15
15
|
- [Documentation](https://kudzujs.cloud/docs)
|
|
16
16
|
- [Installation guide](https://kudzujs.cloud/docs#install)
|
package/RELEASES.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Kudzu Releases
|
|
2
2
|
|
|
3
|
+
## 0.7.16 - Build-time lazy state
|
|
4
|
+
|
|
5
|
+
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.
|
|
6
|
+
|
|
7
|
+
### New in 0.7.16
|
|
8
|
+
|
|
9
|
+
- Anonymous synchronous zero-argument lazy initializers may directly return primitive, plain-object, or array literals.
|
|
10
|
+
- The compiler lowers those functions before state analysis, so ordinary, repeated, imported, conditional, and keyed-row state reuse existing ownership paths.
|
|
11
|
+
- Conditional removal still deletes owned state, and remount recreates a fresh clone of the serialized object or array initializer.
|
|
12
|
+
- React migration imports and direct `@kudzujs/core` imports receive the same behavior and TypeScript inference.
|
|
13
|
+
- Dynamic calls, captures, parameters, async or generator functions, named function expressions, and multi-statement bodies fail with source-located diagnostics.
|
|
14
|
+
- Static routes remain JavaScript-free; lazy initializer functions are absent from generated component and browser modules.
|
|
15
|
+
|
|
16
|
+
### Boundary
|
|
17
|
+
|
|
18
|
+
This release evaluates no arbitrary user code at build time. Initializers must directly return data the compiler can already serialize. Lazy `useReducer` initialization and dynamic state factories remain unsupported.
|
|
19
|
+
|
|
20
|
+
### Upgrade
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install @kudzujs/core@^0.7.16
|
|
24
|
+
```
|
|
25
|
+
|
|
3
26
|
## 0.7.15 - Child state ownership
|
|
4
27
|
|
|
5
28
|
Kudzu 0.7.15 gives repeated ordinary child components independent state and adds explicit state ownership to reactive conditional branches without introducing a browser component tree.
|
package/framework/README.md
CHANGED
|
@@ -35,7 +35,7 @@ Inline SVG rendering normalizes an explicit set of common React presentation ali
|
|
|
35
35
|
|
|
36
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.
|
|
37
37
|
|
|
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. 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,
|
|
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
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.
|
|
41
41
|
|
package/framework/build.mjs
CHANGED
|
@@ -287,6 +287,7 @@ export async function build({ quiet = false, minify = true } = {}) {
|
|
|
287
287
|
.replace("entries.push({ item, index, key, token, value:", "entries.push({ item, key, token, value:")
|
|
288
288
|
.replace("for (const { item, index, key, token, value } of entries) {", "for (const { item, key, token, value } of entries) {")
|
|
289
289
|
.replaceAll("fillListItem(node, item, list.descriptor.nested, index)", "fillListItem(node, item, list.descriptor.nested)")
|
|
290
|
+
.replace("fillListItem(node, item, list.descriptor.nested, index, mapListItemParts", "fillListItem(node, item, list.descriptor.nested, 0, mapListItemParts")
|
|
290
291
|
.replace("function addListRoot(list, { item, index = list.roots.size, key, token, value })", "function addListRoot(list, { item, key, token, value })")
|
|
291
292
|
.replace("fillListParts(root, listItemParts(root), listItems.get(owner), 0, __KUDZU_LIST_INDEXES__ ? listIndexes.get(owner) ?? 0 : 0)", "fillListParts(root, listItemParts(root), listItems.get(owner), 0)")
|
|
292
293
|
.replace("function fillListItem(root, item, nested = false, index = 0)", "function fillListItem(root, item, nested = false)")
|
|
@@ -2147,6 +2148,31 @@ function validateUseIdSyntax(sourceFile) {
|
|
|
2147
2148
|
visit(sourceFile)
|
|
2148
2149
|
}
|
|
2149
2150
|
|
|
2151
|
+
function normalizeLazyStateInitializers(sourceFile, factory, context) {
|
|
2152
|
+
const bindings = new Set()
|
|
2153
|
+
for (const statement of sourceFile.statements) {
|
|
2154
|
+
if (!ts.isImportDeclaration(statement) || statement.importClause?.isTypeOnly || !ts.isStringLiteral(statement.moduleSpecifier) || !["react", "@kudzujs/core"].includes(statement.moduleSpecifier.text)) continue
|
|
2155
|
+
const named = statement.importClause?.namedBindings
|
|
2156
|
+
if (named && ts.isNamedImports(named)) for (const entry of named.elements) {
|
|
2157
|
+
if (!entry.isTypeOnly && (entry.propertyName ?? entry.name).text === "useState" && entry.name.text === "useState") bindings.add("useState")
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
if (!bindings.size) return sourceFile
|
|
2161
|
+
const visitor = node => {
|
|
2162
|
+
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
|
+
const initializer = node.arguments[0]
|
|
2164
|
+
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")
|
|
2165
|
+
const expression = ts.isBlock(initializer.body)
|
|
2166
|
+
? initializer.body.statements.length === 1 && ts.isReturnStatement(initializer.body.statements[0]) ? initializer.body.statements[0].expression : undefined
|
|
2167
|
+
: initializer.body
|
|
2168
|
+
if (!expression || !isSerializableStateLiteral(expression)) throw sourceNodeError(initializer.body, sourceFile, "Lazy useState() initializer must return one directly serializable primitive, plain-object, or array literal")
|
|
2169
|
+
return factory.updateCallExpression(node, node.expression, node.typeArguments, [cloneAst(expression, factory, context)])
|
|
2170
|
+
}
|
|
2171
|
+
return ts.visitEachChild(node, visitor, context)
|
|
2172
|
+
}
|
|
2173
|
+
return ts.visitNode(sourceFile, visitor)
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2150
2176
|
function importDeclarationNames(statement) {
|
|
2151
2177
|
const names = []
|
|
2152
2178
|
if (statement.importClause?.name) names.push(statement.importClause.name.text)
|
|
@@ -2232,6 +2258,8 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2232
2258
|
sourceFile = normalizeReactMigrationSyntax(sourceFile, factory, context, importedCollections)
|
|
2233
2259
|
ts.setParentRecursive(sourceFile, false)
|
|
2234
2260
|
validateUseIdSyntax(sourceFile)
|
|
2261
|
+
sourceFile = normalizeLazyStateInitializers(sourceFile, factory, context)
|
|
2262
|
+
ts.setParentRecursive(sourceFile, false)
|
|
2235
2263
|
sourceFile = normalizeZustandMigrationSyntax(sourceFile, factory, context)
|
|
2236
2264
|
ts.setParentRecursive(sourceFile, false)
|
|
2237
2265
|
sourceFile = normalizeRenderControlFlow(sourceFile, factory, context)
|
|
@@ -2248,6 +2276,8 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2248
2276
|
imported = normalizeReactMigrationSyntax(imported, factory, context, importedSerializableCollectionNames(imported, target, sourceFiles, sourceIndex))
|
|
2249
2277
|
ts.setParentRecursive(imported, false)
|
|
2250
2278
|
validateUseIdSyntax(imported)
|
|
2279
|
+
imported = normalizeLazyStateInitializers(imported, factory, context)
|
|
2280
|
+
ts.setParentRecursive(imported, false)
|
|
2251
2281
|
imported = normalizeZustandMigrationSyntax(imported, factory, context)
|
|
2252
2282
|
ts.setParentRecursive(imported, false)
|
|
2253
2283
|
imported = normalizeRenderControlFlow(imported, factory, context)
|
|
@@ -3634,7 +3664,7 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
|
|
|
3634
3664
|
if (!ts.isVariableStatement(statement) || (statement.declarationList.flags & ts.NodeFlags.Const) === 0 || statement.declarationList.declarations.length !== 1) fail(statement, `${label} component locals must be single const declarations`)
|
|
3635
3665
|
const declaration = statement.declarationList.declarations[0]
|
|
3636
3666
|
if (declaration.initializer && ts.isCallExpression(declaration.initializer) && ts.isIdentifier(declaration.initializer.expression) && declaration.initializer.expression.text === "useState") {
|
|
3637
|
-
if (declaration.initializer.arguments.length !== 1 || !isSerializableStateLiteral(declaration.initializer.arguments[0])) throw sourceNodeError(declaration.initializer, component.getSourceFile(), "Keyed row useState() must use one directly serializable primitive, plain object, or array initial value;
|
|
3667
|
+
if (declaration.initializer.arguments.length !== 1 || !isSerializableStateLiteral(declaration.initializer.arguments[0])) throw sourceNodeError(declaration.initializer, component.getSourceFile(), "Keyed row useState() must use one directly serializable primitive, plain object, or array initial value; dynamic initializers are not supported")
|
|
3638
3668
|
if (!ts.isArrayBindingPattern(declaration.name) || declaration.name.elements.length !== 2 || declaration.name.elements.some(element => !element || !ts.isBindingElement(element) || !ts.isIdentifier(element.name) || element.initializer || element.dotDotDotToken)) throw sourceNodeError(declaration.name, component.getSourceFile(), "Keyed row useState() must use [state, setter] identifier destructuring")
|
|
3639
3669
|
const suffix = `${Math.max(0, call.pos)}_${rowStates.length}`
|
|
3640
3670
|
const state = `__kRowState${suffix}`
|
package/framework/core.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export type EffectDependency = string | number | boolean | null
|
|
|
6
6
|
export const Fragment: unique symbol
|
|
7
7
|
|
|
8
8
|
export function useId(): string
|
|
9
|
+
export function useState<T>(initialValue: () => T): [T, StateSetter<T>]
|
|
9
10
|
export function useState<T>(initialValue: T): [T, StateSetter<T>]
|
|
10
11
|
export function useReducer<State, Action>(reducer: Reducer<State, Action>, initialValue: State): [State, Dispatch<Action>]
|
|
11
12
|
export function useEffect(effect: () => void | EffectCleanup | Promise<void>, dependencies: readonly EffectDependency[]): void
|
|
@@ -195,7 +195,7 @@ function updateList(list) {
|
|
|
195
195
|
for (const { item, index, key, token, value } of entries) {
|
|
196
196
|
let node = list.roots.get(token)
|
|
197
197
|
if (!node) {
|
|
198
|
-
const staticRoot = __KUDZU_STATIC_COLLECTIONS__
|
|
198
|
+
const staticRoot = __KUDZU_STATIC_COLLECTIONS__ ? list.staticRows?.get(token)?.cloneNode(true) : undefined
|
|
199
199
|
node = staticRoot ?? (__KUDZU_NESTED_LISTS__ ? list.templateRoot : list.start.content.firstElementChild)?.cloneNode(true)
|
|
200
200
|
if (!staticRoot && node?.dataset.kListRoot !== list.descriptor.id) node = undefined
|
|
201
201
|
if (!node) throw new Error("Keyed list template has no root element")
|