@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 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.4 - Browser-native handlers.** Named event handlers now keep `localStorage`, `FileReader`, and `alert` in browser ESM instead of attempting to serialize them during static rendering. Read the [release notes](./RELEASES.md#084---browser-native-handlers) or open the [release page](https://kudzujs.cloud/releases/0.8.4).
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.
@@ -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. Private timer refs, timers started by unowned event callbacks, and arbitrary timed callback graphs are not supported as owned debounce patterns.
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
 
@@ -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 analysis = { callbacks, fields, states }
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)
@@ -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
- readonly current: T | null
19
+ current: T | null
20
20
  }
21
21
 
22
22
  export function useRef<T>(initialValue: null): RefObject<T>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kudzujs/core",
3
- "version": "0.8.4",
3
+ "version": "0.8.5",
4
4
  "description": "HTML-first TSX framework with synchronous state semantics and no virtual DOM",
5
5
  "type": "module",
6
6
  "license": "MIT",