@kudzujs/core 0.7.29 → 0.7.30
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 +24 -0
- package/framework/README.md +1 -1
- package/framework/build.mjs +19 -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.30 - Reactive number formatting.** Fixed-locale `Intl.NumberFormat` display chains now remain reactive, restoring comma-formatted controlled inputs without broadening arbitrary render calls. Read the [release notes](./RELEASES.md#0730---reactive-number-formatting) or open the [release page](https://kudzujs.cloud/releases/0.7.30).
|
|
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,29 @@
|
|
|
1
1
|
# Kudzu Releases
|
|
2
2
|
|
|
3
|
+
## 0.7.30 - Reactive number formatting
|
|
4
|
+
|
|
5
|
+
Kudzu 0.7.30 supports fixed-locale `Intl.NumberFormat` display chains in reactive JSX locals, restoring conventional comma-formatted controlled inputs while preserving narrow render-call validation.
|
|
6
|
+
|
|
7
|
+
### New in 0.7.30
|
|
8
|
+
|
|
9
|
+
- Reactive text and attributes accept `new Intl.NumberFormat("literal").format(Math.round(expression))` over supported state-derived primitive expressions.
|
|
10
|
+
- Initial HTML is formatted during the build and existing binding ESM reevaluates the same native expression after state changes.
|
|
11
|
+
- No collection selector opcode, shared formatting runtime, hydration, or browser component instance is added.
|
|
12
|
+
- Locale values must be direct string literals; dynamic locales and options remain unsupported.
|
|
13
|
+
- Optional calls, aliases, shadowed `Intl` or `Math`, arbitrary constructors, and other render calls retain source diagnostics.
|
|
14
|
+
- A migration-derived FIRE `CurrencyInput` restores `100,000` annual and `8,333` monthly display while preserving annualized state updates.
|
|
15
|
+
- The complete suite passes 123/123 tests, including emitted evaluator and invalid dynamic-locale coverage.
|
|
16
|
+
|
|
17
|
+
### Boundary
|
|
18
|
+
|
|
19
|
+
This is display formatting only. The accepted expression requires one static locale and exactly `Math.round(expression)` as the format argument. Collection selectors and effect dependency expressions keep their existing pure expression language.
|
|
20
|
+
|
|
21
|
+
### Upgrade
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install @kudzujs/core@^0.7.30
|
|
25
|
+
```
|
|
26
|
+
|
|
3
27
|
## 0.7.29 - Hermetic TypeScript checks
|
|
4
28
|
|
|
5
29
|
Kudzu 0.7.29 makes project and migration-fixture typechecks independent of unrelated ambient types and package declarations installed in ancestor directories.
|
package/framework/README.md
CHANGED
|
@@ -14,7 +14,7 @@ A named or aliased React Router `useNavigate` import may initialize one top-leve
|
|
|
14
14
|
|
|
15
15
|
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.
|
|
16
16
|
|
|
17
|
-
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 text and attributes may reference recursively chained top-level immutable locals derived through supported pure primitive expressions from direct state; the compiler substitutes those expressions into the existing binding evaluator and subscribes every source state. Multiple direct primitive dependencies share the existing commit batching path: every value is compared with `Object.is`, and one or more same-turn changes cause one cleanup and rerun. A top-level immutable local derived through a supported pure primitive expression from direct state may also be an effect dependency: source state commits schedule evaluation, the derived result is compared with `Object.is`, and the expression is substituted into setup and cleanup handlers. Effect setup and directly returned cleanup callbacks may each resolve one top-level simple `const` function in the same component; those functions are substituted into the existing handler graph rather than retained in a browser registry. 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.
|
|
17
|
+
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 text and attributes may reference recursively chained top-level immutable locals derived through supported pure primitive expressions from direct state; the compiler substitutes those expressions into the existing binding evaluator and subscribes every source state. Fixed-locale `new Intl.NumberFormat("literal").format(Math.round(expression))` display chains reuse that binding ESM, while dynamic locales and options remain unsupported. Multiple direct primitive dependencies share the existing commit batching path: every value is compared with `Object.is`, and one or more same-turn changes cause one cleanup and rerun. A top-level immutable local derived through a supported pure primitive expression from direct state may also be an effect dependency: source state commits schedule evaluation, the derived result is compared with `Object.is`, and the expression is substituted into setup and cleanup handlers. Effect setup and directly returned cleanup callbacks may each resolve one top-level simple `const` function in the same component; those functions are substituted into the existing handler graph rather than retained in a browser registry. 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.
|
|
18
18
|
|
|
19
19
|
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.
|
|
20
20
|
|
package/framework/build.mjs
CHANGED
|
@@ -2722,6 +2722,24 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2722
2722
|
const fail = (node, message) => {
|
|
2723
2723
|
throw sourceNodeError(node, sourceFile, message)
|
|
2724
2724
|
}
|
|
2725
|
+
const validateReactiveJsxExpression = (expression, allowedNames) => {
|
|
2726
|
+
const value = unwrapExpression(expression)
|
|
2727
|
+
const formatAccess = ts.isCallExpression(value) && !value.questionDotToken && ts.isPropertyAccessExpression(value.expression) && !value.expression.questionDotToken && value.expression.name.text === "format" ? value.expression : undefined
|
|
2728
|
+
const formatter = formatAccess && unwrapExpression(formatAccess.expression)
|
|
2729
|
+
const constructor = formatter && ts.isNewExpression(formatter) && ts.isPropertyAccessExpression(formatter.expression) && formatter.expression.name.text === "NumberFormat" && ts.isIdentifier(formatter.expression.expression) && formatter.expression.expression.text === "Intl" ? formatter : undefined
|
|
2730
|
+
if (!constructor) {
|
|
2731
|
+
collectionExpression(value, {}, (node, message) => fail(node, message.replace("Rendered collection", "Reactive JSX local")), allowedNames)
|
|
2732
|
+
return
|
|
2733
|
+
}
|
|
2734
|
+
const intl = constructor.expression.expression
|
|
2735
|
+
if (!isUnshadowedGlobal(intl, sourceFile)) fail(intl, "Reactive JSX Intl.NumberFormat requires the unshadowed global Intl object")
|
|
2736
|
+
if (constructor.arguments?.length !== 1 || !ts.isStringLiteral(constructor.arguments[0])) fail(constructor, "Reactive JSX Intl.NumberFormat requires exactly one static string locale")
|
|
2737
|
+
const rounded = value.arguments.length === 1 ? unwrapExpression(value.arguments[0]) : undefined
|
|
2738
|
+
const roundAccess = rounded && ts.isCallExpression(rounded) && !rounded.questionDotToken && rounded.arguments.length === 1 && ts.isPropertyAccessExpression(rounded.expression) && !rounded.expression.questionDotToken && rounded.expression.name.text === "round" && ts.isIdentifier(rounded.expression.expression) && rounded.expression.expression.text === "Math" ? rounded.expression : undefined
|
|
2739
|
+
if (!roundAccess) fail(value, "Reactive JSX Intl.NumberFormat format() requires exactly Math.round(expression)")
|
|
2740
|
+
if (!isUnshadowedGlobal(roundAccess.expression, sourceFile)) fail(roundAccess.expression, "Reactive JSX Intl.NumberFormat requires the unshadowed global Math object")
|
|
2741
|
+
collectionExpression(rounded.arguments[0], {}, (node, message) => fail(node, message.replace("Rendered collection", "Reactive JSX local")), allowedNames)
|
|
2742
|
+
}
|
|
2725
2743
|
const resolveReactiveJsxExpression = (expression, owner, setters) => {
|
|
2726
2744
|
const declarations = jsxLocalDeclarations.get(owner)
|
|
2727
2745
|
if (!declarations) return expression
|
|
@@ -2758,7 +2776,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2758
2776
|
if (!usedStates.size) return expression
|
|
2759
2777
|
const captures = captureNames(expanded, expanded, setters)
|
|
2760
2778
|
const allowedNames = new Set([...setters.values(), ...captures])
|
|
2761
|
-
|
|
2779
|
+
validateReactiveJsxExpression(expanded, allowedNames)
|
|
2762
2780
|
return expanded
|
|
2763
2781
|
}
|
|
2764
2782
|
const componentSpecializations = new WeakMap()
|