@kudzujs/core 0.8.13 → 0.8.15

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.
@@ -0,0 +1,103 @@
1
+ import ts from "typescript"
2
+ import { effectReturns, importDeclarationNames, isShadowedIdentifier, nearestFunction, referenceIdentifiers, sourceNodeError, statementDeclaresName, unwrapExpression } from "./ast-helpers.mjs"
3
+
4
+ export function normalizeEffectAnimationFrameRefs(sourceFile, factory, context) {
5
+ const frameCall = (node, name) => ts.isCallExpression(node) && (
6
+ ts.isIdentifier(node.expression) && node.expression.text === name ||
7
+ ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.expression) && node.expression.expression.text === "window" && node.expression.name.text === name
8
+ )
9
+ const unshadowedFrameCall = (node, owner) => {
10
+ const name = ts.isIdentifier(node.expression) ? node.expression : ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.expression) ? node.expression.expression : undefined
11
+ return name && !isShadowedIdentifier(name, owner) && !sourceFile.statements.some(statement => statementDeclaresName(statement, name.text) || ts.isImportDeclaration(statement) && importDeclarationNames(statement).includes(name.text))
12
+ }
13
+ const currentAccess = (node, name) => ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === name && node.name.text === "current"
14
+ const inside = (node, root) => {
15
+ for (let current = node; current; current = current.parent) if (current === root) return true
16
+ return false
17
+ }
18
+ const directOrGuarded = (statement, body, name, negated) => {
19
+ if (statement.parent === body) return true
20
+ let branch = statement
21
+ if (ts.isBlock(statement.parent) && statement.parent.statements.length === 1) branch = statement.parent
22
+ const conditional = branch.parent
23
+ if (!ts.isIfStatement(conditional) || conditional.thenStatement !== branch || conditional.parent !== body || conditional.elseStatement) return false
24
+ let condition = unwrapExpression(conditional.expression)
25
+ const isNegated = ts.isPrefixUnaryExpression(condition) && condition.operator === ts.SyntaxKind.ExclamationToken
26
+ if (isNegated) condition = unwrapExpression(condition.operand)
27
+ return isNegated === negated && currentAccess(condition, name)
28
+ }
29
+ 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"))
30
+ 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"))
31
+ const replacements = new Set()
32
+ const inspect = node => {
33
+ 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) {
34
+ ts.forEachChild(node, inspect)
35
+ return
36
+ }
37
+ const owner = nearestFunction(node)
38
+ if (!owner?.body || !ts.isBlock(owner.body)) return
39
+ const accesses = []
40
+ const collect = current => {
41
+ if (currentAccess(current, node.name.text) && !isShadowedIdentifier(current.expression, owner.body)) accesses.push(current)
42
+ ts.forEachChild(current, collect)
43
+ }
44
+ collect(owner.body)
45
+ 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))
46
+ if (!frameAssignments.length) return
47
+ const invalidReference = referenceIdentifiers(owner.body, node.name.text).find(reference => !ts.isPropertyAccessExpression(reference.parent) || reference.parent.expression !== reference || reference.parent.name.text !== "current")
48
+ if (invalidReference) throw sourceNodeError(invalidReference, sourceFile, "Animation frame refs may only use direct .current reads and assignments")
49
+ const statement = node.parent?.parent
50
+ const topLevelOwner = owner.parent === sourceFile || ts.isVariableDeclaration(owner.parent) && owner.parent.parent?.parent?.parent === sourceFile
51
+ 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")
52
+ if (frameAssignments.length !== 1) throw sourceNodeError(node, sourceFile, "Animation frame refs require one direct ref.current = requestAnimationFrame(callback) assignment")
53
+ const frame = unwrapExpression(frameAssignments[0].parent.right)
54
+ const frameCallback = frame.arguments.length === 1 && ts.isIdentifier(unwrapExpression(frame.arguments[0])) ? unwrapExpression(frame.arguments[0]) : undefined
55
+ 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] : [])
56
+ const effects = effectCalls.filter(effect => effect.arguments[0] && inside(frameAssignments[0], effect.arguments[0]))
57
+ if (effects.length !== 1) throw sourceNodeError(node, sourceFile, "Animation frame refs must belong to one inline component effect")
58
+ const effect = effects[0]
59
+ const callback = effect.arguments[0]
60
+ if (!(ts.isArrowFunction(callback) || ts.isFunctionExpression(callback)) || !ts.isBlock(callback.body)) throw sourceNodeError(callback, sourceFile, "Animation frame refs require one inline block-bodied effect")
61
+ if (accesses.some(access => !inside(access, callback))) throw sourceNodeError(node, sourceFile, "Animation frame refs may only be used inside their owning effect")
62
+ const callbacks = new Map()
63
+ for (const statement of callback.body.statements) {
64
+ if (ts.isVariableStatement(statement)) for (const declaration of statement.declarationList.declarations) {
65
+ if (ts.isIdentifier(declaration.name) && declaration.initializer && (ts.isArrowFunction(declaration.initializer) || ts.isFunctionExpression(declaration.initializer))) callbacks.set(declaration.name.text, declaration.initializer)
66
+ }
67
+ if (ts.isFunctionDeclaration(statement) && statement.name && statement.body) callbacks.set(statement.name.text, statement)
68
+ }
69
+ const frameOwner = nearestFunction(frameAssignments[0])
70
+ const shadowedCallback = frameCallback && isShadowedIdentifier(frameCallback, callback.body)
71
+ const update = frameCallback && !shadowedCallback && callbacks.get(frameCallback.text)
72
+ if (!update) throw sourceNodeError(frame, sourceFile, "Animation frame refs require a direct local callback")
73
+ const frameStatement = frameAssignments[0].parent.parent
74
+ 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")
75
+ 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)
76
+ const resetStatement = resetAssignments[0]?.parent.parent
77
+ 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")
78
+ 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)
79
+ if (writes.length !== 2) throw sourceNodeError(node, sourceFile, "Animation frame refs may only be assigned by their request and reset operations")
80
+ const returns = effectReturns(callback)
81
+ const cancellations = []
82
+ const collectCancellations = current => {
83
+ const argument = current.arguments?.[0] && unwrapExpression(current.arguments[0])
84
+ if (frameCall(current, "cancelAnimationFrame") && unshadowedFrameCall(current, owner) && current.arguments.length === 1 && currentAccess(argument, node.name.text) && !isShadowedIdentifier(argument.expression, owner.body)) cancellations.push(current)
85
+ ts.forEachChild(current, collectCancellations)
86
+ }
87
+ collectCancellations(callback.body)
88
+ const cancellation = cancellations[0]
89
+ const cleanup = cancellation && returns.cleanups.find(candidate => nearestFunction(cancellation) === candidate)
90
+ const cancellationStatement = cancellation?.parent
91
+ 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")
92
+ replacements.add(node)
93
+ }
94
+ inspect(sourceFile)
95
+ if (!replacements.size) return sourceFile
96
+ const visitor = node => {
97
+ if (replacements.has(node)) return factory.updateVariableDeclaration(node, node.name, node.exclamationToken, node.type, factory.createObjectLiteralExpression([
98
+ factory.createPropertyAssignment("current", factory.createNumericLiteral(0))
99
+ ]))
100
+ return ts.visitEachChild(node, visitor, context)
101
+ }
102
+ return ts.visitNode(sourceFile, visitor)
103
+ }
@@ -0,0 +1,181 @@
1
+ import ts from "typescript"
2
+
3
+ export function importDeclarationNames(statement) {
4
+ const names = []
5
+ if (statement.importClause?.name) names.push(statement.importClause.name.text)
6
+ const bindings = statement.importClause?.namedBindings
7
+ if (bindings && ts.isNamespaceImport(bindings)) names.push(bindings.name.text)
8
+ if (bindings && ts.isNamedImports(bindings)) for (const entry of bindings.elements) names.push(entry.name.text)
9
+ return names
10
+ }
11
+
12
+ export function referenceIdentifiers(root, name) {
13
+ const references = []
14
+ const visit = node => {
15
+ if (ts.isIdentifier(node) && node.text === name && isReferenceIdentifier(node) && !isShadowedIdentifier(node, root)) references.push(node)
16
+ ts.forEachChild(node, visit)
17
+ }
18
+ visit(root)
19
+ return references
20
+ }
21
+
22
+ export function isFunctionLike(node) {
23
+ return ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node) || ts.isMethodDeclaration(node) || ts.isGetAccessorDeclaration(node) || ts.isSetAccessorDeclaration(node) || ts.isConstructorDeclaration(node)
24
+ }
25
+
26
+ export function containsJsx(root) {
27
+ let found = false
28
+ const visit = node => {
29
+ if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node)) found = true
30
+ if (!found) ts.forEachChild(node, visit)
31
+ }
32
+ visit(root)
33
+ return found
34
+ }
35
+
36
+ export function referencesIdentifier(root, name) {
37
+ let found = false
38
+ const visit = node => {
39
+ if (ts.isIdentifier(node) && node.text === name && isReferenceIdentifier(node)) found = true
40
+ if (!found) ts.forEachChild(node, visit)
41
+ }
42
+ visit(root)
43
+ return found
44
+ }
45
+
46
+ export function unwrapExpression(node) {
47
+ return ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isTypeAssertionExpression(node) || ts.isNonNullExpression(node) || ts.isSatisfiesExpression(node) ? unwrapExpression(node.expression) : node
48
+ }
49
+
50
+ export function isLocalConst(node) {
51
+ const list = node.parent
52
+ const statement = list?.parent
53
+ return ts.isVariableDeclarationList(list) && (list.flags & ts.NodeFlags.Const) !== 0 && ts.isVariableStatement(statement)
54
+ }
55
+
56
+ export function isUnshadowedGlobal(identifier, sourceFile) {
57
+ if (isShadowedIdentifier(identifier, sourceFile)) return false
58
+ return !sourceFile.statements.some(statement => {
59
+ if (statementDeclaresName(statement, identifier.text)) return true
60
+ if (!ts.isImportDeclaration(statement) || !statement.importClause) return false
61
+ const clause = statement.importClause
62
+ if (clause.name?.text === identifier.text) return true
63
+ if (clause.namedBindings && ts.isNamespaceImport(clause.namedBindings)) return clause.namedBindings.name.text === identifier.text
64
+ return clause.namedBindings && ts.isNamedImports(clause.namedBindings) ? clause.namedBindings.elements.some(entry => entry.name.text === identifier.text) : false
65
+ })
66
+ }
67
+
68
+ export function bindingNames(name) {
69
+ if (ts.isIdentifier(name)) return [name.text]
70
+ return name.elements.flatMap(element => ts.isBindingElement(element) ? bindingNames(element.name) : [])
71
+ }
72
+
73
+ export function isReferenceIdentifier(node) {
74
+ const parent = node.parent
75
+ if (!parent) return true
76
+ if ((ts.isPropertyAccessExpression(parent) && parent.name === node) ||
77
+ (ts.isPropertyAssignment(parent) && parent.name === node) ||
78
+ (ts.isMethodDeclaration(parent) && parent.name === node) ||
79
+ ((ts.isGetAccessorDeclaration(parent) || ts.isSetAccessorDeclaration(parent)) && parent.name === node) ||
80
+ (ts.isVariableDeclaration(parent) && parent.name === node) ||
81
+ (ts.isParameter(parent) && parent.name === node) ||
82
+ (ts.isFunctionDeclaration(parent) && parent.name === node) ||
83
+ (ts.isJsxAttribute(parent) && parent.name === node) ||
84
+ (ts.isBindingElement(parent) && (parent.name === node || parent.propertyName === node)) ||
85
+ ts.isImportSpecifier(parent) || ts.isImportClause(parent)) return false
86
+ return true
87
+ }
88
+
89
+ export function nearestFunction(node) {
90
+ for (let current = node.parent; current; current = current.parent) {
91
+ if (ts.isFunctionDeclaration(current) || ts.isFunctionExpression(current) || ts.isArrowFunction(current)) return current
92
+ }
93
+ return undefined
94
+ }
95
+
96
+ export function nearestFunctionLike(node) {
97
+ for (let current = node.parent; current; current = current.parent) if (isFunctionLike(current)) return current
98
+ return undefined
99
+ }
100
+
101
+ export function isShadowedByParameter(node, scopeRoot) {
102
+ for (let current = node.parent; current; current = current.parent) {
103
+ if (isFunctionLike(current) && current.parameters.some(parameter => bindingNames(parameter.name).includes(node.text))) return true
104
+ if (current === scopeRoot) break
105
+ }
106
+ return false
107
+ }
108
+
109
+ export function isShadowedIdentifier(node, scopeRoot) {
110
+ if (isShadowedByParameter(node, scopeRoot)) return true
111
+ if (node === scopeRoot) return false
112
+ if (isFunctionLike(scopeRoot) && scopeRoot.name?.text === node.text) return true
113
+ if (isFunctionLike(scopeRoot) && functionVarDeclaresName(scopeRoot, node.text)) return true
114
+ for (let current = node.parent; current; current = current.parent) {
115
+ if (current === scopeRoot) break
116
+ if (ts.isFunctionExpression(current) && current.name?.text === node.text) return true
117
+ if (ts.isBlock(current) && current.statements.some(statement => statementDeclaresName(statement, node.text))) return true
118
+ if (ts.isCaseBlock(current) && current.clauses.some(clause => clause.statements.some(statement => statementDeclaresName(statement, node.text)))) return true
119
+ if (ts.isCatchClause(current) && current.variableDeclaration && bindingNames(current.variableDeclaration.name).includes(node.text)) return true
120
+ if ((ts.isForStatement(current) || ts.isForInStatement(current) || ts.isForOfStatement(current)) && loopDeclaresName(current, node.text)) return true
121
+ if (isFunctionLike(current) && functionVarDeclaresName(current, node.text)) return true
122
+ }
123
+ return false
124
+ }
125
+
126
+ export function statementDeclaresName(statement, name) {
127
+ if (ts.isVariableStatement(statement)) return statement.declarationList.declarations.some(declaration => bindingNames(declaration.name).includes(name))
128
+ if (ts.isFunctionDeclaration(statement) || ts.isClassDeclaration(statement) || ts.isImportEqualsDeclaration(statement)) return statement.name?.text === name
129
+ if ((ts.isEnumDeclaration(statement) || ts.isModuleDeclaration(statement)) && !statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.DeclareKeyword)) return ts.isIdentifier(statement.name) && statement.name.text === name
130
+ return false
131
+ }
132
+
133
+ export function loopDeclaresName(loop, name) {
134
+ const declaration = ts.isForStatement(loop) ? loop.initializer : loop.initializer
135
+ return declaration && ts.isVariableDeclarationList(declaration) && declaration.declarations.some(entry => bindingNames(entry.name).includes(name))
136
+ }
137
+
138
+ export function functionVarDeclaresName(fn, name) {
139
+ let found = false
140
+ const visit = node => {
141
+ if (found || node !== fn.body && isFunctionLike(node)) return
142
+ if (ts.isVariableDeclarationList(node) && (node.flags & ts.NodeFlags.BlockScoped) === 0 && node.declarations.some(entry => bindingNames(entry.name).includes(name))) found = true
143
+ if (!found) ts.forEachChild(node, visit)
144
+ }
145
+ if (fn.body) visit(fn.body)
146
+ return found
147
+ }
148
+
149
+ export function sourceNodeError(node, fallbackSource, message) {
150
+ const original = ts.getOriginalNode(node)
151
+ const sourceFile = original.getSourceFile?.()?.fileName ? original.getSourceFile() : fallbackSource
152
+ const position = sourceFile.getLineAndCharacterOfPosition(original.getStart(sourceFile))
153
+ return new Error(`${sourceFile.fileName}:${position.line + 1}:${position.character + 1} ${message}`)
154
+ }
155
+
156
+ export function sourceLocation(node, fallbackSource) {
157
+ const original = ts.getOriginalNode(node)
158
+ const sourceFile = original.getSourceFile?.()?.fileName ? original.getSourceFile() : fallbackSource
159
+ const position = sourceFile.getLineAndCharacterOfPosition(original.getStart(sourceFile))
160
+ return `${sourceFile.fileName}:${position.line + 1}:${position.character + 1}`
161
+ }
162
+
163
+ export function effectReturns(callback) {
164
+ let cleanup = false
165
+ let invalid
166
+ const cleanups = []
167
+ const visit = node => {
168
+ if (invalid || node !== callback.body && isFunctionLike(node)) return
169
+ if (ts.isReturnStatement(node) && node.expression) {
170
+ const expression = unwrapExpression(node.expression)
171
+ if (ts.isArrowFunction(expression) || ts.isFunctionExpression(expression)) {
172
+ cleanup = true
173
+ cleanups.push(expression)
174
+ }
175
+ else invalid = node
176
+ }
177
+ if (!invalid) ts.forEachChild(node, visit)
178
+ }
179
+ visit(callback.body)
180
+ return { cleanup, cleanups, invalid }
181
+ }
@@ -0,0 +1,182 @@
1
+ import ts from "typescript"
2
+ import { bindingNames, containsJsx, functionVarDeclaresName, importDeclarationNames, isShadowedIdentifier, isUnshadowedGlobal, nearestFunction, referenceIdentifiers, sourceNodeError, statementDeclaresName, unwrapExpression } from "./ast-helpers.mjs"
3
+
4
+ export function normalizeMediaQueryExternalStores(sourceFile, factory, context) {
5
+ 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))
6
+ const externalStoreImport = imports.flatMap(statement => statement.importClause.namedBindings.elements.map(entry => ({ entry, statement }))).find(({ entry }) => !entry.isTypeOnly && !entry.propertyName && entry.name.text === "useSyncExternalStore")
7
+ if (!externalStoreImport) return sourceFile
8
+ const returnedExpression = callback => {
9
+ if (!(ts.isArrowFunction(callback) || ts.isFunctionExpression(callback)) || callback.parameters.length) return undefined
10
+ if (!ts.isBlock(callback.body)) return unwrapExpression(callback.body)
11
+ if (callback.body.statements.length !== 1 || !ts.isReturnStatement(callback.body.statements[0]) || !callback.body.statements[0].expression) return undefined
12
+ return unwrapExpression(callback.body.statements[0].expression)
13
+ }
14
+ const matchMediaQuery = expression => {
15
+ expression = unwrapExpression(expression)
16
+ 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
17
+ return unwrapExpression(expression.arguments[0]).text
18
+ }
19
+ 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
20
+ const candidates = new Map()
21
+ let index = 0
22
+ const inspect = node => {
23
+ if (!ts.isVariableStatement(node) || !(node.declarationList.flags & ts.NodeFlags.Const) || node.declarationList.declarations.length !== 1) {
24
+ ts.forEachChild(node, inspect)
25
+ return
26
+ }
27
+ const declaration = node.declarationList.declarations[0]
28
+ const call = declaration.initializer && unwrapExpression(declaration.initializer)
29
+ if (!ts.isIdentifier(declaration.name) || !call || !ts.isCallExpression(call) || !ts.isIdentifier(call.expression) || call.expression.text !== "useSyncExternalStore" || isShadowedIdentifier(call.expression, sourceFile)) {
30
+ ts.forEachChild(node, inspect)
31
+ return
32
+ }
33
+ if (call.arguments.length !== 3) throw sourceNodeError(call, sourceFile, "Media query useSyncExternalStore() requires subscribe, browser snapshot, and false server snapshot callbacks")
34
+ const [subscribe, snapshot, serverSnapshot] = call.arguments.map(unwrapExpression)
35
+ 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")
36
+ if (subscribe.body.statements.length !== 3) throw sourceNodeError(subscribe, sourceFile, "Media query subscriptions must add and remove one matching change listener")
37
+ const callback = subscribe.parameters[0].name.text
38
+ const [mediaStatement, addStatement, returnStatement] = subscribe.body.statements
39
+ const mediaDeclaration = ts.isVariableStatement(mediaStatement) && (mediaStatement.declarationList.flags & ts.NodeFlags.Const) && mediaStatement.declarationList.declarations.length === 1 ? mediaStatement.declarationList.declarations[0] : undefined
40
+ const media = mediaDeclaration && ts.isIdentifier(mediaDeclaration.name) ? mediaDeclaration.name.text : undefined
41
+ const query = mediaDeclaration?.initializer && matchMediaQuery(mediaDeclaration.initializer)
42
+ const cleanup = ts.isReturnStatement(returnStatement) && returnStatement.expression ? unwrapExpression(returnStatement.expression) : undefined
43
+ const cleanupStatement = cleanup && (ts.isArrowFunction(cleanup) || ts.isFunctionExpression(cleanup)) && !cleanup.parameters.length
44
+ ? ts.isBlock(cleanup.body) ? cleanup.body.statements.length === 1 ? cleanup.body.statements[0] : undefined : factory.createExpressionStatement(cleanup.body)
45
+ : undefined
46
+ 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")
47
+ const snapshotValue = returnedExpression(snapshot)
48
+ const snapshotQuery = snapshotValue && ts.isPropertyAccessExpression(snapshotValue) && snapshotValue.name.text === "matches" ? matchMediaQuery(snapshotValue.expression) : undefined
49
+ const serverValue = returnedExpression(serverSnapshot)
50
+ 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")
51
+ const owner = nearestFunction(node)
52
+ const topLevelOwner = owner && (owner.parent === sourceFile || ts.isVariableDeclaration(owner.parent) && owner.parent.parent?.parent?.parent === sourceFile)
53
+ 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")
54
+ 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}`)
55
+ let setter = `__kSetMediaQuery${index++}`
56
+ while (sourceFile.text.includes(setter)) setter = `__kSetMediaQuery${index++}`
57
+ candidates.set(node, { declaration, query, setter })
58
+ }
59
+ inspect(sourceFile)
60
+ const references = referenceIdentifiers(sourceFile, "useSyncExternalStore")
61
+ 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")
62
+ const directHooks = new Set(imports.flatMap(statement => statement.importClause.namedBindings.elements.filter(entry => !entry.propertyName).map(entry => entry.name.text)))
63
+ const missingHooks = ["useEffect", "useState"].filter(name => !directHooks.has(name))
64
+ for (const name of missingHooks) {
65
+ const collision = sourceFile.statements.some(statement => statementDeclaresName(statement, name) || ts.isImportDeclaration(statement) && importDeclarationNames(statement).includes(name))
66
+ if (collision) throw sourceNodeError([...candidates.values()][0].declaration, sourceFile, `Media query external stores conflict with local ${name}`)
67
+ }
68
+
69
+ const visitor = node => {
70
+ const candidate = ts.isVariableStatement(node) ? candidates.get(node) : undefined
71
+ if (candidate) {
72
+ const state = factory.createVariableStatement(node.modifiers, factory.createVariableDeclarationList([
73
+ factory.createVariableDeclaration(factory.createArrayBindingPattern([
74
+ factory.createBindingElement(undefined, undefined, candidate.declaration.name),
75
+ factory.createBindingElement(undefined, undefined, candidate.setter)
76
+ ]), undefined, undefined, factory.createCallExpression(factory.createIdentifier("useState"), undefined, [factory.createFalse()]))
77
+ ], ts.NodeFlags.Const))
78
+ const media = factory.createIdentifier("media")
79
+ const update = factory.createIdentifier("update")
80
+ const mediaCall = factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("window"), "matchMedia"), undefined, [factory.createStringLiteral(candidate.query)])
81
+ const updateCallback = factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), factory.createCallExpression(factory.createIdentifier(candidate.setter), undefined, [factory.createPropertyAccessExpression(media, "matches")]))
82
+ const listener = method => factory.createCallExpression(factory.createPropertyAccessExpression(media, method), undefined, [factory.createStringLiteral("change"), update])
83
+ const cleanup = factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), listener("removeEventListener"))
84
+ const setup = factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), factory.createBlock([
85
+ factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(media, undefined, undefined, mediaCall)], ts.NodeFlags.Const)),
86
+ factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(update, undefined, undefined, updateCallback)], ts.NodeFlags.Const)),
87
+ factory.createExpressionStatement(factory.createCallExpression(update, undefined, [])),
88
+ factory.createExpressionStatement(listener("addEventListener")),
89
+ factory.createReturnStatement(cleanup)
90
+ ], true))
91
+ const effectCall = factory.createCallExpression(factory.createIdentifier("useEffect"), undefined, [setup, factory.createArrayLiteralExpression()])
92
+ ts.setOriginalNode(effectCall, candidate.declaration.initializer)
93
+ ts.setTextRange(effectCall, candidate.declaration.initializer)
94
+ return [state, factory.createExpressionStatement(effectCall)]
95
+ }
96
+ if (node === externalStoreImport.statement) {
97
+ const clause = node.importClause
98
+ const bindings = clause.namedBindings
99
+ const elements = bindings.elements.filter(entry => entry !== externalStoreImport.entry)
100
+ for (const name of missingHooks) elements.push(factory.createImportSpecifier(false, undefined, factory.createIdentifier(name)))
101
+ return factory.updateImportDeclaration(node, node.modifiers, factory.updateImportClause(clause, false, clause.name, factory.updateNamedImports(bindings, elements)), node.moduleSpecifier, node.attributes)
102
+ }
103
+ return ts.visitEachChild(node, visitor, context)
104
+ }
105
+ return ts.visitNode(sourceFile, visitor)
106
+ }
107
+
108
+ function insideNode(node, root) {
109
+ for (let current = node; current; current = current.parent) if (current === root) return true
110
+ return false
111
+ }
112
+
113
+ export function normalizeNavigatorCapabilityConditions(sourceFile, factory, context) {
114
+ const candidates = new Map()
115
+ let index = 0
116
+ const inspect = node => {
117
+ if (!ts.isVariableStatement(node) || !(node.declarationList.flags & ts.NodeFlags.Const) || node.declarationList.declarations.length !== 1) {
118
+ ts.forEachChild(node, inspect)
119
+ return
120
+ }
121
+ const declaration = node.declarationList.declarations[0]
122
+ const value = declaration.initializer && unwrapExpression(declaration.initializer)
123
+ 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)) {
124
+ ts.forEachChild(node, inspect)
125
+ return
126
+ }
127
+ const owner = nearestFunction(node)
128
+ const topLevelOwner = owner && (owner.parent === sourceFile || ts.isVariableDeclaration(owner.parent) && owner.parent.parent?.parent?.parent === sourceFile)
129
+ 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")
130
+ const references = referenceIdentifiers(owner.body, declaration.name.text)
131
+ const condition = references.length === 1 ? references[0] : undefined
132
+ 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
133
+ if (!structural) throw sourceNodeError(declaration, sourceFile, "Navigator capability values may only control one direct JSX && branch")
134
+ 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}`)
135
+ let setter = `__kSetNavigatorCapability${index++}`
136
+ while (sourceFile.text.includes(setter)) setter = `__kSetNavigatorCapability${index++}`
137
+ candidates.set(node, { declaration, property: unwrapExpression(value.left).text, setter })
138
+ }
139
+ inspect(sourceFile)
140
+ if (!candidates.size) return sourceFile
141
+
142
+ const visitor = node => {
143
+ const candidate = ts.isVariableStatement(node) ? candidates.get(node) : undefined
144
+ if (candidate) {
145
+ const state = factory.createVariableStatement(node.modifiers, factory.createVariableDeclarationList([
146
+ factory.createVariableDeclaration(factory.createArrayBindingPattern([
147
+ factory.createBindingElement(undefined, undefined, candidate.declaration.name),
148
+ factory.createBindingElement(undefined, undefined, candidate.setter)
149
+ ]), undefined, undefined, factory.createCallExpression(factory.createIdentifier("useState"), undefined, [factory.createFalse()]))
150
+ ], ts.NodeFlags.Const))
151
+ const capability = factory.createBinaryExpression(factory.createStringLiteral(candidate.property), factory.createToken(ts.SyntaxKind.InKeyword), factory.createIdentifier("navigator"))
152
+ const setup = factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), factory.createBlock([
153
+ factory.createExpressionStatement(factory.createCallExpression(factory.createIdentifier(candidate.setter), undefined, [capability]))
154
+ ], true))
155
+ const effectCall = factory.createCallExpression(factory.createIdentifier("useEffect"), undefined, [setup, factory.createArrayLiteralExpression()])
156
+ ts.setOriginalNode(effectCall, candidate.declaration.initializer)
157
+ ts.setTextRange(effectCall, candidate.declaration.initializer)
158
+ const effect = factory.createExpressionStatement(effectCall)
159
+ return [state, effect]
160
+ }
161
+ return ts.visitEachChild(node, visitor, context)
162
+ }
163
+ let normalized = ts.visitNode(sourceFile, visitor)
164
+ 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))
165
+ const hookImport = hookImports[0]
166
+ const bindings = hookImport?.importClause.namedBindings
167
+ const imported = new Set(hookImports.flatMap(statement => statement.importClause.namedBindings.elements.map(entry => entry.name.text)))
168
+ const missing = ["useEffect", "useState"].filter(name => !imported.has(name))
169
+ if (!missing.length) return normalized
170
+ 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}`)
171
+ if (hookImport && bindings && ts.isNamedImports(bindings)) {
172
+ const statements = normalized.statements.map(statement => statement === hookImport ? factory.updateImportDeclaration(statement, statement.modifiers, factory.updateImportClause(statement.importClause, false, statement.importClause.name, factory.updateNamedImports(bindings, [
173
+ ...bindings.elements,
174
+ ...missing.map(name => factory.createImportSpecifier(false, undefined, factory.createIdentifier(name)))
175
+ ])), statement.moduleSpecifier, statement.attributes) : statement)
176
+ return factory.updateSourceFile(normalized, statements)
177
+ }
178
+ 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"))
179
+ const statements = [...normalized.statements]
180
+ statements.splice(statements.findLastIndex(statement => ts.isImportDeclaration(statement)) + 1, 0, declaration)
181
+ return factory.updateSourceFile(normalized, statements)
182
+ }
@@ -0,0 +1,126 @@
1
+ import { createHash } from "node:crypto"
2
+ import ts from "typescript"
3
+ import { effectReturns, nearestFunction, referencesIdentifier, sourceNodeError, unwrapExpression } from "./ast-helpers.mjs"
4
+
5
+ const customHookTimerStatePrefix = "__kTimerState_"
6
+ const customHookTimerSetterPrefix = "__kSetTimerState_"
7
+
8
+ export function normalizeCustomHookTimerRefs(sourceFile, factory, context) {
9
+ const timerCall = (node, name) => ts.isCallExpression(node) && (
10
+ ts.isIdentifier(node.expression) && node.expression.text === name ||
11
+ ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.expression) && node.expression.expression.text === "window" && node.expression.name.text === name
12
+ )
13
+ const currentAccess = (node, name) => ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === name && node.name.text === "current"
14
+ const clearStatement = (node, name) => {
15
+ if (!ts.isIfStatement(node) || node.elseStatement || !currentAccess(unwrapExpression(node.expression), name)) return undefined
16
+ const statement = ts.isBlock(node.thenStatement) && node.thenStatement.statements.length === 1 ? node.thenStatement.statements[0] : node.thenStatement
17
+ if (!ts.isExpressionStatement(statement) || !timerCall(statement.expression, "clearTimeout") || statement.expression.arguments.length !== 1 || !currentAccess(unwrapExpression(statement.expression.arguments[0]), name)) return undefined
18
+ return { condition: unwrapExpression(node.expression), argument: unwrapExpression(statement.expression.arguments[0]) }
19
+ }
20
+ const analyze = (hook, hookName) => {
21
+ if (!/^use[A-Z]/.test(hookName) || hook.parameters.length || !hook.body || !ts.isBlock(hook.body)) return undefined
22
+ const returnedStatement = hook.body.statements.at(-1)
23
+ const returned = returnedStatement && ts.isReturnStatement(returnedStatement) && returnedStatement.expression ? unwrapExpression(returnedStatement.expression) : undefined
24
+ if (!returned || !ts.isObjectLiteralExpression(returned)) return undefined
25
+ const returnedNames = new Set(returned.properties.filter(ts.isShorthandPropertyAssignment).map(property => property.name.text))
26
+ const callbacks = new Map()
27
+ const refs = []
28
+ for (const statement of hook.body.statements) {
29
+ if (!ts.isVariableStatement(statement) || !(statement.declarationList.flags & ts.NodeFlags.Const)) continue
30
+ for (const declaration of statement.declarationList.declarations) {
31
+ if (ts.isIdentifier(declaration.name) && declaration.initializer && (ts.isArrowFunction(declaration.initializer) || ts.isFunctionExpression(declaration.initializer))) callbacks.set(declaration.name.text, declaration.initializer)
32
+ 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 })
33
+ }
34
+ }
35
+ const candidates = []
36
+ for (const ref of refs) {
37
+ const assignments = []
38
+ const accesses = []
39
+ const clearStatements = []
40
+ const collect = node => {
41
+ if (currentAccess(node, ref.name)) accesses.push(node)
42
+ if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken && currentAccess(unwrapExpression(node.left), ref.name)) assignments.push(node)
43
+ const clear = clearStatement(node, ref.name)
44
+ if (clear) clearStatements.push({ node, ...clear })
45
+ ts.forEachChild(node, collect)
46
+ }
47
+ collect(hook.body)
48
+ if (assignments.some(assignment => timerCall(unwrapExpression(assignment.right), "setTimeout"))) candidates.push({ ...ref, assignments, accesses, clearStatements })
49
+ }
50
+ if (!candidates.length) return undefined
51
+ if (candidates.length !== 1) throw sourceNodeError(hook, sourceFile, "Relative custom hooks may own only one private timeout ref")
52
+ const timer = candidates[0]
53
+ if (timer.assignments.length !== 1) throw sourceNodeError(timer.declaration, sourceFile, "Private timeout refs require one direct timer.current = setTimeout(...) assignment")
54
+ const assignment = timer.assignments[0]
55
+ const timeout = unwrapExpression(assignment.right)
56
+ const timeoutCallback = timeout.arguments[0]
57
+ const delay = timeout.arguments[1]
58
+ 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")
59
+ const callback = nearestFunction(assignment)
60
+ const callbackName = [...callbacks].find(([, value]) => value === callback)?.[0]
61
+ 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")
62
+ const callbackClear = timer.clearStatements.find(entry => nearestFunction(entry.node) === callback)
63
+ 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")
64
+ const effectCalls = hook.body.statements.flatMap(statement => {
65
+ if (!ts.isExpressionStatement(statement) || !ts.isCallExpression(statement.expression) || !ts.isIdentifier(statement.expression.expression) || statement.expression.expression.text !== "useEffect") return []
66
+ return [statement.expression]
67
+ })
68
+ let cleanupClear
69
+ for (const effect of effectCalls) {
70
+ const [setup, dependencies] = effect.arguments
71
+ if (!(ts.isArrowFunction(setup) || ts.isFunctionExpression(setup)) || !ts.isArrayLiteralExpression(dependencies) || dependencies.elements.length) continue
72
+ const returns = effectReturns(setup)
73
+ if (returns.cleanups.length !== 1) continue
74
+ const cleanup = returns.cleanups[0]
75
+ const entry = timer.clearStatements.find(candidate => nearestFunction(candidate.node) === cleanup)
76
+ if (entry && ts.isBlock(cleanup.body) && cleanup.body.statements.length === 1 && cleanup.body.statements[0] === entry.node) cleanupClear = entry
77
+ }
78
+ if (!cleanupClear) throw sourceNodeError(timer.declaration, sourceFile, "Private timeout refs require one empty-dependency effect that directly clears the timer on cleanup")
79
+ const accepted = new Set([assignment.left, callbackClear.condition, callbackClear.argument, cleanupClear.condition, cleanupClear.argument].map(unwrapExpression))
80
+ const unsupported = timer.accesses.find(access => !accepted.has(access))
81
+ if (unsupported) throw sourceNodeError(unsupported, sourceFile, "Private timeout refs may only be read by their direct replacement and cleanup guards")
82
+ const identity = createHash("sha256").update(`${sourceFile.fileName}:${hook.pos}:${timer.name}`).digest("hex").slice(0, 10)
83
+ const stateName = `${customHookTimerStatePrefix}${identity}`
84
+ const setterName = `${customHookTimerSetterPrefix}${identity}`
85
+ if (referencesIdentifier(hook.body, stateName) || referencesIdentifier(hook.body, setterName)) throw sourceNodeError(timer.declaration, sourceFile, "Private timeout ref conflicts with compiler-owned bindings")
86
+ return { assignment, declaration: timer.declaration, refName: timer.name, returned, stateName, setterName }
87
+ }
88
+ const timerStates = new Set()
89
+ const transform = (hook, hookName) => {
90
+ const timer = analyze(hook, hookName)
91
+ if (!timer) return undefined
92
+ timerStates.add(timer.stateName)
93
+ const timerVisitor = current => {
94
+ if (current === timer.declaration) {
95
+ const binding = factory.createArrayBindingPattern([
96
+ factory.createBindingElement(undefined, undefined, timer.stateName),
97
+ factory.createBindingElement(undefined, undefined, timer.setterName)
98
+ ])
99
+ const initializer = factory.updateCallExpression(current.initializer, factory.createIdentifier("useState"), current.initializer.typeArguments, current.initializer.arguments)
100
+ return factory.updateVariableDeclaration(current, binding, current.exclamationToken, undefined, initializer)
101
+ }
102
+ if (current === timer.assignment) return factory.createCallExpression(factory.createIdentifier(timer.setterName), undefined, [ts.visitNode(current.right, timerVisitor)])
103
+ if (currentAccess(current, timer.refName)) return factory.createIdentifier(timer.stateName)
104
+ if (current === timer.returned) return factory.updateObjectLiteralExpression(current, [
105
+ ...current.properties,
106
+ factory.createShorthandPropertyAssignment(timer.stateName),
107
+ factory.createShorthandPropertyAssignment(timer.setterName)
108
+ ])
109
+ return ts.visitEachChild(current, timerVisitor, context)
110
+ }
111
+ return ts.visitEachChild(hook, timerVisitor, context)
112
+ }
113
+ const visitor = node => {
114
+ if (ts.isFunctionDeclaration(node)) {
115
+ const hookName = node.name?.text ?? (node.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.DefaultKeyword) ? "useDefault" : "")
116
+ const transformed = transform(node, hookName)
117
+ if (transformed) return transformed
118
+ }
119
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && (ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer))) {
120
+ const transformed = transform(node.initializer, node.name.text)
121
+ if (transformed) return factory.updateVariableDeclaration(node, node.name, node.exclamationToken, node.type, transformed)
122
+ }
123
+ return ts.visitEachChild(node, visitor, context)
124
+ }
125
+ return { sourceFile: ts.visitNode(sourceFile, visitor), timerStates }
126
+ }