@kudzujs/core 0.8.4 → 0.8.5
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 +157 -8
- package/framework/core.d.ts +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.8.x`: the compiler API and supported TSX surface may change.
|
|
12
12
|
|
|
13
|
-
**Latest release: 0.8.
|
|
13
|
+
**Latest release: 0.8.5 - Owned timer actions.** A directly returned relative custom-hook callback may own one private timeout ref with latest-only replacement and effect cleanup, without adding a timer runtime. Read the [release notes](./RELEASES.md#085---owned-timer-actions) or open the [release page](https://kudzujs.cloud/releases/0.8.5).
|
|
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.8.5 - Owned timer actions
|
|
4
|
+
|
|
5
|
+
Kudzu 0.8.5 compiles one proven private timeout ref pattern from a returned relative custom-hook action into existing state and effect ownership.
|
|
6
|
+
|
|
7
|
+
### New in 0.8.5
|
|
8
|
+
|
|
9
|
+
- One directly returned relative custom-hook callback may own one `useRef<number | null>(null)`, directly clear its previous timeout, and assign a numeric-literal-delay `setTimeout()` whose callback updates hook state.
|
|
10
|
+
- One empty-dependency effect directly clears the latest timer on cleanup. Conditional unmount cancels pending work, and remount creates fresh ownership.
|
|
11
|
+
- The compiler lowers the private ref to a collision-free hidden state slot shared by existing native-handler and effect contexts; no timer scheduler or runtime is added.
|
|
12
|
+
- Named, default-arrow, and relative re-export hook forms retain the same specialization and independent timer identities.
|
|
13
|
+
- Browser coverage verifies replacement, latest-only firing, unmount cancellation, and fresh remount. Dynamic delays fail with source diagnostics.
|
|
14
|
+
- Static siblings remain JavaScript-free, and the complete suite passes 134/134 tests.
|
|
15
|
+
|
|
16
|
+
### Boundary
|
|
17
|
+
|
|
18
|
+
The hook may own one private timeout ref with one direct returned callback, one literal delay, and one direct cleanup effect. Multiple refs, aliases, dynamic delays, intervals, keyed ownership, indirect callbacks, and arbitrary timed graphs remain unsupported.
|
|
19
|
+
|
|
20
|
+
### Upgrade
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install @kudzujs/core@^0.8.5
|
|
24
|
+
```
|
|
25
|
+
|
|
3
26
|
## 0.8.4 - Browser-native handlers
|
|
4
27
|
|
|
5
28
|
Kudzu 0.8.4 fixes named event handlers that use standard browser globals during real React application migrations.
|
package/framework/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Migration source may retain conventional `react` imports for supported named or
|
|
|
6
6
|
|
|
7
7
|
Compilation begins from page entries and follows relative runtime imports, re-exports, and validated Worker references; unreachable TypeScript migration files are not transformed. Direct maps over imported immutable JSON-safe arrays fold to literals for zero-JavaScript static rows. Synchronous relative calculation functions may return objects whose direct static fields feed reactive JSX bindings; build rendering uses current signal values and route-specific binding ESM reevaluates the same helper after state commits. One direct array field may instead feed a keyed intrinsic map: its evaluator refreshes a compiler-owned array anchor before the existing list reconciler runs, preserving keyed DOM and SVG identity without a calculation runtime. That field must remain a JSON-safe array after every source-state commit. Package imports have a separate narrow boundary: direct references inside intrinsic JSX event callbacks are erased from build modules and bundled into route handler ESM, while render-time, effect, helper-indirect, and mixed package use fails.
|
|
8
8
|
|
|
9
|
-
Native platform work remains ordinary source. A direct async handler or directly returned relative custom-hook callback may call `navigator.clipboard.writeText()` and update application-owned success/failure state; Kudzu emits only its existing route handler ESM. Debounced synchronization uses a dependency effect that creates `setTimeout()` work and directly returns `clearTimeout()` cleanup, reusing dependency, conditional, keyed, and route ownership.
|
|
9
|
+
Native platform work remains ordinary source. A direct async handler or directly returned relative custom-hook callback may call `navigator.clipboard.writeText()` and update application-owned success/failure state; Kudzu emits only its existing route handler ESM. Debounced synchronization uses a dependency effect that creates `setTimeout()` work and directly returns `clearTimeout()` cleanup, reusing dependency, conditional, keyed, and route ownership. One directly returned relative custom-hook callback may own one `null`-initialized private timeout ref when it directly clears the previous value, assigns a numeric-literal-delay `setTimeout()`, and an empty-dependency effect directly clears the timer on cleanup. Kudzu lowers that ref to compiler-owned state shared by existing handler and effect contexts. Multiple timers, dynamic delays, intervals, aliases, unowned delayed writes, and arbitrary timed callback graphs remain unsupported.
|
|
10
10
|
|
|
11
11
|
A named or aliased `Link` import from `react-router-dom` may render directly with one static root-relative `to` plus native anchor props. The compiler prefixes the configured `base`, changes the element to `<a href>`, and erases the import, so native navigation remains the default and configured navigation groups see an ordinary eligible anchor. Dynamic or relative destinations, `NavLink`, router-only props, spreads, default/namespace imports, and non-JSX uses fail with source diagnostics. No React Router package code or router runtime is emitted.
|
|
12
12
|
|
package/framework/build.mjs
CHANGED
|
@@ -2607,6 +2607,132 @@ function reactMemoReferenceNames(root) {
|
|
|
2607
2607
|
return names
|
|
2608
2608
|
}
|
|
2609
2609
|
|
|
2610
|
+
const customHookTimerStatePrefix = "__kTimerState_"
|
|
2611
|
+
const customHookTimerSetterPrefix = "__kSetTimerState_"
|
|
2612
|
+
const customHookTimerStatesBySource = new WeakMap()
|
|
2613
|
+
|
|
2614
|
+
function normalizeCustomHookTimerRefs(sourceFile, factory, context) {
|
|
2615
|
+
const timerCall = (node, name) => ts.isCallExpression(node) && (
|
|
2616
|
+
ts.isIdentifier(node.expression) && node.expression.text === name ||
|
|
2617
|
+
ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.expression) && node.expression.expression.text === "window" && node.expression.name.text === name
|
|
2618
|
+
)
|
|
2619
|
+
const currentAccess = (node, name) => ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === name && node.name.text === "current"
|
|
2620
|
+
const clearStatement = (node, name) => {
|
|
2621
|
+
if (!ts.isIfStatement(node) || node.elseStatement || !currentAccess(unwrapExpression(node.expression), name)) return undefined
|
|
2622
|
+
const statement = ts.isBlock(node.thenStatement) && node.thenStatement.statements.length === 1 ? node.thenStatement.statements[0] : node.thenStatement
|
|
2623
|
+
if (!ts.isExpressionStatement(statement) || !timerCall(statement.expression, "clearTimeout") || statement.expression.arguments.length !== 1 || !currentAccess(unwrapExpression(statement.expression.arguments[0]), name)) return undefined
|
|
2624
|
+
return { condition: unwrapExpression(node.expression), argument: unwrapExpression(statement.expression.arguments[0]) }
|
|
2625
|
+
}
|
|
2626
|
+
const analyze = (hook, hookName) => {
|
|
2627
|
+
if (!/^use[A-Z]/.test(hookName) || hook.parameters.length || !hook.body || !ts.isBlock(hook.body)) return undefined
|
|
2628
|
+
const returnedStatement = hook.body.statements.at(-1)
|
|
2629
|
+
const returned = returnedStatement && ts.isReturnStatement(returnedStatement) && returnedStatement.expression ? unwrapExpression(returnedStatement.expression) : undefined
|
|
2630
|
+
if (!returned || !ts.isObjectLiteralExpression(returned)) return undefined
|
|
2631
|
+
const returnedNames = new Set(returned.properties.filter(ts.isShorthandPropertyAssignment).map(property => property.name.text))
|
|
2632
|
+
const callbacks = new Map()
|
|
2633
|
+
const refs = []
|
|
2634
|
+
for (const statement of hook.body.statements) {
|
|
2635
|
+
if (!ts.isVariableStatement(statement) || !(statement.declarationList.flags & ts.NodeFlags.Const)) continue
|
|
2636
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
2637
|
+
if (ts.isIdentifier(declaration.name) && declaration.initializer && (ts.isArrowFunction(declaration.initializer) || ts.isFunctionExpression(declaration.initializer))) callbacks.set(declaration.name.text, declaration.initializer)
|
|
2638
|
+
if (ts.isIdentifier(declaration.name) && declaration.initializer && ts.isCallExpression(declaration.initializer) && ts.isIdentifier(declaration.initializer.expression) && declaration.initializer.expression.text === "useRef" && declaration.initializer.arguments.length === 1 && declaration.initializer.arguments[0].kind === ts.SyntaxKind.NullKeyword) refs.push({ declaration, name: declaration.name.text })
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
const candidates = []
|
|
2642
|
+
for (const ref of refs) {
|
|
2643
|
+
const assignments = []
|
|
2644
|
+
const accesses = []
|
|
2645
|
+
const clearStatements = []
|
|
2646
|
+
const collect = node => {
|
|
2647
|
+
if (currentAccess(node, ref.name)) accesses.push(node)
|
|
2648
|
+
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken && currentAccess(unwrapExpression(node.left), ref.name)) assignments.push(node)
|
|
2649
|
+
const clear = clearStatement(node, ref.name)
|
|
2650
|
+
if (clear) clearStatements.push({ node, ...clear })
|
|
2651
|
+
ts.forEachChild(node, collect)
|
|
2652
|
+
}
|
|
2653
|
+
collect(hook.body)
|
|
2654
|
+
if (assignments.some(assignment => timerCall(unwrapExpression(assignment.right), "setTimeout"))) candidates.push({ ...ref, assignments, accesses, clearStatements })
|
|
2655
|
+
}
|
|
2656
|
+
if (!candidates.length) return undefined
|
|
2657
|
+
if (candidates.length !== 1) throw sourceNodeError(hook, sourceFile, "Relative custom hooks may own only one private timeout ref")
|
|
2658
|
+
const timer = candidates[0]
|
|
2659
|
+
if (timer.assignments.length !== 1) throw sourceNodeError(timer.declaration, sourceFile, "Private timeout refs require one direct timer.current = setTimeout(...) assignment")
|
|
2660
|
+
const assignment = timer.assignments[0]
|
|
2661
|
+
const timeout = unwrapExpression(assignment.right)
|
|
2662
|
+
const timeoutCallback = timeout.arguments[0]
|
|
2663
|
+
const delay = timeout.arguments[1]
|
|
2664
|
+
if (!timerCall(timeout, "setTimeout") || timeout.arguments.length !== 2 || !timeoutCallback || !(ts.isArrowFunction(timeoutCallback) || ts.isFunctionExpression(timeoutCallback)) || timeoutCallback.parameters.length || !delay || !ts.isNumericLiteral(unwrapExpression(delay))) throw sourceNodeError(assignment, sourceFile, "Private timeout refs require setTimeout() with one zero-argument callback and a numeric literal delay")
|
|
2665
|
+
const callback = nearestFunction(assignment)
|
|
2666
|
+
const callbackName = [...callbacks].find(([, value]) => value === callback)?.[0]
|
|
2667
|
+
if (!callbackName || !returnedNames.has(callbackName) || !ts.isBlock(callback.body) || !ts.isExpressionStatement(assignment.parent) || assignment.parent.parent !== callback.body) throw sourceNodeError(assignment, sourceFile, "Private timeout refs must be assigned directly inside one returned custom-hook callback")
|
|
2668
|
+
const callbackClear = timer.clearStatements.find(entry => nearestFunction(entry.node) === callback)
|
|
2669
|
+
if (!callbackClear || callbackClear.node.parent !== callback.body || callback.body.statements.indexOf(callbackClear.node) >= callback.body.statements.indexOf(assignment.parent)) throw sourceNodeError(callback, sourceFile, "Private timeout callbacks must directly clear the previous timer before assigning its replacement")
|
|
2670
|
+
const effectCalls = hook.body.statements.flatMap(statement => {
|
|
2671
|
+
if (!ts.isExpressionStatement(statement) || !ts.isCallExpression(statement.expression) || !ts.isIdentifier(statement.expression.expression) || statement.expression.expression.text !== "useEffect") return []
|
|
2672
|
+
return [statement.expression]
|
|
2673
|
+
})
|
|
2674
|
+
let cleanupClear
|
|
2675
|
+
for (const effect of effectCalls) {
|
|
2676
|
+
const [setup, dependencies] = effect.arguments
|
|
2677
|
+
if (!(ts.isArrowFunction(setup) || ts.isFunctionExpression(setup)) || !ts.isArrayLiteralExpression(dependencies) || dependencies.elements.length) continue
|
|
2678
|
+
const returns = effectReturns(setup)
|
|
2679
|
+
if (returns.cleanups.length !== 1) continue
|
|
2680
|
+
const cleanup = returns.cleanups[0]
|
|
2681
|
+
const entry = timer.clearStatements.find(candidate => nearestFunction(candidate.node) === cleanup)
|
|
2682
|
+
if (entry && ts.isBlock(cleanup.body) && cleanup.body.statements.length === 1 && cleanup.body.statements[0] === entry.node) cleanupClear = entry
|
|
2683
|
+
}
|
|
2684
|
+
if (!cleanupClear) throw sourceNodeError(timer.declaration, sourceFile, "Private timeout refs require one empty-dependency effect that directly clears the timer on cleanup")
|
|
2685
|
+
const accepted = new Set([assignment.left, callbackClear.condition, callbackClear.argument, cleanupClear.condition, cleanupClear.argument].map(unwrapExpression))
|
|
2686
|
+
const unsupported = timer.accesses.find(access => !accepted.has(access))
|
|
2687
|
+
if (unsupported) throw sourceNodeError(unsupported, sourceFile, "Private timeout refs may only be read by their direct replacement and cleanup guards")
|
|
2688
|
+
const identity = createHash("sha256").update(`${sourceFile.fileName}:${hook.pos}:${timer.name}`).digest("hex").slice(0, 10)
|
|
2689
|
+
const stateName = `${customHookTimerStatePrefix}${identity}`
|
|
2690
|
+
const setterName = `${customHookTimerSetterPrefix}${identity}`
|
|
2691
|
+
if (referencesIdentifier(hook.body, stateName) || referencesIdentifier(hook.body, setterName)) throw sourceNodeError(timer.declaration, sourceFile, "Private timeout ref conflicts with compiler-owned bindings")
|
|
2692
|
+
return { assignment, declaration: timer.declaration, refName: timer.name, returned, stateName, setterName }
|
|
2693
|
+
}
|
|
2694
|
+
const timerStates = new Set()
|
|
2695
|
+
const transform = (hook, hookName) => {
|
|
2696
|
+
const timer = analyze(hook, hookName)
|
|
2697
|
+
if (!timer) return undefined
|
|
2698
|
+
timerStates.add(timer.stateName)
|
|
2699
|
+
const timerVisitor = current => {
|
|
2700
|
+
if (current === timer.declaration) {
|
|
2701
|
+
const binding = factory.createArrayBindingPattern([
|
|
2702
|
+
factory.createBindingElement(undefined, undefined, timer.stateName),
|
|
2703
|
+
factory.createBindingElement(undefined, undefined, timer.setterName)
|
|
2704
|
+
])
|
|
2705
|
+
const initializer = factory.updateCallExpression(current.initializer, factory.createIdentifier("useState"), current.initializer.typeArguments, current.initializer.arguments)
|
|
2706
|
+
return factory.updateVariableDeclaration(current, binding, current.exclamationToken, undefined, initializer)
|
|
2707
|
+
}
|
|
2708
|
+
if (current === timer.assignment) return factory.createCallExpression(factory.createIdentifier(timer.setterName), undefined, [ts.visitNode(current.right, timerVisitor)])
|
|
2709
|
+
if (currentAccess(current, timer.refName)) return factory.createIdentifier(timer.stateName)
|
|
2710
|
+
if (current === timer.returned) return factory.updateObjectLiteralExpression(current, [
|
|
2711
|
+
...current.properties,
|
|
2712
|
+
factory.createShorthandPropertyAssignment(timer.stateName),
|
|
2713
|
+
factory.createShorthandPropertyAssignment(timer.setterName)
|
|
2714
|
+
])
|
|
2715
|
+
return ts.visitEachChild(current, timerVisitor, context)
|
|
2716
|
+
}
|
|
2717
|
+
return ts.visitEachChild(hook, timerVisitor, context)
|
|
2718
|
+
}
|
|
2719
|
+
const visitor = node => {
|
|
2720
|
+
if (ts.isFunctionDeclaration(node)) {
|
|
2721
|
+
const hookName = node.name?.text ?? (node.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.DefaultKeyword) ? "useDefault" : "")
|
|
2722
|
+
const transformed = transform(node, hookName)
|
|
2723
|
+
if (transformed) return transformed
|
|
2724
|
+
}
|
|
2725
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && (ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer))) {
|
|
2726
|
+
const transformed = transform(node.initializer, node.name.text)
|
|
2727
|
+
if (transformed) return factory.updateVariableDeclaration(node, node.name, node.exclamationToken, node.type, transformed)
|
|
2728
|
+
}
|
|
2729
|
+
return ts.visitEachChild(node, visitor, context)
|
|
2730
|
+
}
|
|
2731
|
+
const normalized = ts.visitNode(sourceFile, visitor)
|
|
2732
|
+
customHookTimerStatesBySource.set(normalized, timerStates)
|
|
2733
|
+
return normalized
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2610
2736
|
function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings, listExpressions, handlerUrl, file, sourceFiles, sourceIndex, staticFiles, importedAssets, cssModules, base, clientImports, workerReferences) {
|
|
2611
2737
|
return context => sourceFile => {
|
|
2612
2738
|
const factory = context.factory
|
|
@@ -2621,6 +2747,9 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2621
2747
|
ts.setParentRecursive(sourceFile, false)
|
|
2622
2748
|
sourceFile = normalizeReactMigrationSyntax(sourceFile, factory, context, importedCollections)
|
|
2623
2749
|
ts.setParentRecursive(sourceFile, false)
|
|
2750
|
+
sourceFile = normalizeCustomHookTimerRefs(sourceFile, factory, context)
|
|
2751
|
+
const customHookTimerStates = customHookTimerStatesBySource.get(sourceFile) ?? new Set()
|
|
2752
|
+
ts.setParentRecursive(sourceFile, false)
|
|
2624
2753
|
validateUseIdSyntax(sourceFile)
|
|
2625
2754
|
sourceFile = normalizeLazyStateInitializers(sourceFile, factory, context, file, sourceFiles, sourceIndex)
|
|
2626
2755
|
ts.setParentRecursive(sourceFile, false)
|
|
@@ -2638,6 +2767,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2638
2767
|
}
|
|
2639
2768
|
const hasUseEffectImport = sourceFile.statements.some(statement => ts.isImportDeclaration(statement) && ["@kudzujs/core", "react"].includes(statement.moduleSpecifier.text) && statement.importClause?.namedBindings && ts.isNamedImports(statement.importClause.namedBindings) && statement.importClause.namedBindings.elements.some(entry => !entry.propertyName && entry.name.text === "useEffect"))
|
|
2640
2769
|
const importedSources = new Map()
|
|
2770
|
+
const importedTimerStates = new Map()
|
|
2641
2771
|
const importedSource = target => {
|
|
2642
2772
|
let imported = importedSources.get(target)
|
|
2643
2773
|
if (!imported) {
|
|
@@ -2647,6 +2777,9 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2647
2777
|
ts.setParentRecursive(imported, false)
|
|
2648
2778
|
imported = normalizeReactMigrationSyntax(imported, factory, context, importedSerializableCollectionNames(imported, target, sourceFiles, sourceIndex))
|
|
2649
2779
|
ts.setParentRecursive(imported, false)
|
|
2780
|
+
imported = normalizeCustomHookTimerRefs(imported, factory, context)
|
|
2781
|
+
importedTimerStates.set(target, customHookTimerStatesBySource.get(imported) ?? new Set())
|
|
2782
|
+
ts.setParentRecursive(imported, false)
|
|
2650
2783
|
validateUseIdSyntax(imported)
|
|
2651
2784
|
imported = normalizeLazyStateInitializers(imported, factory, context, target, sourceFiles, sourceIndex)
|
|
2652
2785
|
ts.setParentRecursive(imported, false)
|
|
@@ -2681,6 +2814,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2681
2814
|
}
|
|
2682
2815
|
const functions = new Map()
|
|
2683
2816
|
const customHookFunctionsByOwner = new Map()
|
|
2817
|
+
const customHookPrivateFields = new WeakMap()
|
|
2684
2818
|
const components = new Map()
|
|
2685
2819
|
const contexts = new Set()
|
|
2686
2820
|
const customHooks = new Map()
|
|
@@ -2739,7 +2873,8 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2739
2873
|
const capture = nativeCaptureNames(callback, states).values().next().value
|
|
2740
2874
|
if (capture) throw sourceNodeError(callback, hookSource, `Relative custom hook callback ${JSON.stringify(name)} cannot capture private binding ${JSON.stringify(capture)}`)
|
|
2741
2875
|
}
|
|
2742
|
-
const
|
|
2876
|
+
const privateStates = new Set([...states.values()].filter(state => importedTimerStates.get(hookSource.fileName)?.has(state)))
|
|
2877
|
+
const analysis = { callbacks, fields, privateStates, states }
|
|
2743
2878
|
customHooks.set(key, analysis)
|
|
2744
2879
|
return analysis
|
|
2745
2880
|
}
|
|
@@ -2761,6 +2896,13 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2761
2896
|
if (!owner) throw sourceNodeError(node, sourceFile, "Relative custom hooks cannot be used outside a Kudzu component")
|
|
2762
2897
|
const setters = settersByFunction.get(owner) ?? new Map()
|
|
2763
2898
|
for (const [setter, state] of hook.states) {
|
|
2899
|
+
if (hook.privateStates.has(state)) {
|
|
2900
|
+
setters.set(setter, state)
|
|
2901
|
+
const fields = customHookPrivateFields.get(node) ?? []
|
|
2902
|
+
fields.push(state, setter)
|
|
2903
|
+
customHookPrivateFields.set(node, fields)
|
|
2904
|
+
continue
|
|
2905
|
+
}
|
|
2764
2906
|
if (names.has(setter) !== names.has(state)) throw sourceNodeError(node.name, sourceFile, `Relative custom hook state ${JSON.stringify(state)} and setter ${JSON.stringify(setter)} must be destructured together`)
|
|
2765
2907
|
if (names.has(setter)) setters.set(setter, state)
|
|
2766
2908
|
}
|
|
@@ -3543,6 +3685,13 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
3543
3685
|
}
|
|
3544
3686
|
|
|
3545
3687
|
const visitor = node => {
|
|
3688
|
+
if (ts.isVariableDeclaration(node) && ts.isObjectBindingPattern(node.name) && customHookPrivateFields.has(node)) {
|
|
3689
|
+
const privateFields = customHookPrivateFields.get(node)
|
|
3690
|
+
return factory.updateVariableDeclaration(node, factory.updateObjectBindingPattern(node.name, [
|
|
3691
|
+
...node.name.elements,
|
|
3692
|
+
...privateFields.map(name => factory.createBindingElement(undefined, undefined, name))
|
|
3693
|
+
]), node.exclamationToken, node.type, node.initializer)
|
|
3694
|
+
}
|
|
3546
3695
|
if (ts.isBlock(node) && setterHookHelpers.has(node)) {
|
|
3547
3696
|
return ts.visitEachChild(factory.updateBlock(node, [...setterHookHelpers.get(node), ...node.statements]), visitor, context)
|
|
3548
3697
|
}
|
|
@@ -3686,7 +3835,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
3686
3835
|
} else {
|
|
3687
3836
|
compiledCallback = rewriteEffectWorkers(compiledCallback, callbackFile, callbackSource, sourceFiles, workerReferences, factory, context)
|
|
3688
3837
|
}
|
|
3689
|
-
const descriptor = compileNativeCallback(compiledCallback, setters, reducersForNode(node, reducersByFunction), factory, effectHandlers, specializedEffect?.imports ?? importBindings, clientImports, "effect", dependencyItem, true, returns.cleanup)
|
|
3838
|
+
const descriptor = compileNativeCallback(compiledCallback, setters, reducersForNode(node, reducersByFunction), factory, effectHandlers, specializedEffect?.imports ?? importBindings, clientImports, "effect", dependencyItem, true, returns.cleanup, customHookTimerStates)
|
|
3690
3839
|
for (const reference of workerReferences.slice(workerStart)) Object.assign(reference, { module: handlerUrl, handler: descriptor.exportName })
|
|
3691
3840
|
usesListItem ||= Boolean(itemDependencies.length && !listEffect)
|
|
3692
3841
|
usesBehavior = true
|
|
@@ -5058,7 +5207,7 @@ function compileEvent(expression, setters, reducers, functions, factory, nativeH
|
|
|
5058
5207
|
])
|
|
5059
5208
|
}
|
|
5060
5209
|
|
|
5061
|
-
function compileNativeCallback(expression, setters, reducers, factory, entries, importBindings, clientImports, prefix, listItem, deferValues = false, snapshotNested = false) {
|
|
5210
|
+
function compileNativeCallback(expression, setters, reducers, factory, entries, importBindings, clientImports, prefix, listItem, deferValues = false, snapshotNested = false, liveStates = new Set()) {
|
|
5062
5211
|
const allCaptures = nativeCaptureNames(expression, setters)
|
|
5063
5212
|
const usedReducers = referencedReducerDispatches(expression.body, reducers, expression)
|
|
5064
5213
|
const imports = [...referencedImportedBindings(expression, importBindings)].map(name => importBindings.get(name))
|
|
@@ -5067,7 +5216,7 @@ function compileNativeCallback(expression, setters, reducers, factory, entries,
|
|
|
5067
5216
|
for (const entry of imports) if (!entry.package) clientImports.add(entry.target)
|
|
5068
5217
|
const usedStates = nativeStateNames(expression, setters)
|
|
5069
5218
|
const exportName = `${prefix}${entries.length}`
|
|
5070
|
-
entries.push({ exportName, expression, captures, imports, setters: new Map([...setters].filter(([, state]) => usedStates.has(state))), reducers: new Map([...reducers].filter(([name]) => usedReducers.has(name))), snapshotNested })
|
|
5219
|
+
entries.push({ exportName, expression, captures, imports, liveStates, setters: new Map([...setters].filter(([, state]) => usedStates.has(state))), reducers: new Map([...reducers].filter(([name]) => usedReducers.has(name))), snapshotNested })
|
|
5071
5220
|
const value = name => deferValues
|
|
5072
5221
|
? factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), factory.createIdentifier(name))
|
|
5073
5222
|
: factory.createIdentifier(name)
|
|
@@ -5937,10 +6086,10 @@ function relativeModulePath(from, to) {
|
|
|
5937
6086
|
return path.startsWith(".") ? path : `./${path}`
|
|
5938
6087
|
}
|
|
5939
6088
|
|
|
5940
|
-
function printNativeHandler({ exportName, expression, captures, setters, reducers = new Map(), snapshotNested }) {
|
|
6089
|
+
function printNativeHandler({ exportName, expression, captures, setters, reducers = new Map(), snapshotNested, liveStates = new Set() }) {
|
|
5941
6090
|
const factory = ts.factory
|
|
5942
6091
|
const stateNames = new Set(setters.values())
|
|
5943
|
-
const snapshotNames = snapshotNested ? nestedStateNames(expression, setters) : new Set()
|
|
6092
|
+
const snapshotNames = snapshotNested ? nestedStateNames(expression, setters, liveStates) : new Set()
|
|
5944
6093
|
const snapshots = new Map([...snapshotNames].map(name => [name, factory.createUniqueName("__kEffectState")]))
|
|
5945
6094
|
const captureSnapshotNames = snapshotNested ? nestedCaptureNames(expression, captures) : new Set()
|
|
5946
6095
|
const captureSnapshots = new Map([...captureSnapshotNames].map(name => [name, factory.createUniqueName("__kEffectCapture")]))
|
|
@@ -6037,11 +6186,11 @@ function nestedCaptureNames(expression, captures) {
|
|
|
6037
6186
|
return names
|
|
6038
6187
|
}
|
|
6039
6188
|
|
|
6040
|
-
function nestedStateNames(expression, setters) {
|
|
6189
|
+
function nestedStateNames(expression, setters, liveStates = new Set()) {
|
|
6041
6190
|
const states = new Set(setters.values())
|
|
6042
6191
|
const names = new Set()
|
|
6043
6192
|
const visit = node => {
|
|
6044
|
-
if (ts.isIdentifier(node) && states.has(node.text) && isReferenceIdentifier(node) && insideNestedFunction(node, expression) && !isShadowedIdentifier(node, expression)) names.add(node.text)
|
|
6193
|
+
if (ts.isIdentifier(node) && states.has(node.text) && !liveStates.has(node.text) && isReferenceIdentifier(node) && insideNestedFunction(node, expression) && !isShadowedIdentifier(node, expression)) names.add(node.text)
|
|
6045
6194
|
ts.forEachChild(node, visit)
|
|
6046
6195
|
}
|
|
6047
6196
|
visit(expression.body)
|
package/framework/core.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export function useSearchParam(name: string): string | null
|
|
|
16
16
|
export function useSearchParamsWriter(): [undefined, undefined]
|
|
17
17
|
|
|
18
18
|
export interface RefObject<T> {
|
|
19
|
-
|
|
19
|
+
current: T | null
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export function useRef<T>(initialValue: null): RefObject<T>
|