@kudzujs/core 0.7.26 → 0.7.29
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 +72 -0
- package/framework/README.md +2 -2
- package/framework/build.mjs +242 -32
- 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.29 - Hermetic TypeScript checks.** Project and fixture typechecks no longer inherit unrelated ambient types or third-party declarations from ancestor `node_modules` directories. Read the [release notes](./RELEASES.md#0729---hermetic-typescript-checks) or open the [release page](https://kudzujs.cloud/releases/0.7.29).
|
|
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,77 @@
|
|
|
1
1
|
# Kudzu Releases
|
|
2
2
|
|
|
3
|
+
## 0.7.29 - Hermetic TypeScript checks
|
|
4
|
+
|
|
5
|
+
Kudzu 0.7.29 makes project and migration-fixture typechecks independent of unrelated ambient types and package declarations installed in ancestor directories.
|
|
6
|
+
|
|
7
|
+
### New in 0.7.29
|
|
8
|
+
|
|
9
|
+
- The root TypeScript configuration declares an empty ambient `types` set instead of scanning every visible ancestor `node_modules/@types` directory.
|
|
10
|
+
- `create-kudzu` emits the same isolated TypeScript configuration for new projects.
|
|
11
|
+
- Migration fixture checks skip third-party declaration bodies while continuing to typecheck fixture TS and TSX against Kudzu's reduced declarations.
|
|
12
|
+
- A clean `npm ci` checkout nested below conflicting `@types/minimatch`, React, and React Router installations now passes `npm run check`.
|
|
13
|
+
- The complete suite continues to pass 122/122 tests.
|
|
14
|
+
|
|
15
|
+
### Upgrade
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @kudzujs/core@^0.7.29
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## 0.7.28 - Nested component specialization
|
|
22
|
+
|
|
23
|
+
Kudzu 0.7.28 recursively specializes components rendered inside setter-adapter children, allowing conventional optional UI such as an imported hookful Tooltip to remain declarative without adding a browser component runtime.
|
|
24
|
+
|
|
25
|
+
### New in 0.7.28
|
|
26
|
+
|
|
27
|
+
- Setter-adapter children may recursively render synchronous same-file or relative-imported components.
|
|
28
|
+
- Nested component props, defaults, JSX children, static assets, handlers, and relative TypeScript imports reuse the existing call-site specialization path.
|
|
29
|
+
- Nested `useState()`, `useId()`, `useRef()`, and supported effects join the outer generated owner on unconditional or statically truthy paths.
|
|
30
|
+
- Literal `&&` and ternary conditions fold before specialization, so omitted and false optional components allocate no hooks or browser capabilities.
|
|
31
|
+
- Recursive component cycles, namespace/package components, and unresolved component imports retain source diagnostics.
|
|
32
|
+
- Hookful nested components on dynamic paths fail instead of receiving incorrect unconditional ownership.
|
|
33
|
+
- Setter callbacks cannot cross a second component boundary.
|
|
34
|
+
- A migration-derived FIRE `AgeInput` now restores its imported hookful `Tooltip`, including generated ARIA identity and hover/focus state.
|
|
35
|
+
- The complete suite passes 122/122 tests with Chrome coverage for nested tooltip state, omitted optional UI, and existing parent/child lifecycle behavior.
|
|
36
|
+
|
|
37
|
+
### Boundary
|
|
38
|
+
|
|
39
|
+
Nested hooks are accepted only when the nested component is unconditional or guarded by a statically truthy literal after outer prop substitution. Dynamic conditional hook ownership and forwarding the setter through another component remain unsupported. Components still erase to intrinsic HTML and route-specific capabilities; React is never emitted or executed.
|
|
40
|
+
|
|
41
|
+
### Upgrade
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm install @kudzujs/core@^0.7.28
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## 0.7.27 - Stateful component migration
|
|
48
|
+
|
|
49
|
+
Kudzu 0.7.27 lets ordinary setter-adapter components keep their local hooks and derived render values while compiling them into static HTML plus existing route-specific capabilities, not a browser component runtime.
|
|
50
|
+
|
|
51
|
+
### New in 0.7.27
|
|
52
|
+
|
|
53
|
+
- Same-file and relative-imported children receiving one supported setter callback may own multiple directly serializable `useState()` values.
|
|
54
|
+
- A child string draft may initialize from one direct primitive state prop through zero-argument `.toString()`, covering conventional controlled age and numeric inputs.
|
|
55
|
+
- Setter-adapter children may use deterministic build-time `useId()` values and `null`-initialized object refs.
|
|
56
|
+
- Supported child effects reuse existing route ESM, dependency comparison, cleanup, conditional ownership, and stale-work isolation.
|
|
57
|
+
- Conditional removal deletes child state, resolves refs to `null`, and runs cleanup; remount creates fresh ownership and prop dependency effects synchronize drafts to the latest parent value.
|
|
58
|
+
- Inline and simple `const` parent callbacks continue to specialize into the generated child helper instead of becoming serialized function captures.
|
|
59
|
+
- Reactive JSX text and attributes may read recursively chained top-level immutable locals built from supported pure primitive state expressions.
|
|
60
|
+
- Impure derived render calls, non-null child refs, unsupported dynamic state initializers, repeated callback use, and wider callback forwarding retain source-located diagnostics.
|
|
61
|
+
- React Router normalization now safely ignores local export declarations without module specifiers.
|
|
62
|
+
- A migration-derived FIRE age input verifies local draft editing, parent updates, blur validation, effect synchronization, and remount after a hidden parent update.
|
|
63
|
+
- The complete suite passes 121/121 tests, including same-file/imported hook ownership and Chrome lifecycle coverage.
|
|
64
|
+
|
|
65
|
+
### Boundary
|
|
66
|
+
|
|
67
|
+
This remains one compiler-specialized component boundary with one supported setter callback use. Prop-derived state initialization accepts only a direct primitive state prop's zero-argument `.toString()`; other dynamic initializers remain unsupported. Refs must initialize with `null`, effects must use the existing supported callback and dependency forms, and React is never emitted or executed.
|
|
68
|
+
|
|
69
|
+
### Upgrade
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
npm install @kudzujs/core@^0.7.27
|
|
73
|
+
```
|
|
74
|
+
|
|
3
75
|
## 0.7.26 - React-shaped migration and faster lists
|
|
4
76
|
|
|
5
77
|
Kudzu 0.7.26 broadens ordinary React-shaped migration input and removes repeated keyed-row cleanup scans, making the matched 500-card search faster than React without adding a VDOM, hydration, or a retained component tree.
|
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. 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. 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
|
|
|
@@ -47,7 +47,7 @@ Rendered collection selectors compile immutable local aliases and inline `(item)
|
|
|
47
47
|
|
|
48
48
|
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.
|
|
49
49
|
|
|
50
|
-
A direct setter may cross one same-file or relative-imported component boundary when the child invokes it exactly once inside an intrinsic event handler. Inline and simple `const` setter callbacks may use that adapter shape or direct event forwarding. Value adapters such as `event => onValueChange(event.currentTarget.value)` specialize into the parent setter graph instead of serializing a function. A `null`-initialized object ref may cross the same boundary to the direct intrinsic root.
|
|
50
|
+
A direct setter may cross one same-file or relative-imported component boundary when the child invokes it exactly once inside an intrinsic event handler. Inline and simple `const` setter callbacks may use that adapter shape or direct event forwarding. Value adapters such as `event => onValueChange(event.currentTarget.value)` specialize into the parent setter graph instead of serializing a function. The child may also own directly serializable `useState()` values or initialize string state with one direct primitive state prop's zero-argument `.toString()`, plus `useId()` values, supported effects, and object refs initialized with `null`. Same-file and relative-imported nested components recursively specialize away; nested hooks join the same generated owner only on unconditional or statically truthy paths. Dynamic nested hook paths, forwarding the setter through a second boundary, and other dynamic state initializers remain unsupported. A parent-owned `null`-initialized object ref may cross the same boundary to the direct intrinsic root. These remain compiler-owned descriptors: conditional removal drops the handler with the element, deletes child state, resolves refs to `null`, and runs effect cleanup, while remount creates fresh ownership and dependency effects synchronize prop-derived state without retaining a component instance.
|
|
51
51
|
|
|
52
52
|
`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.
|
|
53
53
|
|
package/framework/build.mjs
CHANGED
|
@@ -1790,7 +1790,7 @@ function normalizeReactRouterSyntax(sourceFile, factory, context, base) {
|
|
|
1790
1790
|
const searchHooks = new Set()
|
|
1791
1791
|
const navigateHooks = new Set()
|
|
1792
1792
|
for (const statement of sourceFile.statements) {
|
|
1793
|
-
if ((ts.isExportDeclaration(statement) || ts.isImportDeclaration(statement)) && ts.isStringLiteral(statement.moduleSpecifier) && statement.moduleSpecifier.text === "react-router-dom") {
|
|
1793
|
+
if ((ts.isExportDeclaration(statement) || ts.isImportDeclaration(statement)) && statement.moduleSpecifier && ts.isStringLiteral(statement.moduleSpecifier) && statement.moduleSpecifier.text === "react-router-dom") {
|
|
1794
1794
|
if (ts.isExportDeclaration(statement)) throw sourceNodeError(statement, sourceFile, "React Router exports are not supported; import Link directly where it renders")
|
|
1795
1795
|
const clause = statement.importClause
|
|
1796
1796
|
if (clause?.isTypeOnly) continue
|
|
@@ -2584,6 +2584,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2584
2584
|
const listConditions = new WeakMap()
|
|
2585
2585
|
const nestedLists = new WeakMap()
|
|
2586
2586
|
const listEffectEntries = new WeakMap()
|
|
2587
|
+
const componentEffectEntries = new WeakMap()
|
|
2587
2588
|
let usesBehavior = false
|
|
2588
2589
|
let usesBinding = false
|
|
2589
2590
|
let usesConditional = false
|
|
@@ -2592,6 +2593,10 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2592
2593
|
let usesListItem = false
|
|
2593
2594
|
let usesRowState = false
|
|
2594
2595
|
let usesRowRef = false
|
|
2596
|
+
let usesComponentState = false
|
|
2597
|
+
let usesComponentId = false
|
|
2598
|
+
let usesComponentRef = false
|
|
2599
|
+
let usesComponentEffects = false
|
|
2595
2600
|
|
|
2596
2601
|
const collect = node => {
|
|
2597
2602
|
if (ts.isVariableDeclaration(node) && node.initializer && ts.isCallExpression(node.initializer)) {
|
|
@@ -2717,7 +2722,47 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2717
2722
|
const fail = (node, message) => {
|
|
2718
2723
|
throw sourceNodeError(node, sourceFile, message)
|
|
2719
2724
|
}
|
|
2725
|
+
const resolveReactiveJsxExpression = (expression, owner, setters) => {
|
|
2726
|
+
const declarations = jsxLocalDeclarations.get(owner)
|
|
2727
|
+
if (!declarations) return expression
|
|
2728
|
+
const substitutions = new Map()
|
|
2729
|
+
const resolving = []
|
|
2730
|
+
const resolve = (name, reference) => {
|
|
2731
|
+
if (substitutions.has(name)) return
|
|
2732
|
+
const entries = declarations.get(name)
|
|
2733
|
+
if (!entries?.length) return
|
|
2734
|
+
if (jsxLocalsByFunction.get(owner)?.has(name)) return
|
|
2735
|
+
if (entries.length !== 1 || entries[0].node.parent?.parent?.parent !== owner?.body) return
|
|
2736
|
+
const cycle = resolving.indexOf(name)
|
|
2737
|
+
if (cycle >= 0) fail(reference, `Reactive JSX local cycle: ${[...resolving.slice(cycle), name].join(" -> ")}`)
|
|
2738
|
+
resolving.push(name)
|
|
2739
|
+
const initializer = entries[0].initializer
|
|
2740
|
+
const visit = node => {
|
|
2741
|
+
if (ts.isIdentifier(node) && isReferenceIdentifier(node) && !isShadowedByParameter(node, initializer) && declarations.has(node.text)) resolve(node.text, node)
|
|
2742
|
+
ts.forEachChild(node, visit)
|
|
2743
|
+
}
|
|
2744
|
+
visit(initializer)
|
|
2745
|
+
substitutions.set(name, substituteClone(initializer, substitutions, factory, context))
|
|
2746
|
+
resolving.pop()
|
|
2747
|
+
}
|
|
2748
|
+
const visit = node => {
|
|
2749
|
+
if (ts.isIdentifier(node) && isReferenceIdentifier(node) && !isShadowedByParameter(node, expression) && declarations.has(node.text)) resolve(node.text, node)
|
|
2750
|
+
ts.forEachChild(node, visit)
|
|
2751
|
+
}
|
|
2752
|
+
visit(expression)
|
|
2753
|
+
if (!substitutions.size) return expression
|
|
2754
|
+
const expanded = substituteClone(expression, substitutions, factory, context)
|
|
2755
|
+
ts.setParentRecursive(expanded, false)
|
|
2756
|
+
expanded.parent = expression.parent
|
|
2757
|
+
const usedStates = referencedStateNames(expanded, setters)
|
|
2758
|
+
if (!usedStates.size) return expression
|
|
2759
|
+
const captures = captureNames(expanded, expanded, setters)
|
|
2760
|
+
const allowedNames = new Set([...setters.values(), ...captures])
|
|
2761
|
+
collectionExpression(expanded, {}, (node, message) => fail(node, message.replace("Rendered collection", "Reactive JSX local")), allowedNames)
|
|
2762
|
+
return expanded
|
|
2763
|
+
}
|
|
2720
2764
|
const componentSpecializations = new WeakMap()
|
|
2765
|
+
const setterHookHelpers = new WeakMap()
|
|
2721
2766
|
const expandedRowSpecializations = new WeakMap()
|
|
2722
2767
|
const nestedRowSpecializations = new Map()
|
|
2723
2768
|
const reducerComponentCalls = new WeakSet()
|
|
@@ -2806,6 +2851,89 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2806
2851
|
expanded.parent = root.parent
|
|
2807
2852
|
return expanded
|
|
2808
2853
|
}
|
|
2854
|
+
const staticConditionValue = expression => {
|
|
2855
|
+
const value = unwrapExpression(expression)
|
|
2856
|
+
if (value.kind === ts.SyntaxKind.TrueKeyword) return true
|
|
2857
|
+
if (value.kind === ts.SyntaxKind.FalseKeyword || value.kind === ts.SyntaxKind.NullKeyword || ts.isIdentifier(value) && value.text === "undefined") return false
|
|
2858
|
+
if (ts.isStringLiteral(value) || ts.isNoSubstitutionTemplateLiteral(value)) return Boolean(value.text)
|
|
2859
|
+
if (ts.isNumericLiteral(value)) return Number(value.text) !== 0
|
|
2860
|
+
return undefined
|
|
2861
|
+
}
|
|
2862
|
+
const foldSetterStaticConditions = root => {
|
|
2863
|
+
const visit = node => {
|
|
2864
|
+
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
|
|
2865
|
+
const condition = staticConditionValue(node.left)
|
|
2866
|
+
if (condition !== undefined) return condition ? ts.visitNode(node.right, visit) : node.left
|
|
2867
|
+
}
|
|
2868
|
+
if (ts.isConditionalExpression(node)) {
|
|
2869
|
+
const condition = staticConditionValue(node.condition)
|
|
2870
|
+
if (condition !== undefined) return ts.visitNode(condition ? node.whenTrue : node.whenFalse, visit)
|
|
2871
|
+
}
|
|
2872
|
+
return ts.visitEachChild(node, visit, context)
|
|
2873
|
+
}
|
|
2874
|
+
const folded = ts.visitNode(root, visit)
|
|
2875
|
+
ts.setParentRecursive(folded, false)
|
|
2876
|
+
folded.parent = root.parent
|
|
2877
|
+
return folded
|
|
2878
|
+
}
|
|
2879
|
+
const expandSetterComponents = (root, componentSource, trail, aggregate, parentSetters) => {
|
|
2880
|
+
root = foldSetterStaticConditions(root)
|
|
2881
|
+
const replacements = new WeakMap()
|
|
2882
|
+
let count = 0
|
|
2883
|
+
const visit = (node, dynamic = false) => {
|
|
2884
|
+
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
|
|
2885
|
+
visit(node.left, dynamic)
|
|
2886
|
+
visit(node.right, true)
|
|
2887
|
+
return
|
|
2888
|
+
}
|
|
2889
|
+
if (ts.isConditionalExpression(node)) {
|
|
2890
|
+
visit(node.condition, dynamic)
|
|
2891
|
+
visit(node.whenTrue, true)
|
|
2892
|
+
visit(node.whenFalse, true)
|
|
2893
|
+
return
|
|
2894
|
+
}
|
|
2895
|
+
const tag = jsxTagName(node)
|
|
2896
|
+
if (tag && (!ts.isIdentifier(tag) || tag.text[0] !== tag.text[0].toLowerCase())) {
|
|
2897
|
+
if (!ts.isIdentifier(tag)) fail(node, "Nested setter-callback components must use identifier JSX tags")
|
|
2898
|
+
const name = tag.text
|
|
2899
|
+
let component = localComponentDeclaration(componentSource, name)
|
|
2900
|
+
let imported = false
|
|
2901
|
+
if (!component) {
|
|
2902
|
+
const binding = clientImportBindings(componentSource, componentSource.fileName, sourceFiles).get(name)
|
|
2903
|
+
if (!binding || binding.kind === "namespace") fail(node, `Nested setter-callback component ${name} must be declared locally or imported from a relative TypeScript module`)
|
|
2904
|
+
component = resolveComponentExport(binding.target, binding.kind === "default" ? "default" : binding.imported, importedSource, sourceFiles)
|
|
2905
|
+
imported = true
|
|
2906
|
+
}
|
|
2907
|
+
if (trail.includes(component)) {
|
|
2908
|
+
const chain = [...trail, component].map(entry => entry.name?.text || "anonymous").join(" -> ")
|
|
2909
|
+
fail(node, `Nested setter-callback component cycle: ${chain}`)
|
|
2910
|
+
}
|
|
2911
|
+
const setters = new Map(parentSetters)
|
|
2912
|
+
for (const state of aggregate.ordinaryStates) setters.set(state.setter, state.state)
|
|
2913
|
+
if (jsxSetterCallbackProps(node, setters, functions, reducersForNode(node, reducersByFunction)).length) fail(node, "Setter callbacks cannot cross a second component boundary")
|
|
2914
|
+
const nested = specializeComponentCall(node, component, sourceFile, factory, context, fail, "Nested setter-callback", true, true, new Set(setters.values()))
|
|
2915
|
+
if (dynamic && (nested.hookDeclarations.length || nested.effects.length)) fail(node, "Hookful nested setter-callback components require an unconditional or statically truthy render path")
|
|
2916
|
+
nested.root = expandSetterComponents(nested.root, component.getSourceFile(), [...trail, component], nested, setters)
|
|
2917
|
+
if (imported) synthesizeTree(nested.root = mergeSpecializedImports(nested.root, component.getSourceFile(), node, nested.effects))
|
|
2918
|
+
aggregate.calculations.push(...nested.calculations)
|
|
2919
|
+
aggregate.effects.push(...nested.effects)
|
|
2920
|
+
aggregate.hookDeclarations.push(...nested.hookDeclarations)
|
|
2921
|
+
aggregate.ordinaryStates.push(...nested.ordinaryStates)
|
|
2922
|
+
aggregate.ordinaryRefs.push(...nested.ordinaryRefs)
|
|
2923
|
+
aggregate.usesComponentId ||= nested.usesComponentId
|
|
2924
|
+
replacements.set(node, nested.root)
|
|
2925
|
+
count++
|
|
2926
|
+
return
|
|
2927
|
+
}
|
|
2928
|
+
ts.forEachChild(node, child => visit(child, dynamic))
|
|
2929
|
+
}
|
|
2930
|
+
visit(root)
|
|
2931
|
+
if (!count) return root
|
|
2932
|
+
const expanded = replaceSpecializedCalls(root, replacements, context)
|
|
2933
|
+
ts.setParentRecursive(expanded, false)
|
|
2934
|
+
expanded.parent = root.parent
|
|
2935
|
+
return expanded
|
|
2936
|
+
}
|
|
2809
2937
|
for (const [name, component] of components) {
|
|
2810
2938
|
const calls = jsxTagUses(sourceFile, name)
|
|
2811
2939
|
const stateBackedCalls = calls.filter(call => isStateBackedListComponentCall(call, component.function, settersByFunction.get(nearestFunction(call)) ?? new Map()))
|
|
@@ -2856,9 +2984,57 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2856
2984
|
collectReferences(component.body)
|
|
2857
2985
|
if (references.length !== 1) fail(element, `Setter-callback prop ${JSON.stringify(prop)} must be used exactly once in the component`)
|
|
2858
2986
|
}
|
|
2859
|
-
const specialization = specializeComponentCall(call, component, sourceFile, factory, context, fail, "Setter-callback")
|
|
2860
|
-
if (specialization.
|
|
2987
|
+
const specialization = specializeComponentCall(call, component, sourceFile, factory, context, fail, "Setter-callback", false, true, new Set(settersForNode(call, settersByFunction).values()))
|
|
2988
|
+
if (specialization.hookDeclarations.length || specialization.effects.length) {
|
|
2989
|
+
const substitutions = new Map()
|
|
2990
|
+
const attributes = ts.isJsxElement(call) ? call.openingElement.attributes : call.attributes
|
|
2991
|
+
for (const attribute of attributes.properties) {
|
|
2992
|
+
if (!ts.isJsxAttribute(attribute) || !callbackProps.includes(attribute.name.text) || !attribute.initializer || !ts.isJsxExpression(attribute.initializer) || !ts.isIdentifier(attribute.initializer.expression)) continue
|
|
2993
|
+
const callback = functions.get(attribute.initializer.expression.text)
|
|
2994
|
+
if (callback) substitutions.set(attribute.initializer.expression.text, callback)
|
|
2995
|
+
}
|
|
2996
|
+
if (substitutions.size) {
|
|
2997
|
+
specialization.root = substituteClone(specialization.root, substitutions, factory, context)
|
|
2998
|
+
for (const effect of specialization.effects) effect.call = substituteClone(effect.call, substitutions, factory, context)
|
|
2999
|
+
}
|
|
3000
|
+
}
|
|
3001
|
+
specialization.root = expandSetterComponents(specialization.root, component.getSourceFile(), [component], specialization, settersForNode(call, settersByFunction))
|
|
2861
3002
|
if (imported) synthesizeTree(specialization.root = mergeSpecializedImports(specialization.root, component.getSourceFile(), call, specialization.effects))
|
|
3003
|
+
if (specialization.hookDeclarations.length || specialization.effects.length) {
|
|
3004
|
+
const owner = nearestFunction(call)
|
|
3005
|
+
const name = `KSetterComponent${Math.max(0, call.pos)}`
|
|
3006
|
+
const effectStatements = specialization.effects.map(entry => {
|
|
3007
|
+
const effectCall = factory.updateCallExpression(entry.call, factory.createIdentifier("__kComponentUseEffect"), entry.call.typeArguments, entry.call.arguments)
|
|
3008
|
+
synthesizeTree(effectCall)
|
|
3009
|
+
const effectSource = entry.source.getSourceFile()
|
|
3010
|
+
componentEffectEntries.set(effectCall, { source: entry.source, sourceFile: effectSource, imports: clientImportBindings(effectSource, effectSource.fileName, sourceFiles) })
|
|
3011
|
+
return factory.createExpressionStatement(effectCall)
|
|
3012
|
+
})
|
|
3013
|
+
const helper = factory.createFunctionDeclaration(
|
|
3014
|
+
undefined,
|
|
3015
|
+
undefined,
|
|
3016
|
+
name,
|
|
3017
|
+
undefined,
|
|
3018
|
+
[],
|
|
3019
|
+
undefined,
|
|
3020
|
+
factory.createBlock([...specialization.hookDeclarations, ...effectStatements, factory.createReturnStatement(specialization.root)], true)
|
|
3021
|
+
)
|
|
3022
|
+
ts.setParentRecursive(helper, false)
|
|
3023
|
+
helper.parent = owner.body
|
|
3024
|
+
const helpers = setterHookHelpers.get(owner.body) ?? []
|
|
3025
|
+
helpers.push(helper)
|
|
3026
|
+
setterHookHelpers.set(owner.body, helpers)
|
|
3027
|
+
const setters = new Map(settersForNode(call, settersByFunction))
|
|
3028
|
+
for (const state of specialization.ordinaryStates) setters.set(state.setter, state.state)
|
|
3029
|
+
settersByFunction.set(helper, setters)
|
|
3030
|
+
usesComponentState ||= specialization.ordinaryStates.length > 0
|
|
3031
|
+
usesComponentId ||= specialization.usesComponentId
|
|
3032
|
+
usesComponentRef ||= specialization.ordinaryRefs.length > 0
|
|
3033
|
+
usesComponentEffects ||= specialization.effects.length > 0
|
|
3034
|
+
specialization.root = factory.createJsxSelfClosingElement(factory.createIdentifier(name), undefined, factory.createJsxAttributes([]))
|
|
3035
|
+
ts.setParentRecursive(specialization.root, false)
|
|
3036
|
+
specialization.root.parent = call.parent
|
|
3037
|
+
}
|
|
2862
3038
|
componentSpecializations.set(call, specialization)
|
|
2863
3039
|
}
|
|
2864
3040
|
for (const [name, component] of components) {
|
|
@@ -3114,6 +3290,9 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
3114
3290
|
}
|
|
3115
3291
|
|
|
3116
3292
|
const visitor = node => {
|
|
3293
|
+
if (ts.isBlock(node) && setterHookHelpers.has(node)) {
|
|
3294
|
+
return ts.visitEachChild(factory.updateBlock(node, [...setterHookHelpers.get(node), ...node.statements]), visitor, context)
|
|
3295
|
+
}
|
|
3117
3296
|
if (specializedDeclarations.has(node)) return node
|
|
3118
3297
|
if (componentSpecializations.has(node)) return ts.visitNode(componentSpecializations.get(node).root, visitor)
|
|
3119
3298
|
|
|
@@ -3139,9 +3318,11 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
3139
3318
|
}
|
|
3140
3319
|
|
|
3141
3320
|
const listEffect = ts.isCallExpression(node) ? listEffectEntries.get(node) : undefined
|
|
3142
|
-
|
|
3321
|
+
const componentEffect = ts.isCallExpression(node) ? componentEffectEntries.get(node) : undefined
|
|
3322
|
+
const specializedEffect = listEffect ?? componentEffect
|
|
3323
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && (hasUseEffectImport && node.expression.text === "useEffect" || specializedEffect)) {
|
|
3143
3324
|
const effectFail = (target, message) => {
|
|
3144
|
-
if (
|
|
3325
|
+
if (specializedEffect) throw sourceNodeError(specializedEffect.source, specializedEffect.sourceFile, message)
|
|
3145
3326
|
fail(target, message)
|
|
3146
3327
|
}
|
|
3147
3328
|
if (node.arguments.length !== 2) effectFail(node, "useEffect() requires exactly a callback and literal dependency array")
|
|
@@ -3234,7 +3415,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
3234
3415
|
const invalidCleanup = returns.cleanups.find(cleanup => cleanup.parameters.length || cleanup.asteriskToken)
|
|
3235
3416
|
if (invalidCleanup) effectFail(invalidCleanup, "useEffect() cleanup functions cannot declare parameters or be generators")
|
|
3236
3417
|
if (returns.cleanup && callback.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) effectFail(callback, "useEffect() async callbacks cannot return cleanup functions")
|
|
3237
|
-
const callbackSource =
|
|
3418
|
+
const callbackSource = specializedEffect?.sourceFile ?? sourceFile
|
|
3238
3419
|
const callbackFile = callbackSource.fileName
|
|
3239
3420
|
const workerStart = workerReferences.length
|
|
3240
3421
|
let compiledCallback = dependencySubstitutions.size ? substituteClone(callback, dependencySubstitutions, factory, context) : callback
|
|
@@ -3248,7 +3429,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
3248
3429
|
} else {
|
|
3249
3430
|
compiledCallback = rewriteEffectWorkers(compiledCallback, callbackFile, callbackSource, sourceFiles, workerReferences, factory, context)
|
|
3250
3431
|
}
|
|
3251
|
-
const descriptor = compileNativeCallback(compiledCallback, setters, reducersForNode(node, reducersByFunction), factory, effectHandlers,
|
|
3432
|
+
const descriptor = compileNativeCallback(compiledCallback, setters, reducersForNode(node, reducersByFunction), factory, effectHandlers, specializedEffect?.imports ?? importBindings, clientImports, "effect", dependencyItem, true, returns.cleanup)
|
|
3252
3433
|
for (const reference of workerReferences.slice(workerStart)) Object.assign(reference, { module: handlerUrl, handler: descriptor.exportName })
|
|
3253
3434
|
usesListItem ||= Boolean(itemDependencies.length && !listEffect)
|
|
3254
3435
|
usesBehavior = true
|
|
@@ -3259,7 +3440,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
3259
3440
|
factory.createStringLiteral(descriptor.exportName),
|
|
3260
3441
|
descriptor.states,
|
|
3261
3442
|
descriptor.scope,
|
|
3262
|
-
factory.createStringLiteral(
|
|
3443
|
+
factory.createStringLiteral(specializedEffect ? sourceLocation(specializedEffect.source, specializedEffect.sourceFile) : sourceLocation(node, sourceFile)),
|
|
3263
3444
|
returns.cleanup ? factory.createTrue() : factory.createFalse(),
|
|
3264
3445
|
factory.createArrayLiteralExpression(itemDependencies.map(field => factory.createStringLiteral(field))),
|
|
3265
3446
|
hasDerivedDependency ? jsonExpression(dependencyExpressions, factory) : factory.createArrayLiteralExpression(),
|
|
@@ -3267,7 +3448,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
3267
3448
|
])
|
|
3268
3449
|
}
|
|
3269
3450
|
|
|
3270
|
-
if (ts.isVariableDeclaration(node) && ts.isArrayBindingPattern(node.name) && node.initializer && ts.isCallExpression(node.initializer) && ts.isIdentifier(node.initializer.expression) && ((node.initializer.expression.text === "useState" || node.initializer.expression.text === "__kRowUseState") && node.initializer.arguments.length === 1 || node.initializer.expression.text === "useReducer" && node.initializer.arguments.length === 2)) {
|
|
3451
|
+
if (ts.isVariableDeclaration(node) && ts.isArrayBindingPattern(node.name) && node.initializer && ts.isCallExpression(node.initializer) && ts.isIdentifier(node.initializer.expression) && ((node.initializer.expression.text === "useState" || node.initializer.expression.text === "__kRowUseState" || node.initializer.expression.text === "__kComponentUseState") && node.initializer.arguments.length === 1 || node.initializer.expression.text === "useReducer" && node.initializer.arguments.length === 2)) {
|
|
3271
3452
|
const stateElement = node.name.elements[0]
|
|
3272
3453
|
if (!stateElement || !ts.isBindingElement(stateElement) || !ts.isIdentifier(stateElement.name)) return node
|
|
3273
3454
|
const initializer = factory.updateCallExpression(node.initializer, node.initializer.expression, node.initializer.typeArguments, [
|
|
@@ -3331,18 +3512,20 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
3331
3512
|
if (compiled !== node.expression) return factory.updateJsxExpression(node, compiled)
|
|
3332
3513
|
}
|
|
3333
3514
|
const setters = settersForNode(node, settersByFunction)
|
|
3334
|
-
const
|
|
3335
|
-
const
|
|
3336
|
-
|
|
3515
|
+
const expression = resolveReactiveJsxExpression(node.expression, nearestFunction(node), setters)
|
|
3516
|
+
const usedStates = referencedStateNames(expression, setters)
|
|
3517
|
+
const captures = captureNames(expression, expression, setters)
|
|
3518
|
+
if ((usedStates.size || captures.size) && !ts.isIdentifier(expression) && !containsJsx(expression)) {
|
|
3337
3519
|
usesBehavior = true
|
|
3338
3520
|
usesBinding = true
|
|
3339
|
-
return factory.updateJsxExpression(node, compileReactiveBinding(
|
|
3521
|
+
return factory.updateJsxExpression(node, compileReactiveBinding(expression, setters, factory, context, reactiveBindings, handlerUrl))
|
|
3340
3522
|
}
|
|
3341
3523
|
}
|
|
3342
3524
|
|
|
3343
3525
|
if (ts.isJsxAttribute(node) && node.initializer && ts.isJsxExpression(node.initializer) && node.initializer.expression && !isContextProviderValue(node, contexts) && !/^on/i.test(node.name.text) && !["key", "ref", "dangerouslysetinnerhtml"].includes(node.name.text.toLowerCase())) {
|
|
3344
|
-
const
|
|
3526
|
+
const sourceExpression = node.initializer.expression
|
|
3345
3527
|
const setters = settersForNode(node, settersByFunction)
|
|
3528
|
+
const expression = resolveReactiveJsxExpression(sourceExpression, nearestFunction(node), setters)
|
|
3346
3529
|
const usedStates = referencedStateNames(expression, setters)
|
|
3347
3530
|
const captures = captureNames(expression, expression, setters)
|
|
3348
3531
|
if ((usedStates.size || captures.size) && !ts.isIdentifier(expression)) {
|
|
@@ -3393,6 +3576,10 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
3393
3576
|
if (usesListEffects) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("useEffect"), factory.createIdentifier("__kListUseEffect")))
|
|
3394
3577
|
if (usesRowState) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("useState"), factory.createIdentifier("__kRowUseState")))
|
|
3395
3578
|
if (usesRowRef) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("useRef"), factory.createIdentifier("__kRowUseRef")))
|
|
3579
|
+
if (usesComponentState) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("useState"), factory.createIdentifier("__kComponentUseState")))
|
|
3580
|
+
if (usesComponentId) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("useId"), factory.createIdentifier("__kComponentUseId")))
|
|
3581
|
+
if (usesComponentRef) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("useRef"), factory.createIdentifier("__kComponentUseRef")))
|
|
3582
|
+
if (usesComponentEffects) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("useEffect"), factory.createIdentifier("__kComponentUseEffect")))
|
|
3396
3583
|
if (usesBinding || usesConditional) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("bindingValue"), factory.createIdentifier("__kBindingValue")))
|
|
3397
3584
|
const behaviorImport = factory.createImportDeclaration(
|
|
3398
3585
|
undefined,
|
|
@@ -4029,7 +4216,7 @@ function expandSpecializedRest(root, returned, component, rest, entries, factory
|
|
|
4029
4216
|
return factory.updateJsxElement(root, opening, root.children, root.closingElement)
|
|
4030
4217
|
}
|
|
4031
4218
|
|
|
4032
|
-
function specializeComponentCall(call, component, sourceFile, factory, context, fail, label = "Keyed list", allowComponentRoot = false) {
|
|
4219
|
+
function specializeComponentCall(call, component, sourceFile, factory, context, fail, label = "Keyed list", allowComponentRoot = false, ordinaryHooks = false, ordinaryStateNames = new Set()) {
|
|
4033
4220
|
if (component.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword) || component.asteriskToken) fail(component, `${label} components must be synchronous`)
|
|
4034
4221
|
if (component.parameters.length !== 1 || !ts.isObjectBindingPattern(component.parameters[0].name)) fail(component, `${label} components must use one destructured props parameter`)
|
|
4035
4222
|
const callAttributes = ts.isJsxElement(call) ? call.openingElement.attributes : call.attributes
|
|
@@ -4090,6 +4277,9 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
|
|
|
4090
4277
|
const hookDeclarations = []
|
|
4091
4278
|
const rowStates = []
|
|
4092
4279
|
const rowRefs = []
|
|
4280
|
+
const ordinaryStates = []
|
|
4281
|
+
const ordinaryRefs = []
|
|
4282
|
+
let usesComponentId = false
|
|
4093
4283
|
if (!ts.isBlock(component.body)) {
|
|
4094
4284
|
returned = component.body
|
|
4095
4285
|
} else {
|
|
@@ -4104,35 +4294,52 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
|
|
|
4104
4294
|
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`)
|
|
4105
4295
|
const declaration = statement.declarationList.declarations[0]
|
|
4106
4296
|
if (declaration.initializer && ts.isCallExpression(declaration.initializer) && ts.isIdentifier(declaration.initializer.expression) && declaration.initializer.expression.text === "useState") {
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
const
|
|
4110
|
-
const
|
|
4111
|
-
const
|
|
4297
|
+
const hookLabel = ordinaryHooks ? "Setter-callback component" : "Keyed row"
|
|
4298
|
+
const initialArgument = declaration.initializer.arguments[0]
|
|
4299
|
+
const propReceiver = ordinaryHooks && initialArgument && ts.isCallExpression(initialArgument) && initialArgument.arguments.length === 0 && !initialArgument.questionDotToken && ts.isPropertyAccessExpression(initialArgument.expression) && !initialArgument.expression.questionDotToken && initialArgument.expression.name.text === "toString" && ts.isIdentifier(initialArgument.expression.expression) ? initialArgument.expression.expression : undefined
|
|
4300
|
+
const substitutedProp = propReceiver ? substitutions.get(propReceiver.text) : undefined
|
|
4301
|
+
const propStringInitializer = substitutedProp && ts.isIdentifier(unwrapExpression(substitutedProp)) && ordinaryStateNames.has(unwrapExpression(substitutedProp).text)
|
|
4302
|
+
if (declaration.initializer.arguments.length !== 1 || !isSerializableStateLiteral(initialArgument) && !propStringInitializer) throw sourceNodeError(declaration.initializer, component.getSourceFile(), `${hookLabel} useState() must use one directly serializable primitive, plain object, or array initial value${ordinaryHooks ? " or direct primitive state prop.toString()" : ""}; other dynamic initializers are not supported`)
|
|
4303
|
+
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(), `${hookLabel} useState() must use [state, setter] identifier destructuring`)
|
|
4304
|
+
const suffix = `${Math.max(0, call.pos)}_${ordinaryHooks ? ordinaryStates.length : rowStates.length}`
|
|
4305
|
+
const state = ordinaryHooks ? `__kComponentState${suffix}` : `__kRowState${suffix}`
|
|
4306
|
+
const setter = ordinaryHooks ? `__kComponentSetter${suffix}` : `__kRowSetter${suffix}`
|
|
4112
4307
|
substitutions.set(declaration.name.elements[0].name.text, factory.createIdentifier(state))
|
|
4113
4308
|
substitutions.set(declaration.name.elements[1].name.text, factory.createIdentifier(setter))
|
|
4114
4309
|
const binding = factory.createArrayBindingPattern([
|
|
4115
4310
|
factory.createBindingElement(undefined, undefined, factory.createIdentifier(state)),
|
|
4116
4311
|
factory.createBindingElement(undefined, undefined, factory.createIdentifier(setter))
|
|
4117
4312
|
])
|
|
4118
|
-
const initialValue = cloneAst(
|
|
4313
|
+
const initialValue = propStringInitializer ? substituteClone(initialArgument, substitutions, factory, context) : cloneAst(initialArgument, factory, context)
|
|
4119
4314
|
synthesizeTree(initialValue)
|
|
4120
|
-
const initializer = factory.createCallExpression(factory.createIdentifier("__kRowUseState"), undefined, [initialValue])
|
|
4315
|
+
const initializer = factory.createCallExpression(factory.createIdentifier(ordinaryHooks ? "__kComponentUseState" : "__kRowUseState"), undefined, [initialValue])
|
|
4121
4316
|
hookDeclarations.push(factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(binding, undefined, undefined, initializer)], ts.NodeFlags.Const)))
|
|
4122
|
-
|
|
4317
|
+
if (ordinaryHooks) ordinaryStates.push({ state, setter })
|
|
4318
|
+
else rowStates.push({ state, setter })
|
|
4123
4319
|
continue
|
|
4124
4320
|
}
|
|
4125
4321
|
if (declaration.initializer && ts.isCallExpression(declaration.initializer) && ts.isIdentifier(declaration.initializer.expression) && declaration.initializer.expression.text === "useRef") {
|
|
4126
|
-
|
|
4127
|
-
if (
|
|
4128
|
-
|
|
4322
|
+
const hookLabel = ordinaryHooks ? "Setter-callback component" : "Keyed row"
|
|
4323
|
+
if (declaration.initializer.arguments.length !== 1 || declaration.initializer.arguments[0].kind !== ts.SyntaxKind.NullKeyword) throw sourceNodeError(declaration.initializer, component.getSourceFile(), `${hookLabel} useRef() must use the direct initial value null`)
|
|
4324
|
+
if (!ts.isIdentifier(declaration.name)) throw sourceNodeError(declaration.name, component.getSourceFile(), `${hookLabel} useRef() must be assigned to one identifier`)
|
|
4325
|
+
const refs = ordinaryHooks ? ordinaryRefs : rowRefs
|
|
4326
|
+
const name = `${ordinaryHooks ? "__kComponentRef" : "__kRowRef"}${Math.max(0, call.pos)}_${refs.length}`
|
|
4327
|
+
substitutions.set(declaration.name.text, factory.createIdentifier(name))
|
|
4328
|
+
const initializer = factory.createCallExpression(factory.createIdentifier(ordinaryHooks ? "__kComponentUseRef" : "__kRowUseRef"), declaration.initializer.typeArguments?.map(type => cloneAst(type, factory, context)), [factory.createNull()])
|
|
4329
|
+
hookDeclarations.push(factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(factory.createIdentifier(name), undefined, undefined, initializer)], ts.NodeFlags.Const)))
|
|
4330
|
+
refs.push({ name })
|
|
4331
|
+
continue
|
|
4332
|
+
}
|
|
4333
|
+
if (declaration.initializer && ts.isCallExpression(declaration.initializer) && ts.isIdentifier(declaration.initializer.expression) && declaration.initializer.expression.text === "useId") {
|
|
4334
|
+
if (!ordinaryHooks) throw sourceNodeError(declaration.initializer, component.getSourceFile(), "useId() is not supported in keyed row components")
|
|
4335
|
+
if (declaration.initializer.arguments.length || !ts.isIdentifier(declaration.name)) throw sourceNodeError(declaration.initializer, component.getSourceFile(), "Setter-callback component useId() must initialize one top-level const identifier without arguments")
|
|
4336
|
+
const name = `__kComponentId${Math.max(0, call.pos)}_${hookDeclarations.length}`
|
|
4129
4337
|
substitutions.set(declaration.name.text, factory.createIdentifier(name))
|
|
4130
|
-
const initializer = factory.createCallExpression(factory.createIdentifier("
|
|
4338
|
+
const initializer = factory.createCallExpression(factory.createIdentifier("__kComponentUseId"), undefined, [])
|
|
4131
4339
|
hookDeclarations.push(factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(factory.createIdentifier(name), undefined, undefined, initializer)], ts.NodeFlags.Const)))
|
|
4132
|
-
|
|
4340
|
+
usesComponentId = true
|
|
4133
4341
|
continue
|
|
4134
4342
|
}
|
|
4135
|
-
if (declaration.initializer && ts.isCallExpression(declaration.initializer) && ts.isIdentifier(declaration.initializer.expression) && declaration.initializer.expression.text === "useId") throw sourceNodeError(declaration.initializer, component.getSourceFile(), "useId() is not supported in keyed row components")
|
|
4136
4343
|
if (!ts.isIdentifier(declaration.name) || !declaration.initializer) fail(declaration, `${label} component locals must be initialized identifiers`)
|
|
4137
4344
|
const calculation = substituteClone(declaration.initializer, substitutions, factory, context)
|
|
4138
4345
|
calculations.push({ name: declaration.name.text, expression: calculation })
|
|
@@ -4143,12 +4350,12 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
|
|
|
4143
4350
|
let unsupportedHook
|
|
4144
4351
|
const findUnsupportedHook = node => {
|
|
4145
4352
|
if (unsupportedHook) return
|
|
4146
|
-
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && ["useState", "useRef"].includes(node.expression.text)) unsupportedHook = node
|
|
4353
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && ["useState", "useRef", "useId"].includes(node.expression.text)) unsupportedHook = node
|
|
4147
4354
|
ts.forEachChild(node, findUnsupportedHook)
|
|
4148
4355
|
}
|
|
4149
4356
|
findUnsupportedHook(returned)
|
|
4150
4357
|
for (const calculation of calculations) findUnsupportedHook(calculation.expression)
|
|
4151
|
-
if (unsupportedHook) throw sourceNodeError(unsupportedHook, component.getSourceFile(),
|
|
4358
|
+
if (unsupportedHook) throw sourceNodeError(unsupportedHook, component.getSourceFile(), `${ordinaryHooks ? "Setter-callback component" : "Keyed row"} ${unsupportedHook.expression.text}() must be one top-level const declaration`)
|
|
4152
4359
|
let root = unwrapExpression(flattenForwardedComponentChildren(substituteClone(returned, substitutions, factory, context), factory, context))
|
|
4153
4360
|
if (rest) root = expandSpecializedRest(root, returned, component, rest, restEntries, factory, context, fail, label)
|
|
4154
4361
|
if (!ts.isJsxElement(root) && !ts.isJsxSelfClosingElement(root)) fail(returned, `${label} component must return one JSX element`)
|
|
@@ -4168,7 +4375,10 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
|
|
|
4168
4375
|
effects,
|
|
4169
4376
|
hookDeclarations,
|
|
4170
4377
|
rowStates,
|
|
4171
|
-
rowRefs
|
|
4378
|
+
rowRefs,
|
|
4379
|
+
ordinaryStates,
|
|
4380
|
+
ordinaryRefs,
|
|
4381
|
+
usesComponentId
|
|
4172
4382
|
}
|
|
4173
4383
|
}
|
|
4174
4384
|
|