@kudzujs/core 0.8.14 → 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,338 @@
1
+ import ts from "typescript"
2
+ import { bindingNames, importDeclarationNames, isFunctionLike, isLocalConst, isReferenceIdentifier, isShadowedByParameter, isShadowedIdentifier, loopDeclaresName, nearestFunction, nearestFunctionLike, referenceIdentifiers, sourceNodeError, statementDeclaresName, unwrapExpression } from "./ast-helpers.mjs"
3
+
4
+ export function createReactMigrationPass({ cloneAst, isArrayFromCall, jsxTagName, renderedCollectionSource }) {
5
+ function normalizeReactMigrationSyntax(sourceFile, factory, context, importedCollections = new Set()) {
6
+ const supported = new Set(["createContext", "useContext", "useEffect", "useId", "useReducer", "useRef", "useState"])
7
+ const erased = new Set(["forwardRef", "memo", "useCallback", "useMemo"])
8
+ const aliases = new Map()
9
+ const reactObjects = new Set()
10
+ for (const statement of sourceFile.statements) {
11
+ if (!ts.isImportDeclaration(statement) || statement.importClause?.isTypeOnly || !ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text !== "react") continue
12
+ if (statement.importClause?.name) reactObjects.add(statement.importClause.name.text)
13
+ const bindings = statement.importClause?.namedBindings
14
+ if (bindings && ts.isNamespaceImport(bindings)) reactObjects.add(bindings.name.text)
15
+ if (bindings && ts.isNamedImports(bindings)) for (const entry of bindings.elements) {
16
+ const imported = (entry.propertyName ?? entry.name).text
17
+ if (!entry.isTypeOnly && (supported.has(imported) || erased.has(imported))) aliases.set(entry.name.text, imported)
18
+ else if (!entry.isTypeOnly && /^use[A-Z]/.test(imported)) throw sourceNodeError(entry, sourceFile, `React ${imported} is not supported by Kudzu migration input`)
19
+ }
20
+ }
21
+ if (!aliases.size && !reactObjects.size) return sourceFile
22
+
23
+ const migrationCallName = call => {
24
+ if (ts.isIdentifier(call.expression) && aliases.has(call.expression.text) && !isShadowedIdentifier(call.expression, sourceFile)) return aliases.get(call.expression.text)
25
+ if (ts.isPropertyAccessExpression(call.expression) && ts.isIdentifier(call.expression.expression) && reactObjects.has(call.expression.expression.text) && !isShadowedIdentifier(call.expression.expression, sourceFile)) return call.expression.name.text
26
+ return undefined
27
+ }
28
+ const ownerStateNames = owner => {
29
+ const names = new Set()
30
+ const collect = node => {
31
+ if (node !== owner && isFunctionLike(node)) return
32
+ if (ts.isVariableDeclaration(node) && ts.isArrayBindingPattern(node.name) && node.initializer && ts.isCallExpression(node.initializer) && ["useReducer", "useState"].includes(migrationCallName(node.initializer))) {
33
+ const state = node.name.elements[0]
34
+ if (state && ts.isBindingElement(state) && ts.isIdentifier(state.name)) names.add(state.name.text)
35
+ }
36
+ ts.forEachChild(node, collect)
37
+ }
38
+ collect(owner)
39
+ return names
40
+ }
41
+
42
+ const validate = node => {
43
+ if (ts.isTypeNode(node)) return
44
+ if (ts.isIdentifier(node) && aliases.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, sourceFile) && !(ts.isCallExpression(node.parent) && node.parent.expression === node)) throw sourceNodeError(node, sourceFile, `Aliased React ${aliases.get(node.text)} must be called directly`)
45
+ if (ts.isIdentifier(node) && reactObjects.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, sourceFile) && !(ts.isPropertyAccessExpression(node.parent) && node.parent.expression === node)) throw sourceNodeError(node, sourceFile, "React default or namespace imports may only be used for direct supported members or React.Fragment")
46
+ if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.expression) && reactObjects.has(node.expression.text) && !isShadowedIdentifier(node.expression, sourceFile)) {
47
+ const name = node.name.text
48
+ if (name !== "Fragment" && !(ts.isCallExpression(node.parent) && node.parent.expression === node && (supported.has(name) || erased.has(name)))) throw sourceNodeError(node, sourceFile, `React.${name} is not supported; use a directly supported hook call or React.Fragment`)
49
+ }
50
+ ts.forEachChild(node, validate)
51
+ }
52
+ validate(sourceFile)
53
+
54
+ const memoLocals = new Map()
55
+ const collectMemoLocals = node => {
56
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isCallExpression(node.initializer) && migrationCallName(node.initializer) === "useMemo") {
57
+ if (!isLocalConst(node)) throw sourceNodeError(node, sourceFile, "React useMemo() local values must use const declarations")
58
+ const callback = node.initializer.arguments[0]
59
+ if (callback && (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback))) {
60
+ const expression = lowerReactMemoCollectionExpression(reactMemoExpression(callback), factory)
61
+ const owner = nearestFunction(node)
62
+ if (owner && expression) {
63
+ const entries = memoLocals.get(owner) ?? new Map()
64
+ if (entries.has(node.name.text)) throw sourceNodeError(node.name, sourceFile, `React useMemo() local ${JSON.stringify(node.name.text)} must be unique within its component`)
65
+ entries.set(node.name.text, { declaration: node, expression })
66
+ memoLocals.set(owner, entries)
67
+ }
68
+ }
69
+ }
70
+ ts.forEachChild(node, collectMemoLocals)
71
+ }
72
+ collectMemoLocals(sourceFile)
73
+ const memoLocalIsShadowed = (node, owner, entry) => {
74
+ if (isShadowedByParameter(node, owner)) return true
75
+ const declarationStatement = entry.declaration.parent?.parent
76
+ for (let current = node.parent; current && current !== owner; current = current.parent) {
77
+ if (ts.isFunctionExpression(current) && current.name?.text === node.text) return true
78
+ if (ts.isBlock(current) && current.statements.some(statement => statement !== declarationStatement && statementDeclaresName(statement, node.text))) return true
79
+ if (ts.isCaseBlock(current) && current.clauses.some(clause => clause.statements.some(statement => statement !== declarationStatement && statementDeclaresName(statement, node.text)))) return true
80
+ if (ts.isCatchClause(current) && current.variableDeclaration && bindingNames(current.variableDeclaration.name).includes(node.text)) return true
81
+ if ((ts.isForStatement(current) || ts.isForInStatement(current) || ts.isForOfStatement(current)) && loopDeclaresName(current, node.text)) return true
82
+ }
83
+ return false
84
+ }
85
+ for (const [owner, entries] of memoLocals) for (const [name, entry] of entries) {
86
+ const visit = node => {
87
+ if (ts.isIdentifier(node) && node.text === name && isReferenceIdentifier(node) && nearestFunctionLike(node) !== owner && !memoLocalIsShadowed(node, owner, entry)) throw sourceNodeError(node, sourceFile, `React useMemo() local ${JSON.stringify(name)} cannot be captured by a nested function`)
88
+ ts.forEachChild(node, visit)
89
+ }
90
+ visit(owner.body)
91
+ }
92
+
93
+ const required = new Set()
94
+ const imported = new Set()
95
+ const visitor = node => {
96
+ if (ts.isVariableStatement(node)) {
97
+ const entries = memoLocals.get(nearestFunction(node))
98
+ if (entries) {
99
+ for (const declaration of node.declarationList.declarations) if (ts.isIdentifier(declaration.name) && entries.has(declaration.name.text) && declaration.initializer) ts.visitNode(declaration.initializer, visitor)
100
+ const declarations = node.declarationList.declarations.filter(declaration => !ts.isIdentifier(declaration.name) || !entries.has(declaration.name.text))
101
+ if (!declarations.length) return undefined
102
+ if (declarations.length !== node.declarationList.declarations.length) return factory.updateVariableStatement(node, node.modifiers, factory.updateVariableDeclarationList(node.declarationList, declarations.map(declaration => ts.visitEachChild(declaration, visitor, context))))
103
+ }
104
+ }
105
+ if (ts.isIdentifier(node) && isReferenceIdentifier(node)) {
106
+ const owner = nearestFunctionLike(node)
107
+ const entry = memoLocals.get(owner)?.get(node.text)
108
+ if (entry && !memoLocalIsShadowed(node, owner, entry)) return ts.visitNode(cloneAst(entry.expression, factory, context), visitor)
109
+ }
110
+ if (ts.isCallExpression(node)) {
111
+ const name = migrationCallName(node)
112
+ if (name === "forwardRef") return ts.visitNode(lowerReactForwardRef(node, sourceFile, factory), visitor)
113
+ if (name === "memo") {
114
+ if (node.arguments.length !== 1 || !(ts.isArrowFunction(node.arguments[0]) || ts.isFunctionExpression(node.arguments[0]) || ts.isIdentifier(node.arguments[0]))) throw sourceNodeError(node, sourceFile, "React memo() requires exactly one function component or component identifier")
115
+ if (ts.isIdentifier(node.arguments[0]) && isShadowedIdentifier(node.arguments[0], sourceFile)) throw sourceNodeError(node.arguments[0], sourceFile, "React memo() component identifiers must resolve to an unshadowed same-file top-level function")
116
+ const component = ts.isIdentifier(node.arguments[0]) ? reactMemoComponentExpression(node.arguments[0], sourceFile, factory, context) : node.arguments[0]
117
+ if (!component) throw sourceNodeError(node.arguments[0], sourceFile, "React memo() identifiers must name a same-file top-level function component")
118
+ return ts.visitNode(component, visitor)
119
+ }
120
+ if (name === "useCallback") {
121
+ if (node.arguments.length !== 2 || !ts.isArrayLiteralExpression(node.arguments[1]) || !(ts.isArrowFunction(node.arguments[0]) || ts.isFunctionExpression(node.arguments[0]))) throw sourceNodeError(node, sourceFile, "React useCallback() requires an inline function and a literal dependency array")
122
+ const dependency = node.arguments[1].elements.find(entry => !isReactCallbackDependency(entry))
123
+ if (dependency) throw sourceNodeError(dependency, sourceFile, "React useCallback() dependencies must be identifiers or primitive literals")
124
+ const dependencies = new Set(node.arguments[1].elements.map(unwrapExpression).filter(ts.isIdentifier).map(entry => entry.text))
125
+ const owner = nearestFunction(node)
126
+ const stale = owner && [...ownerStateNames(owner)].find(state => referenceIdentifiers(node.arguments[0], state).length && !dependencies.has(state))
127
+ if (stale) throw sourceNodeError(node.arguments[1], sourceFile, `React useCallback() must list captured state ${JSON.stringify(stale)} as a dependency`)
128
+ return ts.visitNode(node.arguments[0], visitor)
129
+ }
130
+ if (name === "useMemo") {
131
+ if (node.arguments.length !== 2 || !ts.isArrayLiteralExpression(node.arguments[1]) || !(ts.isArrowFunction(node.arguments[0]) || ts.isFunctionExpression(node.arguments[0]))) throw sourceNodeError(node, sourceFile, "React useMemo() requires an inline function and a literal dependency array")
132
+ const callback = node.arguments[0]
133
+ if (callback.parameters.length || callback.asteriskToken || callback.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) throw sourceNodeError(callback, sourceFile, "React useMemo() callback must be synchronous and parameterless")
134
+ const dependency = node.arguments[1].elements.find(entry => !isReactCallbackDependency(entry))
135
+ if (dependency) throw sourceNodeError(dependency, sourceFile, "React useMemo() dependencies must be identifiers or primitive literals")
136
+ const expression = lowerReactMemoCollectionExpression(reactMemoExpression(callback), factory)
137
+ const dependencies = new Set(node.arguments[1].elements.map(unwrapExpression).filter(ts.isIdentifier).map(entry => entry.text))
138
+ const owner = nearestFunction(node)
139
+ const states = owner ? ownerStateNames(owner) : new Set()
140
+ const collection = expression && reactMemoCollection(expression, states, importedCollections, sourceFile)
141
+ if (!expression || !collection && !isPureReactMemoExpression(expression)) throw sourceNodeError(callback.body, sourceFile, "React useMemo() callback must return one pure expression or analyzable collection pipeline")
142
+ if (!collection) {
143
+ const unsupported = [...reactMemoReferenceNames(expression)].find(reference => !states.has(reference))
144
+ if (unsupported) throw sourceNodeError(expression, sourceFile, `React useMemo() pure expressions may only reference direct local state; found ${JSON.stringify(unsupported)}`)
145
+ }
146
+ const collectionDependencies = collection ? new Set([...collection.selectorStates, ...(collection.static ? [] : [collection.state.text])]) : undefined
147
+ const stale = collection
148
+ ? [...collectionDependencies].find(state => !dependencies.has(state))
149
+ : [...states].find(state => referenceIdentifiers(expression, state).length && !dependencies.has(state))
150
+ if (stale) throw sourceNodeError(node.arguments[1], sourceFile, `React useMemo() must list captured state ${JSON.stringify(stale)} as a dependency`)
151
+ return ts.visitNode(expression, visitor)
152
+ }
153
+ if (name && supported.has(name)) {
154
+ required.add(name)
155
+ return factory.updateCallExpression(node, factory.createIdentifier(name), node.typeArguments, ts.visitNodes(node.arguments, visitor))
156
+ }
157
+ }
158
+ if (ts.isImportDeclaration(node) && !node.importClause?.isTypeOnly && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text === "react") {
159
+ const clause = node.importClause
160
+ if (!clause) return node
161
+ let bindings = clause.namedBindings
162
+ if (bindings && ts.isNamedImports(bindings)) {
163
+ const entries = []
164
+ for (const entry of bindings.elements) {
165
+ const name = (entry.propertyName ?? entry.name).text
166
+ if (entry.isTypeOnly) continue
167
+ if (!entry.isTypeOnly && erased.has(name)) continue
168
+ if (!entry.isTypeOnly && supported.has(name)) {
169
+ if (imported.has(name)) continue
170
+ imported.add(name)
171
+ required.add(name)
172
+ entries.push(factory.createImportSpecifier(false, undefined, factory.createIdentifier(name)))
173
+ } else {
174
+ entries.push(entry)
175
+ }
176
+ }
177
+ bindings = entries.length ? factory.updateNamedImports(bindings, entries) : undefined
178
+ }
179
+ if (!clause.name && !bindings) return undefined
180
+ return factory.updateImportDeclaration(node, node.modifiers, factory.updateImportClause(clause, clause.isTypeOnly, clause.name, bindings), node.moduleSpecifier, node.attributes)
181
+ }
182
+ return ts.visitEachChild(node, visitor, context)
183
+ }
184
+ let normalized = ts.visitNode(sourceFile, visitor)
185
+ const missing = [...required].filter(name => !imported.has(name)).sort()
186
+ if (!missing.length) return normalized
187
+ for (const name of missing) {
188
+ const collision = sourceFile.statements.some(statement => statementDeclaresName(statement, name) || ts.isImportDeclaration(statement) && importDeclarationNames(statement).includes(name) && statement.moduleSpecifier.text !== "react")
189
+ if (collision) throw sourceNodeError(sourceFile, sourceFile, `React.${name} cannot be normalized because ${JSON.stringify(name)} is already declared`)
190
+ }
191
+ const declaration = factory.createImportDeclaration(undefined, factory.createImportClause(false, undefined, factory.createNamedImports(missing.map(name => factory.createImportSpecifier(false, undefined, factory.createIdentifier(name))))), factory.createStringLiteral("react"))
192
+ const statements = [...normalized.statements]
193
+ const lastImport = statements.findLastIndex(statement => ts.isImportDeclaration(statement))
194
+ statements.splice(lastImport + 1, 0, declaration)
195
+ normalized = factory.updateSourceFile(normalized, statements)
196
+ return normalized
197
+ }
198
+
199
+ function lowerReactForwardRef(call, sourceFile, factory) {
200
+ const declaration = call.parent
201
+ const statement = declaration?.parent?.parent
202
+ if (!ts.isVariableDeclaration(declaration) || declaration.initializer !== call || !ts.isIdentifier(declaration.name) || !statement || !ts.isVariableStatement(statement) || (statement.declarationList.flags & ts.NodeFlags.Const) === 0 || statement.parent !== sourceFile) {
203
+ throw sourceNodeError(call, sourceFile, "React forwardRef() must directly initialize one top-level const component")
204
+ }
205
+ if (call.arguments.length !== 1 || !ts.isArrowFunction(call.arguments[0]) && !ts.isFunctionExpression(call.arguments[0])) throw sourceNodeError(call, sourceFile, "React forwardRef() requires exactly one inline render function")
206
+ const callback = call.arguments[0]
207
+ if (callback.asteriskToken || callback.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) throw sourceNodeError(callback, sourceFile, "React forwardRef() render function must be synchronous and cannot be a generator")
208
+ if (callback.parameters.length !== 2) throw sourceNodeError(callback, sourceFile, "React forwardRef() render function must declare exactly (props, ref)")
209
+ const [props, ref] = callback.parameters
210
+ if (props.dotDotDotToken || props.initializer || !ts.isIdentifier(props.name) && !ts.isObjectBindingPattern(props.name)) throw sourceNodeError(props, sourceFile, "React forwardRef() props must use one identifier or a flat object binding")
211
+ if (ref.dotDotDotToken || ref.initializer || !ts.isIdentifier(ref.name)) throw sourceNodeError(ref, sourceFile, "React forwardRef() ref parameter must be one identifier")
212
+
213
+ let elements
214
+ if (ts.isIdentifier(props.name)) {
215
+ elements = [
216
+ factory.createBindingElement(undefined, factory.createIdentifier("ref"), factory.createIdentifier(ref.name.text)),
217
+ factory.createBindingElement(factory.createToken(ts.SyntaxKind.DotDotDotToken), undefined, factory.createIdentifier(props.name.text))
218
+ ]
219
+ } else {
220
+ for (const element of props.name.elements) {
221
+ const property = (element.propertyName ?? element.name)
222
+ if (!ts.isIdentifier(element.name) || property.text === "ref") throw sourceNodeError(element, sourceFile, property.text === "ref" ? "React forwardRef() props must not declare ref; Kudzu supplies ref through the second parameter" : "React forwardRef() props must use one identifier or a flat object binding")
223
+ }
224
+ const rest = props.name.elements.findIndex(element => Boolean(element.dotDotDotToken))
225
+ elements = [...props.name.elements]
226
+ elements.splice(rest < 0 ? elements.length : rest, 0, factory.createBindingElement(undefined, factory.createIdentifier("ref"), factory.createIdentifier(ref.name.text)))
227
+ }
228
+
229
+ const last = ts.isBlock(callback.body) ? callback.body.statements.at(-1) : undefined
230
+ let returnCount = 0
231
+ const countReturns = node => {
232
+ if (node !== callback.body && isFunctionLike(node)) return
233
+ if (ts.isReturnStatement(node)) returnCount++
234
+ ts.forEachChild(node, countReturns)
235
+ }
236
+ countReturns(callback.body)
237
+ const returned = ts.isBlock(callback.body)
238
+ ? last && ts.isReturnStatement(last) ? last.expression : undefined
239
+ : callback.body
240
+ const root = returned && unwrapExpression(returned)
241
+ const tag = root && jsxTagName(root)
242
+ if ((ts.isBlock(callback.body) && returnCount !== 1) || !root || !ts.isJsxElement(root) && !ts.isJsxSelfClosingElement(root) || !ts.isIdentifier(tag) || tag.text[0] !== tag.text[0].toLowerCase()) throw sourceNodeError(callback.body, sourceFile, "React forwardRef() render function must directly return one intrinsic JSX element")
243
+ const attributes = ts.isJsxElement(root) ? root.openingElement.attributes : root.attributes
244
+ const forwarded = attributes.properties.filter(attribute => ts.isJsxAttribute(attribute) && attribute.name.text === "ref" && ts.isJsxExpression(attribute.initializer) && ts.isIdentifier(attribute.initializer.expression) && attribute.initializer.expression.text === ref.name.text)
245
+ if (forwarded.length !== 1 || referenceIdentifiers(callback.body, ref.name.text).length !== 1) throw sourceNodeError(ref, sourceFile, "React forwardRef() ref must be forwarded exactly once as ref={ref} on the direct intrinsic root")
246
+
247
+ const parameter = factory.updateParameterDeclaration(props, props.modifiers, undefined, factory.createObjectBindingPattern(elements), props.questionToken, props.type, undefined)
248
+ return ts.isArrowFunction(callback)
249
+ ? factory.updateArrowFunction(callback, callback.modifiers, callback.typeParameters, [parameter], callback.type, callback.equalsGreaterThanToken, callback.body)
250
+ : factory.updateFunctionExpression(callback, callback.modifiers, undefined, callback.name, callback.typeParameters, [parameter], callback.type, callback.body)
251
+ }
252
+
253
+ function validateUseIdSyntax(sourceFile) {
254
+ const imported = sourceFile.statements.some(statement => ts.isImportDeclaration(statement) && ["@kudzujs/core", "react"].includes(statement.moduleSpecifier.text) && statement.importClause?.namedBindings && ts.isNamedImports(statement.importClause.namedBindings) && statement.importClause.namedBindings.elements.some(entry => !entry.propertyName && entry.name.text === "useId"))
255
+ if (!imported) return
256
+ const visit = node => {
257
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "useId" && !isShadowedIdentifier(node.expression, sourceFile)) {
258
+ if (node.arguments.length) throw sourceNodeError(node, sourceFile, "useId() does not accept arguments")
259
+ const declaration = node.parent
260
+ const statement = declaration?.parent?.parent
261
+ const owner = nearestFunction(node)
262
+ if (!ts.isVariableDeclaration(declaration) || declaration.initializer !== node || !ts.isIdentifier(declaration.name) || !statement || !ts.isVariableStatement(statement) || (statement.declarationList.flags & ts.NodeFlags.Const) === 0 || !owner || !ts.isBlock(owner.body) || statement.parent !== owner.body) {
263
+ throw sourceNodeError(node, sourceFile, "useId() must be assigned to one top-level const identifier in a component")
264
+ }
265
+ }
266
+ ts.forEachChild(node, visit)
267
+ }
268
+ visit(sourceFile)
269
+ }
270
+
271
+ function isReactCallbackDependency(node) {
272
+ node = unwrapExpression(node)
273
+ return ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node) || node.kind === ts.SyntaxKind.TrueKeyword || node.kind === ts.SyntaxKind.FalseKeyword || node.kind === ts.SyntaxKind.NullKeyword
274
+ }
275
+
276
+ function lowerReactMemoCollectionExpression(expression, factory) {
277
+ if (!expression) return undefined
278
+ const visit = node => {
279
+ if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "map" && node.arguments.length === 1) {
280
+ return factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("Array"), "from"), undefined, [visit(node.expression.expression), node.arguments[0]])
281
+ }
282
+ if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && ["filter", "flatMap", "slice", "toSorted"].includes(node.expression.name.text)) {
283
+ return factory.updateCallExpression(node, factory.updatePropertyAccessExpression(node.expression, visit(node.expression.expression), node.expression.name), node.typeArguments, node.arguments)
284
+ }
285
+ if (isArrayFromCall(node)) return factory.updateCallExpression(node, node.expression, node.typeArguments, [visit(node.arguments[0]), ...node.arguments.slice(1)])
286
+ return node
287
+ }
288
+ return visit(expression)
289
+ }
290
+
291
+ function reactMemoCollection(expression, states, importedCollections, sourceFile) {
292
+ const setters = new Map([...states].map(state => [state, state]))
293
+ const fail = (node, message) => { throw sourceNodeError(node, sourceFile, message) }
294
+ return renderedCollectionSource(expression, setters, undefined, fail, new Set(), importedCollections, states)
295
+ }
296
+
297
+ function reactMemoComponentExpression(identifier, sourceFile, factory, context) {
298
+ for (const statement of sourceFile.statements) {
299
+ if (ts.isFunctionDeclaration(statement) && statement.name?.text === identifier.text && statement.body) {
300
+ const clone = cloneAst(statement, factory, context)
301
+ return factory.createFunctionExpression(clone.modifiers?.filter(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword), clone.asteriskToken, clone.name, clone.typeParameters, clone.parameters, clone.type, clone.body)
302
+ }
303
+ if (!ts.isVariableStatement(statement)) continue
304
+ const declaration = statement.declarationList.declarations.find(entry => ts.isIdentifier(entry.name) && entry.name.text === identifier.text)
305
+ if (declaration?.initializer && (ts.isArrowFunction(declaration.initializer) || ts.isFunctionExpression(declaration.initializer))) return cloneAst(declaration.initializer, factory, context)
306
+ }
307
+ return undefined
308
+ }
309
+
310
+ function isPureReactMemoExpression(node) {
311
+ node = unwrapExpression(node)
312
+ if (ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) || node.kind === ts.SyntaxKind.TrueKeyword || node.kind === ts.SyntaxKind.FalseKeyword || node.kind === ts.SyntaxKind.NullKeyword) return true
313
+ if (ts.isParenthesizedExpression(node)) return isPureReactMemoExpression(node.expression)
314
+ if (ts.isPrefixUnaryExpression(node)) return ![ts.SyntaxKind.PlusPlusToken, ts.SyntaxKind.MinusMinusToken].includes(node.operator) && isPureReactMemoExpression(node.operand)
315
+ if (ts.isBinaryExpression(node)) return node.operatorToken.kind < ts.SyntaxKind.FirstAssignment && isPureReactMemoExpression(node.left) && isPureReactMemoExpression(node.right)
316
+ if (ts.isConditionalExpression(node)) return isPureReactMemoExpression(node.condition) && isPureReactMemoExpression(node.whenTrue) && isPureReactMemoExpression(node.whenFalse)
317
+ if (ts.isTemplateExpression(node)) return node.templateSpans.every(span => isPureReactMemoExpression(span.expression))
318
+ return false
319
+ }
320
+
321
+ function reactMemoReferenceNames(root) {
322
+ const names = new Set()
323
+ const visit = node => {
324
+ if (ts.isIdentifier(node) && isReferenceIdentifier(node)) names.add(node.text)
325
+ ts.forEachChild(node, visit)
326
+ }
327
+ visit(root)
328
+ return names
329
+ }
330
+
331
+ return { normalizeReactMigrationSyntax, validateUseIdSyntax }
332
+ }
333
+
334
+ export function reactMemoExpression(callback) {
335
+ if (!ts.isBlock(callback.body)) return callback.body
336
+ if (callback.body.statements.length !== 1 || !ts.isReturnStatement(callback.body.statements[0])) return undefined
337
+ return callback.body.statements[0].expression
338
+ }
@@ -0,0 +1,96 @@
1
+ import ts from "typescript"
2
+ import { isFunctionLike } from "./ast-helpers.mjs"
3
+
4
+ export function normalizeRenderControlFlow(sourceFile, factory, context) {
5
+ const normalizeStatements = statements => {
6
+ const nested = statements.map(statement => ts.visitEachChild(statement, visitNested, context))
7
+ const assigned = []
8
+ for (let index = 0; index < nested.length; index++) {
9
+ const statement = nested[index]
10
+ const next = nested[index + 1]
11
+ const declaration = singleUninitializedLet(statement)
12
+ const assignment = declaration && next && assignmentConditional(next, declaration.name.text, factory)
13
+ if (declaration && assignment) {
14
+ const updated = factory.updateVariableDeclaration(declaration, declaration.name, declaration.exclamationToken, declaration.type, assignment)
15
+ const list = factory.createVariableDeclarationList([updated], ts.NodeFlags.Const)
16
+ assigned.push(factory.updateVariableStatement(statement, statement.modifiers, list))
17
+ index++
18
+ } else {
19
+ assigned.push(statement)
20
+ }
21
+ }
22
+
23
+ if (!assigned.length) return assigned
24
+ const finalIf = returnConditional(assigned.at(-1), factory)
25
+ if (finalIf) return [...assigned.slice(0, -1), factory.createReturnStatement(finalIf)]
26
+ if (!ts.isReturnStatement(assigned.at(-1)) || !assigned.at(-1).expression) return assigned
27
+ let expression = assigned.at(-1).expression
28
+ let start = assigned.length - 1
29
+ while (start > 0) {
30
+ const previous = assigned[start - 1]
31
+ if (!ts.isIfStatement(previous) || previous.elseStatement) break
32
+ const truthy = returnOnlyExpression(previous.thenStatement)
33
+ if (!truthy) break
34
+ expression = factory.createConditionalExpression(previous.expression, factory.createToken(ts.SyntaxKind.QuestionToken), truthy, factory.createToken(ts.SyntaxKind.ColonToken), expression)
35
+ start--
36
+ }
37
+ return start === assigned.length - 1 ? assigned : [...assigned.slice(0, start), factory.createReturnStatement(expression)]
38
+ }
39
+
40
+ const visitNested = node => {
41
+ if (ts.isBlock(node)) return factory.updateBlock(node, normalizeStatements([...node.statements]))
42
+ if (isFunctionLike(node) && ts.isBlock(node.body)) {
43
+ if (!isRenderFunction(node)) return node
44
+ const body = factory.updateBlock(node.body, normalizeStatements([...node.body.statements]))
45
+ if (ts.isFunctionDeclaration(node)) return factory.updateFunctionDeclaration(node, node.modifiers, node.asteriskToken, node.name, node.typeParameters, node.parameters, node.type, body)
46
+ if (ts.isFunctionExpression(node)) return factory.updateFunctionExpression(node, node.modifiers, node.asteriskToken, node.name, node.typeParameters, node.parameters, node.type, body)
47
+ if (ts.isArrowFunction(node)) return factory.updateArrowFunction(node, node.modifiers, node.typeParameters, node.parameters, node.type, node.equalsGreaterThanToken, body)
48
+ }
49
+ return ts.visitEachChild(node, visitNested, context)
50
+ }
51
+
52
+ return ts.visitEachChild(sourceFile, visitNested, context)
53
+ }
54
+
55
+ function isRenderFunction(node) {
56
+ if (ts.isFunctionDeclaration(node)) return node.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.DefaultKeyword) || Boolean(node.name && /^[A-Z]/.test(node.name.text))
57
+ const declaration = node.parent
58
+ return ts.isVariableDeclaration(declaration) && ts.isIdentifier(declaration.name) && /^[A-Z]/.test(declaration.name.text)
59
+ }
60
+
61
+ function singleUninitializedLet(statement) {
62
+ if (!ts.isVariableStatement(statement) || (statement.declarationList.flags & ts.NodeFlags.Let) === 0 || statement.declarationList.declarations.length !== 1) return undefined
63
+ const declaration = statement.declarationList.declarations[0]
64
+ return ts.isIdentifier(declaration.name) && !declaration.initializer ? declaration : undefined
65
+ }
66
+
67
+ function assignmentConditional(statement, name, factory) {
68
+ if (!ts.isIfStatement(statement) || !statement.elseStatement) return undefined
69
+ const truthy = assignmentOnlyExpression(statement.thenStatement, name)
70
+ const falsy = ts.isIfStatement(statement.elseStatement)
71
+ ? assignmentConditional(statement.elseStatement, name, factory)
72
+ : assignmentOnlyExpression(statement.elseStatement, name)
73
+ if (!truthy || !falsy) return undefined
74
+ return factory.createConditionalExpression(statement.expression, factory.createToken(ts.SyntaxKind.QuestionToken), truthy, factory.createToken(ts.SyntaxKind.ColonToken), falsy)
75
+ }
76
+
77
+ function assignmentOnlyExpression(statement, name) {
78
+ const candidate = ts.isBlock(statement) && statement.statements.length === 1 ? statement.statements[0] : statement
79
+ if (!ts.isExpressionStatement(candidate) || !ts.isBinaryExpression(candidate.expression) || candidate.expression.operatorToken.kind !== ts.SyntaxKind.EqualsToken || !ts.isIdentifier(candidate.expression.left) || candidate.expression.left.text !== name) return undefined
80
+ return candidate.expression.right
81
+ }
82
+
83
+ function returnConditional(statement, factory) {
84
+ if (!ts.isIfStatement(statement) || !statement.elseStatement) return undefined
85
+ const truthy = returnOnlyExpression(statement.thenStatement)
86
+ const falsy = ts.isIfStatement(statement.elseStatement)
87
+ ? returnConditional(statement.elseStatement, factory)
88
+ : returnOnlyExpression(statement.elseStatement)
89
+ if (!truthy || !falsy) return undefined
90
+ return factory.createConditionalExpression(statement.expression, factory.createToken(ts.SyntaxKind.QuestionToken), truthy, factory.createToken(ts.SyntaxKind.ColonToken), falsy)
91
+ }
92
+
93
+ function returnOnlyExpression(statement) {
94
+ const candidate = ts.isBlock(statement) && statement.statements.length === 1 ? statement.statements[0] : statement
95
+ return ts.isReturnStatement(candidate) && candidate.expression ? candidate.expression : undefined
96
+ }