@kudzujs/core 0.8.12 → 0.8.14

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
@@ -14,7 +14,7 @@ Kudzu compiles ordinary React-shaped TypeScript and TSX into complete static HTM
14
14
 
15
15
  > Experimental `0.8.x`: the compiler API and supported TSX surface may change.
16
16
 
17
- **Latest release: 0.8.12 - Static icon migration.** Used Lucide-shaped icons move to relative TSX and compile to accessible static SVG while build-folded evaluator JavaScript disappears from deploy output. Read the [release notes](./RELEASES.md#0812---static-icon-migration) or open the [release page](https://kudzujs.cloud/releases/0.8.12).
17
+ **Latest release: 0.8.14 - Localized blog migration.** Browser locale entry, prefixed native links, static MDX with interactive copy/tabs, and continuous canvas animation compile without React, eval, a package router, or a canvas runtime. Read the [release notes](./RELEASES.md#0814---localized-blog-migration) or open the [release page](https://kudzujs.cloud/releases/0.8.14).
18
18
 
19
19
  - [Documentation](https://kudzujs.cloud/docs)
20
20
  - [Installation guide](https://kudzujs.cloud/docs#install)
package/RELEASES.md CHANGED
@@ -1,5 +1,64 @@
1
1
  # Kudzu Releases
2
2
 
3
+ ## 0.8.14 - Localized blog migration
4
+
5
+ Kudzu 0.8.14 proves a localized MDX blog and continuous imperative canvas can migrate without React, request-time i18n, eval, or a canvas runtime.
6
+
7
+ ### New in 0.8.14
8
+
9
+ - Build-known MDX emits complete static article HTML without `eval()` or `new Function()`.
10
+ - Locale-prefixed `/ko` and `/en` routes build through `getStaticPaths()`, and relative Link replacements automatically prefix native hrefs from the build-known locale.
11
+ - The root route selects a stored locale or reads `navigator.languages`, then preserves query and hash through native `location.replace()`.
12
+ - MDX copy blocks and tabs compile through existing clipboard, state, binding, and conditional capabilities.
13
+ - One `null`-initialized canvas ref may feed an inline effect whose local state survives recursive animation frames, visibility changes, and native input listeners.
14
+ - Bare `IntersectionObserver` and `performance` remain browser globals in route-specific effect ESM.
15
+ - Static sibling routes remain complete zero-JavaScript HTML.
16
+
17
+ ### Fixed in 0.8.14
18
+
19
+ - Effect-owned `IntersectionObserver` instances must disconnect in cleanup.
20
+ - Local animation-frame handles assigned by effects must be cancelled in cleanup.
21
+ - The complete suite passes 151/151 tests.
22
+
23
+ ### Boundary
24
+
25
+ This release does not execute `next-intl`, add request-time `Accept-Language` negotiation, general mutable refs, callbacks shared across effects or handlers, a canvas runtime, or arbitrary MDX component execution. Request-time locale redirects remain host/edge configuration; resource-private drawing state and listeners stay inside one owning effect.
26
+
27
+ ### Upgrade
28
+
29
+ ```bash
30
+ npm install @kudzujs/core@^0.8.14
31
+ ```
32
+
33
+ ## 0.8.13 - Browser capability migration
34
+
35
+ Kudzu 0.8.13 proves browser-owned migration paths from Memos, Excalidraw, and Cal.com without adding React, general external-store compatibility, or browser capability runtimes.
36
+
37
+ ### New in 0.8.13
38
+
39
+ - Memos-shaped scroll spies retain an effect-owned `useRef(0)` animation-frame handle with burst coalescing, exact cleanup, smooth navigation, and active-heading ARIA.
40
+ - Excalidraw-shaped `"share" in navigator` conditions emit false static fallback and mount Web Share DOM and handlers only in supporting browsers.
41
+ - Cal.com-shaped static `useSyncExternalStore` media queries emit desktop-first HTML and lower matching `change` subscriptions into owned effects.
42
+ - Direct Web Share and clipboard actions preserve application-owned success and failure state without package adapters.
43
+ - Static sibling routes remain complete zero-JavaScript HTML for every migration fixture.
44
+
45
+ ### Fixed in 0.8.13
46
+
47
+ - Build-time Node `navigator` availability can no longer permanently fold browser capability UI to the wrong branch.
48
+ - Optimized state-select text bindings now register the same direct state commit path as optimized attributes.
49
+ - Animation-frame refs reject aliases, shadowed bindings, inverted guards, indirect reset/cancellation, repeated scheduling, and missing cleanup with source locations.
50
+ - The complete suite passes 149/149 tests.
51
+
52
+ ### Boundary
53
+
54
+ This release does not add general mutable refs, external stores, media hooks, browser-expression rendering, animation scheduling, collaboration transport, QR generation, or React package execution. Dynamic capability names, `navigator.canShare()`, parameterized/imported media hooks, dynamic queries, and arbitrary store snapshots remain source migration work.
55
+
56
+ ### Upgrade
57
+
58
+ ```bash
59
+ npm install @kudzujs/core@^0.8.13
60
+ ```
61
+
3
62
  ## 0.8.12 - Static icon migration
4
63
 
5
64
  Kudzu 0.8.12 proves a source migration path from Lucide React-shaped icon usage to accessible static SVG without a package factory or browser runtime.
@@ -6,7 +6,15 @@ 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. 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.
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. An ordinary effect may instead exclusively own `useRef(0)` as one animation-frame handle when a local callback resets it, one scheduler assigns `requestAnimationFrame()`, and cleanup cancels the pending frame; Kudzu lowers it to the existing effect scope without an animation runtime. Multiple timers, dynamic delays, intervals, ref aliases, cross-effect frame handles, unowned delayed writes, and arbitrary timed callback graphs remain unsupported.
10
+
11
+ Imperative canvas migrations use the same effect ownership rather than a component or canvas runtime. One `null`-initialized canvas DOM ref may feed an inline effect whose local variables persist across a recursive animation-frame callback, an `IntersectionObserver`, and native canvas/window listeners; the returned cleanup must cancel the latest frame, disconnect the observer, and remove every listener. Bare `IntersectionObserver` and `performance` identifiers remain browser globals in emitted effect ESM. Component-level mutable value refs and callbacks shared across effects or JSX handlers remain unsupported; move resource-private state and listeners into the owning effect.
12
+
13
+ Localized static migrations do not need a package router. `getStaticPaths()` emits each build-known locale, a small relative Link replacement prefixes native hrefs from the locale prop, and an optional `/` mount effect may choose a stored locale or inspect `navigator.languages` before calling `location.replace()` with the original query and hash. This preserves automatic browser-language entry and locale-prefixed navigation; request-time `Accept-Language` negotiation still requires host or edge configuration. Build-known MDX becomes static `dangerouslySetInnerHTML`, while copy controls, tabs, and other supported interactive components remain ordinary state and handler capabilities beside that HTML.
14
+
15
+ A top-level component local shaped exactly as `const supported = "literal" in navigator` may control one direct `supported && <Branch />` JSX site. Kudzu emits the branch as false static fallback, evaluates the browser capability once through an existing mount effect, and uses existing state-conditional ownership to mount its DOM and handlers only when supported. The value cannot escape or feed aliases, composed expressions, ternaries, dynamic properties, `canShare()`, or other browser globals.
16
+
17
+ A direct top-level `useSyncExternalStore()` may represent one static `window.matchMedia()` boolean when its three inline callbacks add/remove the same `change` listener, read the same literal query, and return `false` for the server snapshot. Kudzu emits false static fallback state and lowers the subscription to an existing owned effect. This is not general external-store support: imported/parameterized media hooks, dynamic queries, legacy listeners, and arbitrary snapshots remain diagnosed.
10
18
 
11
19
  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
20
 
@@ -104,6 +104,13 @@ function mountBindings(root) {
104
104
  mountedBindings.add(node)
105
105
  const registrations = []
106
106
  bindingRegistrations.set(node, registrations)
107
+ if (descriptor.state) {
108
+ const binding = { node, target: "text", read: Object.hasOwn(descriptor, "truthy") ? () => browserState.get(descriptor.state) ? descriptor.truthy : descriptor.falsy : () => browserState.get(descriptor.state) }
109
+ register(bindingTargets, descriptor.state, binding)
110
+ registrations.push([descriptor.state, binding])
111
+ patchBinding(node, "text", binding.read())
112
+ continue
113
+ }
107
114
  loadEvaluator(descriptor).then(evaluator => {
108
115
  if (!node.isConnected) return
109
116
  const binding = { node, target: "text", read: evaluator.read }
@@ -2745,6 +2745,287 @@ function normalizeCustomHookTimerRefs(sourceFile, factory, context) {
2745
2745
  return normalized
2746
2746
  }
2747
2747
 
2748
+ function normalizeMediaQueryExternalStores(sourceFile, factory, context) {
2749
+ const imports = sourceFile.statements.filter(statement => ts.isImportDeclaration(statement) && !statement.importClause?.isTypeOnly && ts.isStringLiteral(statement.moduleSpecifier) && statement.moduleSpecifier.text === "react" && statement.importClause?.namedBindings && ts.isNamedImports(statement.importClause.namedBindings))
2750
+ const externalStoreImport = imports.flatMap(statement => statement.importClause.namedBindings.elements.map(entry => ({ entry, statement }))).find(({ entry }) => !entry.isTypeOnly && !entry.propertyName && entry.name.text === "useSyncExternalStore")
2751
+ if (!externalStoreImport) return sourceFile
2752
+ const returnedExpression = callback => {
2753
+ if (!(ts.isArrowFunction(callback) || ts.isFunctionExpression(callback)) || callback.parameters.length) return undefined
2754
+ if (!ts.isBlock(callback.body)) return unwrapExpression(callback.body)
2755
+ if (callback.body.statements.length !== 1 || !ts.isReturnStatement(callback.body.statements[0]) || !callback.body.statements[0].expression) return undefined
2756
+ return unwrapExpression(callback.body.statements[0].expression)
2757
+ }
2758
+ const matchMediaQuery = expression => {
2759
+ expression = unwrapExpression(expression)
2760
+ if (!ts.isCallExpression(expression) || expression.arguments.length !== 1 || !ts.isStringLiteral(unwrapExpression(expression.arguments[0])) || !ts.isPropertyAccessExpression(expression.expression) || !ts.isIdentifier(expression.expression.expression) || expression.expression.expression.text !== "window" || expression.expression.name.text !== "matchMedia" || !isUnshadowedGlobal(expression.expression.expression, sourceFile)) return undefined
2761
+ return unwrapExpression(expression.arguments[0]).text
2762
+ }
2763
+ const mediaListener = (statement, method, media, callback) => ts.isExpressionStatement(statement) && ts.isCallExpression(statement.expression) && statement.expression.arguments.length === 2 && ts.isPropertyAccessExpression(statement.expression.expression) && ts.isIdentifier(statement.expression.expression.expression) && statement.expression.expression.expression.text === media && statement.expression.expression.name.text === method && ts.isStringLiteral(unwrapExpression(statement.expression.arguments[0])) && unwrapExpression(statement.expression.arguments[0]).text === "change" && ts.isIdentifier(unwrapExpression(statement.expression.arguments[1])) && unwrapExpression(statement.expression.arguments[1]).text === callback
2764
+ const candidates = new Map()
2765
+ let index = 0
2766
+ const inspect = node => {
2767
+ if (!ts.isVariableStatement(node) || !(node.declarationList.flags & ts.NodeFlags.Const) || node.declarationList.declarations.length !== 1) {
2768
+ ts.forEachChild(node, inspect)
2769
+ return
2770
+ }
2771
+ const declaration = node.declarationList.declarations[0]
2772
+ const call = declaration.initializer && unwrapExpression(declaration.initializer)
2773
+ if (!ts.isIdentifier(declaration.name) || !call || !ts.isCallExpression(call) || !ts.isIdentifier(call.expression) || call.expression.text !== "useSyncExternalStore" || isShadowedIdentifier(call.expression, sourceFile)) {
2774
+ ts.forEachChild(node, inspect)
2775
+ return
2776
+ }
2777
+ if (call.arguments.length !== 3) throw sourceNodeError(call, sourceFile, "Media query useSyncExternalStore() requires subscribe, browser snapshot, and false server snapshot callbacks")
2778
+ const [subscribe, snapshot, serverSnapshot] = call.arguments.map(unwrapExpression)
2779
+ if (!(ts.isArrowFunction(subscribe) || ts.isFunctionExpression(subscribe)) || subscribe.parameters.length !== 1 || !ts.isIdentifier(subscribe.parameters[0].name) || !ts.isBlock(subscribe.body)) throw sourceNodeError(subscribe, sourceFile, "Media query subscriptions require one inline callback parameter and block body")
2780
+ if (subscribe.body.statements.length !== 3) throw sourceNodeError(subscribe, sourceFile, "Media query subscriptions must add and remove one matching change listener")
2781
+ const callback = subscribe.parameters[0].name.text
2782
+ const [mediaStatement, addStatement, returnStatement] = subscribe.body.statements
2783
+ const mediaDeclaration = ts.isVariableStatement(mediaStatement) && (mediaStatement.declarationList.flags & ts.NodeFlags.Const) && mediaStatement.declarationList.declarations.length === 1 ? mediaStatement.declarationList.declarations[0] : undefined
2784
+ const media = mediaDeclaration && ts.isIdentifier(mediaDeclaration.name) ? mediaDeclaration.name.text : undefined
2785
+ const query = mediaDeclaration?.initializer && matchMediaQuery(mediaDeclaration.initializer)
2786
+ const cleanup = ts.isReturnStatement(returnStatement) && returnStatement.expression ? unwrapExpression(returnStatement.expression) : undefined
2787
+ const cleanupStatement = cleanup && (ts.isArrowFunction(cleanup) || ts.isFunctionExpression(cleanup)) && !cleanup.parameters.length
2788
+ ? ts.isBlock(cleanup.body) ? cleanup.body.statements.length === 1 ? cleanup.body.statements[0] : undefined : factory.createExpressionStatement(cleanup.body)
2789
+ : undefined
2790
+ if (!media || !query || !mediaListener(addStatement, "addEventListener", media, callback) || !cleanupStatement || !mediaListener(cleanupStatement, "removeEventListener", media, callback)) throw sourceNodeError(subscribe, sourceFile, "Media query subscriptions must add and remove one matching change listener")
2791
+ const snapshotValue = returnedExpression(snapshot)
2792
+ const snapshotQuery = snapshotValue && ts.isPropertyAccessExpression(snapshotValue) && snapshotValue.name.text === "matches" ? matchMediaQuery(snapshotValue.expression) : undefined
2793
+ const serverValue = returnedExpression(serverSnapshot)
2794
+ if (snapshotQuery !== query || !serverValue || serverValue.kind !== ts.SyntaxKind.FalseKeyword) throw sourceNodeError(call, sourceFile, "Media query external stores require matching static snapshots and a false server fallback")
2795
+ const owner = nearestFunction(node)
2796
+ const topLevelOwner = owner && (owner.parent === sourceFile || ts.isVariableDeclaration(owner.parent) && owner.parent.parent?.parent?.parent === sourceFile)
2797
+ if (!topLevelOwner || !owner.body || !ts.isBlock(owner.body) || node.parent !== owner.body) throw sourceNodeError(declaration, sourceFile, "Media query external stores must initialize one top-level component const")
2798
+ for (const name of ["useEffect", "useState"]) if (owner.parameters.some(parameter => bindingNames(parameter.name).includes(name)) || functionVarDeclaresName(owner, name) || owner.body.statements.some(statement => statementDeclaresName(statement, name))) throw sourceNodeError(declaration, sourceFile, `Media query external stores conflict with component-local ${name}`)
2799
+ let setter = `__kSetMediaQuery${index++}`
2800
+ while (sourceFile.text.includes(setter)) setter = `__kSetMediaQuery${index++}`
2801
+ candidates.set(node, { declaration, query, setter })
2802
+ }
2803
+ inspect(sourceFile)
2804
+ const references = referenceIdentifiers(sourceFile, "useSyncExternalStore")
2805
+ if (references.length !== candidates.size) throw sourceNodeError(references.find(reference => ![...candidates.values()].some(candidate => insideNode(reference, candidate.declaration.initializer))) ?? externalStoreImport.entry, sourceFile, "useSyncExternalStore is supported only for direct static media query declarations")
2806
+ const directHooks = new Set(imports.flatMap(statement => statement.importClause.namedBindings.elements.filter(entry => !entry.propertyName).map(entry => entry.name.text)))
2807
+ const missingHooks = ["useEffect", "useState"].filter(name => !directHooks.has(name))
2808
+ for (const name of missingHooks) {
2809
+ const collision = sourceFile.statements.some(statement => statementDeclaresName(statement, name) || ts.isImportDeclaration(statement) && importDeclarationNames(statement).includes(name))
2810
+ if (collision) throw sourceNodeError([...candidates.values()][0].declaration, sourceFile, `Media query external stores conflict with local ${name}`)
2811
+ }
2812
+
2813
+ const visitor = node => {
2814
+ const candidate = ts.isVariableStatement(node) ? candidates.get(node) : undefined
2815
+ if (candidate) {
2816
+ const state = factory.createVariableStatement(node.modifiers, factory.createVariableDeclarationList([
2817
+ factory.createVariableDeclaration(factory.createArrayBindingPattern([
2818
+ factory.createBindingElement(undefined, undefined, candidate.declaration.name),
2819
+ factory.createBindingElement(undefined, undefined, candidate.setter)
2820
+ ]), undefined, undefined, factory.createCallExpression(factory.createIdentifier("useState"), undefined, [factory.createFalse()]))
2821
+ ], ts.NodeFlags.Const))
2822
+ const media = factory.createIdentifier("media")
2823
+ const update = factory.createIdentifier("update")
2824
+ const mediaCall = factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("window"), "matchMedia"), undefined, [factory.createStringLiteral(candidate.query)])
2825
+ const updateCallback = factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), factory.createCallExpression(factory.createIdentifier(candidate.setter), undefined, [factory.createPropertyAccessExpression(media, "matches")]))
2826
+ const listener = method => factory.createCallExpression(factory.createPropertyAccessExpression(media, method), undefined, [factory.createStringLiteral("change"), update])
2827
+ const cleanup = factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), listener("removeEventListener"))
2828
+ const setup = factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), factory.createBlock([
2829
+ factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(media, undefined, undefined, mediaCall)], ts.NodeFlags.Const)),
2830
+ factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(update, undefined, undefined, updateCallback)], ts.NodeFlags.Const)),
2831
+ factory.createExpressionStatement(factory.createCallExpression(update, undefined, [])),
2832
+ factory.createExpressionStatement(listener("addEventListener")),
2833
+ factory.createReturnStatement(cleanup)
2834
+ ], true))
2835
+ const effectCall = factory.createCallExpression(factory.createIdentifier("useEffect"), undefined, [setup, factory.createArrayLiteralExpression()])
2836
+ ts.setOriginalNode(effectCall, candidate.declaration.initializer)
2837
+ ts.setTextRange(effectCall, candidate.declaration.initializer)
2838
+ return [state, factory.createExpressionStatement(effectCall)]
2839
+ }
2840
+ if (node === externalStoreImport.statement) {
2841
+ const clause = node.importClause
2842
+ const bindings = clause.namedBindings
2843
+ const elements = bindings.elements.filter(entry => entry !== externalStoreImport.entry)
2844
+ for (const name of missingHooks) elements.push(factory.createImportSpecifier(false, undefined, factory.createIdentifier(name)))
2845
+ return factory.updateImportDeclaration(node, node.modifiers, factory.updateImportClause(clause, false, clause.name, factory.updateNamedImports(bindings, elements)), node.moduleSpecifier, node.attributes)
2846
+ }
2847
+ return ts.visitEachChild(node, visitor, context)
2848
+ }
2849
+ return ts.visitNode(sourceFile, visitor)
2850
+ }
2851
+
2852
+ function insideNode(node, root) {
2853
+ for (let current = node; current; current = current.parent) if (current === root) return true
2854
+ return false
2855
+ }
2856
+
2857
+ function normalizeNavigatorCapabilityConditions(sourceFile, factory, context) {
2858
+ const candidates = new Map()
2859
+ let index = 0
2860
+ const inspect = node => {
2861
+ if (!ts.isVariableStatement(node) || !(node.declarationList.flags & ts.NodeFlags.Const) || node.declarationList.declarations.length !== 1) {
2862
+ ts.forEachChild(node, inspect)
2863
+ return
2864
+ }
2865
+ const declaration = node.declarationList.declarations[0]
2866
+ const value = declaration.initializer && unwrapExpression(declaration.initializer)
2867
+ if (!ts.isIdentifier(declaration.name) || !value || !ts.isBinaryExpression(value) || value.operatorToken.kind !== ts.SyntaxKind.InKeyword || !ts.isStringLiteral(unwrapExpression(value.left)) || !ts.isIdentifier(unwrapExpression(value.right)) || unwrapExpression(value.right).text !== "navigator" || !isUnshadowedGlobal(unwrapExpression(value.right), sourceFile)) {
2868
+ ts.forEachChild(node, inspect)
2869
+ return
2870
+ }
2871
+ const owner = nearestFunction(node)
2872
+ const topLevelOwner = owner && (owner.parent === sourceFile || ts.isVariableDeclaration(owner.parent) && owner.parent.parent?.parent?.parent === sourceFile)
2873
+ if (!topLevelOwner || !owner.body || !ts.isBlock(owner.body) || node.parent !== owner.body) throw sourceNodeError(declaration, sourceFile, "Navigator capability conditions must be top-level component const declarations")
2874
+ const references = referenceIdentifiers(owner.body, declaration.name.text)
2875
+ const condition = references.length === 1 ? references[0] : undefined
2876
+ const structural = condition && ts.isBinaryExpression(condition.parent) && condition.parent.left === condition && condition.parent.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken && containsJsx(condition.parent.right) && ts.isJsxExpression(condition.parent.parent) && condition.parent.parent.expression === condition.parent
2877
+ if (!structural) throw sourceNodeError(declaration, sourceFile, "Navigator capability values may only control one direct JSX && branch")
2878
+ for (const name of ["useEffect", "useState"]) if (owner.parameters.some(parameter => bindingNames(parameter.name).includes(name)) || functionVarDeclaresName(owner, name) || owner.body.statements.some(statement => statementDeclaresName(statement, name))) throw sourceNodeError(declaration, sourceFile, `Navigator capability conditions conflict with component-local ${name}`)
2879
+ let setter = `__kSetNavigatorCapability${index++}`
2880
+ while (sourceFile.text.includes(setter)) setter = `__kSetNavigatorCapability${index++}`
2881
+ candidates.set(node, { declaration, property: unwrapExpression(value.left).text, setter })
2882
+ }
2883
+ inspect(sourceFile)
2884
+ if (!candidates.size) return sourceFile
2885
+
2886
+ const visitor = node => {
2887
+ const candidate = ts.isVariableStatement(node) ? candidates.get(node) : undefined
2888
+ if (candidate) {
2889
+ const state = factory.createVariableStatement(node.modifiers, factory.createVariableDeclarationList([
2890
+ factory.createVariableDeclaration(factory.createArrayBindingPattern([
2891
+ factory.createBindingElement(undefined, undefined, candidate.declaration.name),
2892
+ factory.createBindingElement(undefined, undefined, candidate.setter)
2893
+ ]), undefined, undefined, factory.createCallExpression(factory.createIdentifier("useState"), undefined, [factory.createFalse()]))
2894
+ ], ts.NodeFlags.Const))
2895
+ const capability = factory.createBinaryExpression(factory.createStringLiteral(candidate.property), factory.createToken(ts.SyntaxKind.InKeyword), factory.createIdentifier("navigator"))
2896
+ const setup = factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), factory.createBlock([
2897
+ factory.createExpressionStatement(factory.createCallExpression(factory.createIdentifier(candidate.setter), undefined, [capability]))
2898
+ ], true))
2899
+ const effectCall = factory.createCallExpression(factory.createIdentifier("useEffect"), undefined, [setup, factory.createArrayLiteralExpression()])
2900
+ ts.setOriginalNode(effectCall, candidate.declaration.initializer)
2901
+ ts.setTextRange(effectCall, candidate.declaration.initializer)
2902
+ const effect = factory.createExpressionStatement(effectCall)
2903
+ return [state, effect]
2904
+ }
2905
+ return ts.visitEachChild(node, visitor, context)
2906
+ }
2907
+ let normalized = ts.visitNode(sourceFile, visitor)
2908
+ const hookImports = normalized.statements.filter(statement => ts.isImportDeclaration(statement) && !statement.importClause?.isTypeOnly && ts.isStringLiteral(statement.moduleSpecifier) && ["react", "@kudzujs/core"].includes(statement.moduleSpecifier.text) && statement.importClause?.namedBindings && ts.isNamedImports(statement.importClause.namedBindings))
2909
+ const hookImport = hookImports[0]
2910
+ const bindings = hookImport?.importClause.namedBindings
2911
+ const imported = new Set(hookImports.flatMap(statement => statement.importClause.namedBindings.elements.map(entry => entry.name.text)))
2912
+ const missing = ["useEffect", "useState"].filter(name => !imported.has(name))
2913
+ if (!missing.length) return normalized
2914
+ for (const name of missing) if (normalized.statements.some(statement => !hookImports.includes(statement) && (statementDeclaresName(statement, name) || ts.isImportDeclaration(statement) && importDeclarationNames(statement).includes(name)))) throw sourceNodeError([...candidates.values()][0].declaration, sourceFile, `Navigator capability conditions conflict with local ${name}`)
2915
+ if (hookImport && bindings && ts.isNamedImports(bindings)) {
2916
+ const statements = normalized.statements.map(statement => statement === hookImport ? factory.updateImportDeclaration(statement, statement.modifiers, factory.updateImportClause(statement.importClause, false, statement.importClause.name, factory.updateNamedImports(bindings, [
2917
+ ...bindings.elements,
2918
+ ...missing.map(name => factory.createImportSpecifier(false, undefined, factory.createIdentifier(name)))
2919
+ ])), statement.moduleSpecifier, statement.attributes) : statement)
2920
+ return factory.updateSourceFile(normalized, statements)
2921
+ }
2922
+ const declaration = factory.createImportDeclaration(undefined, factory.createImportClause(false, undefined, factory.createNamedImports(missing.map(name => factory.createImportSpecifier(false, undefined, factory.createIdentifier(name))))), factory.createStringLiteral("@kudzujs/core"))
2923
+ const statements = [...normalized.statements]
2924
+ statements.splice(statements.findLastIndex(statement => ts.isImportDeclaration(statement)) + 1, 0, declaration)
2925
+ return factory.updateSourceFile(normalized, statements)
2926
+ }
2927
+
2928
+ function normalizeEffectAnimationFrameRefs(sourceFile, factory, context) {
2929
+ const frameCall = (node, name) => ts.isCallExpression(node) && (
2930
+ ts.isIdentifier(node.expression) && node.expression.text === name ||
2931
+ ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.expression) && node.expression.expression.text === "window" && node.expression.name.text === name
2932
+ )
2933
+ const unshadowedFrameCall = (node, owner) => {
2934
+ const name = ts.isIdentifier(node.expression) ? node.expression : ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.expression) ? node.expression.expression : undefined
2935
+ return name && !isShadowedIdentifier(name, owner) && !sourceFile.statements.some(statement => statementDeclaresName(statement, name.text) || ts.isImportDeclaration(statement) && importDeclarationNames(statement).includes(name.text))
2936
+ }
2937
+ const currentAccess = (node, name) => ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === name && node.name.text === "current"
2938
+ const inside = (node, root) => {
2939
+ for (let current = node; current; current = current.parent) if (current === root) return true
2940
+ return false
2941
+ }
2942
+ const directOrGuarded = (statement, body, name, negated) => {
2943
+ if (statement.parent === body) return true
2944
+ let branch = statement
2945
+ if (ts.isBlock(statement.parent) && statement.parent.statements.length === 1) branch = statement.parent
2946
+ const conditional = branch.parent
2947
+ if (!ts.isIfStatement(conditional) || conditional.thenStatement !== branch || conditional.parent !== body || conditional.elseStatement) return false
2948
+ let condition = unwrapExpression(conditional.expression)
2949
+ const isNegated = ts.isPrefixUnaryExpression(condition) && condition.operator === ts.SyntaxKind.ExclamationToken
2950
+ if (isNegated) condition = unwrapExpression(condition.operand)
2951
+ return isNegated === negated && currentAccess(condition, name)
2952
+ }
2953
+ const hasUseRefImport = sourceFile.statements.some(statement => ts.isImportDeclaration(statement) && !statement.importClause?.isTypeOnly && ts.isStringLiteral(statement.moduleSpecifier) && ["react", "@kudzujs/core"].includes(statement.moduleSpecifier.text) && statement.importClause?.namedBindings && ts.isNamedImports(statement.importClause.namedBindings) && statement.importClause.namedBindings.elements.some(entry => !entry.propertyName && entry.name.text === "useRef"))
2954
+ const hasUseEffectImport = sourceFile.statements.some(statement => ts.isImportDeclaration(statement) && !statement.importClause?.isTypeOnly && ts.isStringLiteral(statement.moduleSpecifier) && ["react", "@kudzujs/core"].includes(statement.moduleSpecifier.text) && statement.importClause?.namedBindings && ts.isNamedImports(statement.importClause.namedBindings) && statement.importClause.namedBindings.elements.some(entry => !entry.propertyName && entry.name.text === "useEffect"))
2955
+ const replacements = new Set()
2956
+ const inspect = node => {
2957
+ if (!hasUseRefImport || !ts.isVariableDeclaration(node) || !ts.isIdentifier(node.name) || !node.initializer || !ts.isCallExpression(node.initializer) || !ts.isIdentifier(node.initializer.expression) || node.initializer.expression.text !== "useRef" || isShadowedIdentifier(node.initializer.expression, sourceFile) || node.initializer.arguments.length !== 1 || !ts.isNumericLiteral(node.initializer.arguments[0]) || Number(node.initializer.arguments[0].text) !== 0) {
2958
+ ts.forEachChild(node, inspect)
2959
+ return
2960
+ }
2961
+ const owner = nearestFunction(node)
2962
+ if (!owner?.body || !ts.isBlock(owner.body)) return
2963
+ const accesses = []
2964
+ const collect = current => {
2965
+ if (currentAccess(current, node.name.text) && !isShadowedIdentifier(current.expression, owner.body)) accesses.push(current)
2966
+ ts.forEachChild(current, collect)
2967
+ }
2968
+ collect(owner.body)
2969
+ const frameAssignments = accesses.filter(access => ts.isBinaryExpression(access.parent) && unwrapExpression(access.parent.left) === access && access.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken && frameCall(unwrapExpression(access.parent.right), "requestAnimationFrame") && unshadowedFrameCall(unwrapExpression(access.parent.right), owner))
2970
+ if (!frameAssignments.length) return
2971
+ const invalidReference = referenceIdentifiers(owner.body, node.name.text).find(reference => !ts.isPropertyAccessExpression(reference.parent) || reference.parent.expression !== reference || reference.parent.name.text !== "current")
2972
+ if (invalidReference) throw sourceNodeError(invalidReference, sourceFile, "Animation frame refs may only use direct .current reads and assignments")
2973
+ const statement = node.parent?.parent
2974
+ const topLevelOwner = owner.parent === sourceFile || ts.isVariableDeclaration(owner.parent) && owner.parent.parent?.parent?.parent === sourceFile
2975
+ if (!topLevelOwner || !ts.isVariableStatement(statement) || !(statement.declarationList.flags & ts.NodeFlags.Const) || statement.declarationList.declarations.length !== 1 || statement.parent !== owner.body) throw sourceNodeError(node, sourceFile, "Animation frame refs must be one top-level component const")
2976
+ if (frameAssignments.length !== 1) throw sourceNodeError(node, sourceFile, "Animation frame refs require one direct ref.current = requestAnimationFrame(callback) assignment")
2977
+ const frame = unwrapExpression(frameAssignments[0].parent.right)
2978
+ const frameCallback = frame.arguments.length === 1 && ts.isIdentifier(unwrapExpression(frame.arguments[0])) ? unwrapExpression(frame.arguments[0]) : undefined
2979
+ const effectCalls = owner.body.statements.flatMap(statement => hasUseEffectImport && ts.isExpressionStatement(statement) && ts.isCallExpression(statement.expression) && ts.isIdentifier(statement.expression.expression) && statement.expression.expression.text === "useEffect" && !isShadowedIdentifier(statement.expression.expression, sourceFile) ? [statement.expression] : [])
2980
+ const effects = effectCalls.filter(effect => effect.arguments[0] && inside(frameAssignments[0], effect.arguments[0]))
2981
+ if (effects.length !== 1) throw sourceNodeError(node, sourceFile, "Animation frame refs must belong to one inline component effect")
2982
+ const effect = effects[0]
2983
+ const callback = effect.arguments[0]
2984
+ if (!(ts.isArrowFunction(callback) || ts.isFunctionExpression(callback)) || !ts.isBlock(callback.body)) throw sourceNodeError(callback, sourceFile, "Animation frame refs require one inline block-bodied effect")
2985
+ if (accesses.some(access => !inside(access, callback))) throw sourceNodeError(node, sourceFile, "Animation frame refs may only be used inside their owning effect")
2986
+ const callbacks = new Map()
2987
+ for (const statement of callback.body.statements) {
2988
+ if (ts.isVariableStatement(statement)) for (const declaration of statement.declarationList.declarations) {
2989
+ if (ts.isIdentifier(declaration.name) && declaration.initializer && (ts.isArrowFunction(declaration.initializer) || ts.isFunctionExpression(declaration.initializer))) callbacks.set(declaration.name.text, declaration.initializer)
2990
+ }
2991
+ if (ts.isFunctionDeclaration(statement) && statement.name && statement.body) callbacks.set(statement.name.text, statement)
2992
+ }
2993
+ const frameOwner = nearestFunction(frameAssignments[0])
2994
+ const shadowedCallback = frameCallback && isShadowedIdentifier(frameCallback, callback.body)
2995
+ const update = frameCallback && !shadowedCallback && callbacks.get(frameCallback.text)
2996
+ if (!update) throw sourceNodeError(frame, sourceFile, "Animation frame refs require a direct local callback")
2997
+ const frameStatement = frameAssignments[0].parent.parent
2998
+ if (!frameOwner || ![...callbacks.values()].includes(frameOwner) || !ts.isBlock(frameOwner.body) || !ts.isExpressionStatement(frameStatement) || !directOrGuarded(frameStatement, frameOwner.body, node.name.text, true)) throw sourceNodeError(frameAssignments[0], sourceFile, "Animation frame requests must be assigned directly inside one local scheduler")
2999
+ const resetAssignments = accesses.filter(access => ts.isBinaryExpression(access.parent) && unwrapExpression(access.parent.left) === access && access.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken && ts.isNumericLiteral(unwrapExpression(access.parent.right)) && Number(unwrapExpression(access.parent.right).text) === 0)
3000
+ const resetStatement = resetAssignments[0]?.parent.parent
3001
+ if (resetAssignments.length !== 1 || nearestFunction(resetAssignments[0]) !== update || !ts.isBlock(update.body) || !ts.isExpressionStatement(resetStatement) || resetStatement.parent !== update.body) throw sourceNodeError(update, sourceFile, "Animation frame callbacks must directly reset their ref to 0")
3002
+ const writes = accesses.filter(access => ts.isBinaryExpression(access.parent) && unwrapExpression(access.parent.left) === access && access.parent.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && access.parent.operatorToken.kind <= ts.SyntaxKind.LastAssignment)
3003
+ if (writes.length !== 2) throw sourceNodeError(node, sourceFile, "Animation frame refs may only be assigned by their request and reset operations")
3004
+ const returns = effectReturns(callback)
3005
+ const cancellations = []
3006
+ const collectCancellations = current => {
3007
+ const argument = current.arguments?.[0] && unwrapExpression(current.arguments[0])
3008
+ if (frameCall(current, "cancelAnimationFrame") && unshadowedFrameCall(current, owner) && current.arguments.length === 1 && currentAccess(argument, node.name.text) && !isShadowedIdentifier(argument.expression, owner.body)) cancellations.push(current)
3009
+ ts.forEachChild(current, collectCancellations)
3010
+ }
3011
+ collectCancellations(callback.body)
3012
+ const cancellation = cancellations[0]
3013
+ const cleanup = cancellation && returns.cleanups.find(candidate => nearestFunction(cancellation) === candidate)
3014
+ const cancellationStatement = cancellation?.parent
3015
+ if (cancellations.length !== 1 || !cleanup || !ts.isBlock(cleanup.body) || !ts.isExpressionStatement(cancellationStatement) || !directOrGuarded(cancellationStatement, cleanup.body, node.name.text, false)) throw sourceNodeError(node, sourceFile, "Animation frame refs require direct cancellation in effect cleanup")
3016
+ replacements.add(node)
3017
+ }
3018
+ inspect(sourceFile)
3019
+ if (!replacements.size) return sourceFile
3020
+ const visitor = node => {
3021
+ if (replacements.has(node)) return factory.updateVariableDeclaration(node, node.name, node.exclamationToken, node.type, factory.createObjectLiteralExpression([
3022
+ factory.createPropertyAssignment("current", factory.createNumericLiteral(0))
3023
+ ]))
3024
+ return ts.visitEachChild(node, visitor, context)
3025
+ }
3026
+ return ts.visitNode(sourceFile, visitor)
3027
+ }
3028
+
2748
3029
  function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings, listExpressions, handlerUrl, file, sourceFiles, sourceIndex, staticFiles, importedAssets, cssModules, base, clientImports, workerReferences) {
2749
3030
  return context => sourceFile => {
2750
3031
  const factory = context.factory
@@ -2757,8 +3038,14 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
2757
3038
  ts.setParentRecursive(sourceFile, false)
2758
3039
  sourceFile = normalizeClsxSyntax(sourceFile, factory, context)
2759
3040
  ts.setParentRecursive(sourceFile, false)
3041
+ sourceFile = normalizeMediaQueryExternalStores(sourceFile, factory, context)
3042
+ ts.setParentRecursive(sourceFile, false)
2760
3043
  sourceFile = normalizeReactMigrationSyntax(sourceFile, factory, context, importedCollections)
2761
3044
  ts.setParentRecursive(sourceFile, false)
3045
+ sourceFile = normalizeNavigatorCapabilityConditions(sourceFile, factory, context)
3046
+ ts.setParentRecursive(sourceFile, false)
3047
+ sourceFile = normalizeEffectAnimationFrameRefs(sourceFile, factory, context)
3048
+ ts.setParentRecursive(sourceFile, false)
2762
3049
  sourceFile = normalizeCustomHookTimerRefs(sourceFile, factory, context)
2763
3050
  const customHookTimerStates = customHookTimerStatesBySource.get(sourceFile) ?? new Set()
2764
3051
  ts.setParentRecursive(sourceFile, false)
@@ -2787,8 +3074,14 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
2787
3074
  ts.setParentRecursive(imported, false)
2788
3075
  imported = normalizeClsxSyntax(imported, factory, context)
2789
3076
  ts.setParentRecursive(imported, false)
3077
+ imported = normalizeMediaQueryExternalStores(imported, factory, context)
3078
+ ts.setParentRecursive(imported, false)
2790
3079
  imported = normalizeReactMigrationSyntax(imported, factory, context, importedSerializableCollectionNames(imported, target, sourceFiles, sourceIndex))
2791
3080
  ts.setParentRecursive(imported, false)
3081
+ imported = normalizeNavigatorCapabilityConditions(imported, factory, context)
3082
+ ts.setParentRecursive(imported, false)
3083
+ imported = normalizeEffectAnimationFrameRefs(imported, factory, context)
3084
+ ts.setParentRecursive(imported, false)
2792
3085
  imported = normalizeCustomHookTimerRefs(imported, factory, context)
2793
3086
  importedTimerStates.set(target, customHookTimerStatesBySource.get(imported) ?? new Set())
2794
3087
  ts.setParentRecursive(imported, false)
@@ -3938,6 +4231,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
3938
4231
  const invalidCleanup = returns.cleanups.find(cleanup => cleanup.parameters.length || cleanup.asteriskToken)
3939
4232
  if (invalidCleanup) effectFail(invalidCleanup, "useEffect() cleanup functions cannot declare parameters or be generators")
3940
4233
  if (returns.cleanup && callback.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) effectFail(callback, "useEffect() async callbacks cannot return cleanup functions")
4234
+ validateEffectOwnedBrowserResources(callback, returns, effectFail)
3941
4235
  const callbackSource = specializedEffect?.sourceFile ?? sourceFile
3942
4236
  const callbackFile = callbackSource.fileName
3943
4237
  const workerStart = workerReferences.length
@@ -5434,7 +5728,7 @@ function compileOptimizedEvent(expression, setters, factory) {
5434
5728
  }
5435
5729
 
5436
5730
  const nativeGlobals = new Set([
5437
- "Array", "ArrayBuffer", "BigInt", "Blob", "Boolean", "Date", "Error", "Event", "FileReader", "FormData", "Infinity", "Intl", "JSON", "Map", "Math", "NaN", "Number", "Object", "Promise", "Proxy", "RangeError", "ReferenceError", "Reflect", "RegExp", "Set", "String", "Symbol", "TypeError", "URL", "URLSearchParams", "WeakMap", "WeakSet", "WebSocket", "Worker", "alert", "atob", "btoa", "clearInterval", "clearTimeout", "console", "crypto", "document", "fetch", "globalThis", "history", "isFinite", "isNaN", "localStorage", "location", "navigator", "parseFloat", "parseInt", "queueMicrotask", "requestAnimationFrame", "setInterval", "setTimeout", "structuredClone", "undefined", "window"
5731
+ "Array", "ArrayBuffer", "BigInt", "Blob", "Boolean", "Date", "Error", "Event", "FileReader", "FormData", "Infinity", "IntersectionObserver", "Intl", "JSON", "Map", "Math", "NaN", "Number", "Object", "Promise", "Proxy", "RangeError", "ReferenceError", "Reflect", "RegExp", "Set", "String", "Symbol", "TypeError", "URL", "URLSearchParams", "WeakMap", "WeakSet", "WebSocket", "Worker", "alert", "atob", "btoa", "cancelAnimationFrame", "clearInterval", "clearTimeout", "console", "crypto", "document", "fetch", "globalThis", "history", "isFinite", "isNaN", "localStorage", "location", "navigator", "parseFloat", "parseInt", "performance", "queueMicrotask", "requestAnimationFrame", "setInterval", "setTimeout", "structuredClone", "undefined", "window"
5438
5732
  ])
5439
5733
 
5440
5734
  function rewriteEffectWorkers(callback, file, sourceFile, sourceFiles, workerReferences, factory, context) {
@@ -5837,6 +6131,30 @@ function effectReturns(callback) {
5837
6131
  return { cleanup, cleanups, invalid }
5838
6132
  }
5839
6133
 
6134
+ function validateEffectOwnedBrowserResources(callback, returns, fail) {
6135
+ const observers = []
6136
+ const frameAssignments = []
6137
+ const cancellations = new Set()
6138
+ const disconnected = new Set()
6139
+ const insideCleanup = node => returns.cleanups.some(cleanup => {
6140
+ for (let current = node; current; current = current.parent) if (current === cleanup) return true
6141
+ return false
6142
+ })
6143
+ const visit = node => {
6144
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isNewExpression(unwrapExpression(node.initializer)) && ts.isIdentifier(unwrapExpression(node.initializer).expression) && unwrapExpression(node.initializer).expression.text === "IntersectionObserver") observers.push(node)
6145
+ if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken && ts.isIdentifier(unwrapExpression(node.left)) && ts.isCallExpression(unwrapExpression(node.right)) && ts.isIdentifier(unwrapExpression(node.right).expression) && unwrapExpression(node.right).expression.text === "requestAnimationFrame") frameAssignments.push(node)
6146
+ if (insideCleanup(node) && ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "cancelAnimationFrame" && node.arguments.length === 1 && ts.isIdentifier(unwrapExpression(node.arguments[0]))) cancellations.add(unwrapExpression(node.arguments[0]).text)
6147
+ if (insideCleanup(node) && ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.expression) && node.expression.name.text === "disconnect" && node.arguments.length === 0) disconnected.add(node.expression.expression.text)
6148
+ ts.forEachChild(node, visit)
6149
+ }
6150
+ visit(callback.body)
6151
+ for (const observer of observers) if (!disconnected.has(observer.name.text)) fail(observer, `IntersectionObserver effects must disconnect ${JSON.stringify(observer.name.text)} in cleanup`)
6152
+ for (const assignment of frameAssignments) {
6153
+ const name = unwrapExpression(assignment.left).text
6154
+ if (!cancellations.has(name)) fail(assignment, `Animation loop effects must cancel ${JSON.stringify(name)} in cleanup`)
6155
+ }
6156
+ }
6157
+
5840
6158
  function printClientImports(entries, handlerPath) {
5841
6159
  const unique = new Map(entries.map(entry => [`${entry.target}:${entry.kind}:${entry.imported ?? ""}:${entry.local}`, entry]))
5842
6160
  const groups = Map.groupBy(unique.values(), entry => entry.target)
@@ -20,6 +20,8 @@ export interface RefObject<T> {
20
20
  }
21
21
 
22
22
  export function useRef<T>(initialValue: null): RefObject<T>
23
+ export function useRef(initialValue: 0): { current: number }
24
+ export function useSyncExternalStore<T>(subscribe: (callback: () => void) => () => void, getSnapshot: () => T, getServerSnapshot: () => T): T
23
25
 
24
26
  export interface Context<T> {
25
27
  Provider: (props: { value: T; children?: unknown }) => unknown
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kudzujs/core",
3
- "version": "0.8.12",
3
+ "version": "0.8.14",
4
4
  "description": "HTML-first TSX framework with synchronous state semantics and no virtual DOM",
5
5
  "type": "module",
6
6
  "license": "MIT",