@kudzujs/core 0.8.14 → 0.8.16

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.
Files changed (32) hide show
  1. package/MIGRATION_ROADMAP.md +247 -0
  2. package/PERFORMANCE.md +212 -0
  3. package/README.md +34 -6
  4. package/RELEASES.md +64 -0
  5. package/docs/next-architecture/README.md +42 -0
  6. package/docs/next-architecture/compiler-current-architecture.md +71 -0
  7. package/docs/next-architecture/goal-a-compiler-foundation.md +152 -0
  8. package/docs/next-architecture/goal-b-optimization-benchmarks.md +59 -0
  9. package/docs/next-architecture/goal-c-state-resource-research.md +50 -0
  10. package/docs/next-architecture/goal-d-routing-compatibility-decisions.md +51 -0
  11. package/docs/next-architecture/performance-gates.md +50 -0
  12. package/docs/next-architecture/versioning.md +42 -0
  13. package/framework/README.md +23 -2
  14. package/framework/build.mjs +285 -3569
  15. package/framework/compiler/animation-frame-pass.mjs +103 -0
  16. package/framework/compiler/ast-helpers.mjs +181 -0
  17. package/framework/compiler/browser-signal-passes.mjs +182 -0
  18. package/framework/compiler/collection-analysis.mjs +187 -0
  19. package/framework/compiler/custom-hook-timer-pass.mjs +126 -0
  20. package/framework/compiler/descriptor-session.mjs +222 -0
  21. package/framework/compiler/effect-codegen.mjs +884 -0
  22. package/framework/compiler/event-command-pass.mjs +35 -0
  23. package/framework/compiler/handler-codegen.mjs +296 -0
  24. package/framework/compiler/normalization-pipeline.mjs +9 -0
  25. package/framework/compiler/react-migration-pass.mjs +339 -0
  26. package/framework/compiler/render-control-pass.mjs +96 -0
  27. package/framework/compiler/route-capability-planner.mjs +118 -0
  28. package/framework/compiler/router-pass.mjs +245 -0
  29. package/framework/compiler/worker-compiler.mjs +163 -0
  30. package/framework/compiler/zustand-pass.mjs +95 -0
  31. package/framework/dev-server.mjs +244 -0
  32. package/package.json +4 -1
@@ -0,0 +1,339 @@
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
+ import { analyzeCollectionPipeline, isArrayFromCall } from "./collection-analysis.mjs"
4
+
5
+ export function createReactMigrationPass({ cloneAst, jsxTagName }) {
6
+ function normalizeReactMigrationSyntax(sourceFile, factory, context, importedCollections = new Set()) {
7
+ const supported = new Set(["createContext", "useContext", "useEffect", "useId", "useReducer", "useRef", "useState"])
8
+ const erased = new Set(["forwardRef", "memo", "useCallback", "useMemo"])
9
+ const aliases = new Map()
10
+ const reactObjects = new Set()
11
+ for (const statement of sourceFile.statements) {
12
+ if (!ts.isImportDeclaration(statement) || statement.importClause?.isTypeOnly || !ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text !== "react") continue
13
+ if (statement.importClause?.name) reactObjects.add(statement.importClause.name.text)
14
+ const bindings = statement.importClause?.namedBindings
15
+ if (bindings && ts.isNamespaceImport(bindings)) reactObjects.add(bindings.name.text)
16
+ if (bindings && ts.isNamedImports(bindings)) for (const entry of bindings.elements) {
17
+ const imported = (entry.propertyName ?? entry.name).text
18
+ if (!entry.isTypeOnly && (supported.has(imported) || erased.has(imported))) aliases.set(entry.name.text, imported)
19
+ else if (!entry.isTypeOnly && /^use[A-Z]/.test(imported)) throw sourceNodeError(entry, sourceFile, `React ${imported} is not supported by Kudzu migration input`)
20
+ }
21
+ }
22
+ if (!aliases.size && !reactObjects.size) return sourceFile
23
+
24
+ const migrationCallName = call => {
25
+ if (ts.isIdentifier(call.expression) && aliases.has(call.expression.text) && !isShadowedIdentifier(call.expression, sourceFile)) return aliases.get(call.expression.text)
26
+ 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
27
+ return undefined
28
+ }
29
+ const ownerStateNames = owner => {
30
+ const names = new Set()
31
+ const collect = node => {
32
+ if (node !== owner && isFunctionLike(node)) return
33
+ if (ts.isVariableDeclaration(node) && ts.isArrayBindingPattern(node.name) && node.initializer && ts.isCallExpression(node.initializer) && ["useReducer", "useState"].includes(migrationCallName(node.initializer))) {
34
+ const state = node.name.elements[0]
35
+ if (state && ts.isBindingElement(state) && ts.isIdentifier(state.name)) names.add(state.name.text)
36
+ }
37
+ ts.forEachChild(node, collect)
38
+ }
39
+ collect(owner)
40
+ return names
41
+ }
42
+
43
+ const validate = node => {
44
+ if (ts.isTypeNode(node)) return
45
+ 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`)
46
+ 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")
47
+ if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.expression) && reactObjects.has(node.expression.text) && !isShadowedIdentifier(node.expression, sourceFile)) {
48
+ const name = node.name.text
49
+ 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`)
50
+ }
51
+ ts.forEachChild(node, validate)
52
+ }
53
+ validate(sourceFile)
54
+
55
+ const memoLocals = new Map()
56
+ const collectMemoLocals = node => {
57
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isCallExpression(node.initializer) && migrationCallName(node.initializer) === "useMemo") {
58
+ if (!isLocalConst(node)) throw sourceNodeError(node, sourceFile, "React useMemo() local values must use const declarations")
59
+ const callback = node.initializer.arguments[0]
60
+ if (callback && (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback))) {
61
+ const expression = lowerReactMemoCollectionExpression(reactMemoExpression(callback), factory)
62
+ const owner = nearestFunction(node)
63
+ if (owner && expression) {
64
+ const entries = memoLocals.get(owner) ?? new Map()
65
+ 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`)
66
+ entries.set(node.name.text, { declaration: node, expression })
67
+ memoLocals.set(owner, entries)
68
+ }
69
+ }
70
+ }
71
+ ts.forEachChild(node, collectMemoLocals)
72
+ }
73
+ collectMemoLocals(sourceFile)
74
+ const memoLocalIsShadowed = (node, owner, entry) => {
75
+ if (isShadowedByParameter(node, owner)) return true
76
+ const declarationStatement = entry.declaration.parent?.parent
77
+ for (let current = node.parent; current && current !== owner; current = current.parent) {
78
+ if (ts.isFunctionExpression(current) && current.name?.text === node.text) return true
79
+ if (ts.isBlock(current) && current.statements.some(statement => statement !== declarationStatement && statementDeclaresName(statement, node.text))) return true
80
+ if (ts.isCaseBlock(current) && current.clauses.some(clause => clause.statements.some(statement => statement !== declarationStatement && statementDeclaresName(statement, node.text)))) return true
81
+ if (ts.isCatchClause(current) && current.variableDeclaration && bindingNames(current.variableDeclaration.name).includes(node.text)) return true
82
+ if ((ts.isForStatement(current) || ts.isForInStatement(current) || ts.isForOfStatement(current)) && loopDeclaresName(current, node.text)) return true
83
+ }
84
+ return false
85
+ }
86
+ for (const [owner, entries] of memoLocals) for (const [name, entry] of entries) {
87
+ const visit = node => {
88
+ 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`)
89
+ ts.forEachChild(node, visit)
90
+ }
91
+ visit(owner.body)
92
+ }
93
+
94
+ const required = new Set()
95
+ const imported = new Set()
96
+ const visitor = node => {
97
+ if (ts.isVariableStatement(node)) {
98
+ const entries = memoLocals.get(nearestFunction(node))
99
+ if (entries) {
100
+ for (const declaration of node.declarationList.declarations) if (ts.isIdentifier(declaration.name) && entries.has(declaration.name.text) && declaration.initializer) ts.visitNode(declaration.initializer, visitor)
101
+ const declarations = node.declarationList.declarations.filter(declaration => !ts.isIdentifier(declaration.name) || !entries.has(declaration.name.text))
102
+ if (!declarations.length) return undefined
103
+ 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))))
104
+ }
105
+ }
106
+ if (ts.isIdentifier(node) && isReferenceIdentifier(node)) {
107
+ const owner = nearestFunctionLike(node)
108
+ const entry = memoLocals.get(owner)?.get(node.text)
109
+ if (entry && !memoLocalIsShadowed(node, owner, entry)) return ts.visitNode(cloneAst(entry.expression, factory, context), visitor)
110
+ }
111
+ if (ts.isCallExpression(node)) {
112
+ const name = migrationCallName(node)
113
+ if (name === "forwardRef") return ts.visitNode(lowerReactForwardRef(node, sourceFile, factory), visitor)
114
+ if (name === "memo") {
115
+ 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")
116
+ 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")
117
+ const component = ts.isIdentifier(node.arguments[0]) ? reactMemoComponentExpression(node.arguments[0], sourceFile, factory, context) : node.arguments[0]
118
+ if (!component) throw sourceNodeError(node.arguments[0], sourceFile, "React memo() identifiers must name a same-file top-level function component")
119
+ return ts.visitNode(component, visitor)
120
+ }
121
+ if (name === "useCallback") {
122
+ 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")
123
+ const dependency = node.arguments[1].elements.find(entry => !isReactCallbackDependency(entry))
124
+ if (dependency) throw sourceNodeError(dependency, sourceFile, "React useCallback() dependencies must be identifiers or primitive literals")
125
+ const dependencies = new Set(node.arguments[1].elements.map(unwrapExpression).filter(ts.isIdentifier).map(entry => entry.text))
126
+ const owner = nearestFunction(node)
127
+ const stale = owner && [...ownerStateNames(owner)].find(state => referenceIdentifiers(node.arguments[0], state).length && !dependencies.has(state))
128
+ if (stale) throw sourceNodeError(node.arguments[1], sourceFile, `React useCallback() must list captured state ${JSON.stringify(stale)} as a dependency`)
129
+ return ts.visitNode(node.arguments[0], visitor)
130
+ }
131
+ if (name === "useMemo") {
132
+ 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")
133
+ const callback = node.arguments[0]
134
+ 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")
135
+ const dependency = node.arguments[1].elements.find(entry => !isReactCallbackDependency(entry))
136
+ if (dependency) throw sourceNodeError(dependency, sourceFile, "React useMemo() dependencies must be identifiers or primitive literals")
137
+ const expression = lowerReactMemoCollectionExpression(reactMemoExpression(callback), factory)
138
+ const dependencies = new Set(node.arguments[1].elements.map(unwrapExpression).filter(ts.isIdentifier).map(entry => entry.text))
139
+ const owner = nearestFunction(node)
140
+ const states = owner ? ownerStateNames(owner) : new Set()
141
+ const collection = expression && reactMemoCollection(expression, states, importedCollections, sourceFile)
142
+ if (!expression || !collection && !isPureReactMemoExpression(expression)) throw sourceNodeError(callback.body, sourceFile, "React useMemo() callback must return one pure expression or analyzable collection pipeline")
143
+ if (!collection) {
144
+ const unsupported = [...reactMemoReferenceNames(expression)].find(reference => !states.has(reference))
145
+ if (unsupported) throw sourceNodeError(expression, sourceFile, `React useMemo() pure expressions may only reference direct local state; found ${JSON.stringify(unsupported)}`)
146
+ }
147
+ const collectionDependencies = collection ? new Set([...collection.selectorStates, ...(collection.static ? [] : [collection.state.text])]) : undefined
148
+ const stale = collection
149
+ ? [...collectionDependencies].find(state => !dependencies.has(state))
150
+ : [...states].find(state => referenceIdentifiers(expression, state).length && !dependencies.has(state))
151
+ if (stale) throw sourceNodeError(node.arguments[1], sourceFile, `React useMemo() must list captured state ${JSON.stringify(stale)} as a dependency`)
152
+ return ts.visitNode(expression, visitor)
153
+ }
154
+ if (name && supported.has(name)) {
155
+ required.add(name)
156
+ return factory.updateCallExpression(node, factory.createIdentifier(name), node.typeArguments, ts.visitNodes(node.arguments, visitor))
157
+ }
158
+ }
159
+ if (ts.isImportDeclaration(node) && !node.importClause?.isTypeOnly && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text === "react") {
160
+ const clause = node.importClause
161
+ if (!clause) return node
162
+ let bindings = clause.namedBindings
163
+ if (bindings && ts.isNamedImports(bindings)) {
164
+ const entries = []
165
+ for (const entry of bindings.elements) {
166
+ const name = (entry.propertyName ?? entry.name).text
167
+ if (entry.isTypeOnly) continue
168
+ if (!entry.isTypeOnly && erased.has(name)) continue
169
+ if (!entry.isTypeOnly && supported.has(name)) {
170
+ if (imported.has(name)) continue
171
+ imported.add(name)
172
+ required.add(name)
173
+ entries.push(factory.createImportSpecifier(false, undefined, factory.createIdentifier(name)))
174
+ } else {
175
+ entries.push(entry)
176
+ }
177
+ }
178
+ bindings = entries.length ? factory.updateNamedImports(bindings, entries) : undefined
179
+ }
180
+ if (!clause.name && !bindings) return undefined
181
+ return factory.updateImportDeclaration(node, node.modifiers, factory.updateImportClause(clause, clause.isTypeOnly, clause.name, bindings), node.moduleSpecifier, node.attributes)
182
+ }
183
+ return ts.visitEachChild(node, visitor, context)
184
+ }
185
+ let normalized = ts.visitNode(sourceFile, visitor)
186
+ const missing = [...required].filter(name => !imported.has(name)).sort()
187
+ if (!missing.length) return normalized
188
+ for (const name of missing) {
189
+ const collision = sourceFile.statements.some(statement => statementDeclaresName(statement, name) || ts.isImportDeclaration(statement) && importDeclarationNames(statement).includes(name) && statement.moduleSpecifier.text !== "react")
190
+ if (collision) throw sourceNodeError(sourceFile, sourceFile, `React.${name} cannot be normalized because ${JSON.stringify(name)} is already declared`)
191
+ }
192
+ const declaration = factory.createImportDeclaration(undefined, factory.createImportClause(false, undefined, factory.createNamedImports(missing.map(name => factory.createImportSpecifier(false, undefined, factory.createIdentifier(name))))), factory.createStringLiteral("react"))
193
+ const statements = [...normalized.statements]
194
+ const lastImport = statements.findLastIndex(statement => ts.isImportDeclaration(statement))
195
+ statements.splice(lastImport + 1, 0, declaration)
196
+ normalized = factory.updateSourceFile(normalized, statements)
197
+ return normalized
198
+ }
199
+
200
+ function lowerReactForwardRef(call, sourceFile, factory) {
201
+ const declaration = call.parent
202
+ const statement = declaration?.parent?.parent
203
+ 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) {
204
+ throw sourceNodeError(call, sourceFile, "React forwardRef() must directly initialize one top-level const component")
205
+ }
206
+ 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")
207
+ const callback = call.arguments[0]
208
+ 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")
209
+ if (callback.parameters.length !== 2) throw sourceNodeError(callback, sourceFile, "React forwardRef() render function must declare exactly (props, ref)")
210
+ const [props, ref] = callback.parameters
211
+ 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")
212
+ if (ref.dotDotDotToken || ref.initializer || !ts.isIdentifier(ref.name)) throw sourceNodeError(ref, sourceFile, "React forwardRef() ref parameter must be one identifier")
213
+
214
+ let elements
215
+ if (ts.isIdentifier(props.name)) {
216
+ elements = [
217
+ factory.createBindingElement(undefined, factory.createIdentifier("ref"), factory.createIdentifier(ref.name.text)),
218
+ factory.createBindingElement(factory.createToken(ts.SyntaxKind.DotDotDotToken), undefined, factory.createIdentifier(props.name.text))
219
+ ]
220
+ } else {
221
+ for (const element of props.name.elements) {
222
+ const property = (element.propertyName ?? element.name)
223
+ 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")
224
+ }
225
+ const rest = props.name.elements.findIndex(element => Boolean(element.dotDotDotToken))
226
+ elements = [...props.name.elements]
227
+ elements.splice(rest < 0 ? elements.length : rest, 0, factory.createBindingElement(undefined, factory.createIdentifier("ref"), factory.createIdentifier(ref.name.text)))
228
+ }
229
+
230
+ const last = ts.isBlock(callback.body) ? callback.body.statements.at(-1) : undefined
231
+ let returnCount = 0
232
+ const countReturns = node => {
233
+ if (node !== callback.body && isFunctionLike(node)) return
234
+ if (ts.isReturnStatement(node)) returnCount++
235
+ ts.forEachChild(node, countReturns)
236
+ }
237
+ countReturns(callback.body)
238
+ const returned = ts.isBlock(callback.body)
239
+ ? last && ts.isReturnStatement(last) ? last.expression : undefined
240
+ : callback.body
241
+ const root = returned && unwrapExpression(returned)
242
+ const tag = root && jsxTagName(root)
243
+ 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")
244
+ const attributes = ts.isJsxElement(root) ? root.openingElement.attributes : root.attributes
245
+ 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)
246
+ 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")
247
+
248
+ const parameter = factory.updateParameterDeclaration(props, props.modifiers, undefined, factory.createObjectBindingPattern(elements), props.questionToken, props.type, undefined)
249
+ return ts.isArrowFunction(callback)
250
+ ? factory.updateArrowFunction(callback, callback.modifiers, callback.typeParameters, [parameter], callback.type, callback.equalsGreaterThanToken, callback.body)
251
+ : factory.updateFunctionExpression(callback, callback.modifiers, undefined, callback.name, callback.typeParameters, [parameter], callback.type, callback.body)
252
+ }
253
+
254
+ function validateUseIdSyntax(sourceFile) {
255
+ 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"))
256
+ if (!imported) return
257
+ const visit = node => {
258
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "useId" && !isShadowedIdentifier(node.expression, sourceFile)) {
259
+ if (node.arguments.length) throw sourceNodeError(node, sourceFile, "useId() does not accept arguments")
260
+ const declaration = node.parent
261
+ const statement = declaration?.parent?.parent
262
+ const owner = nearestFunction(node)
263
+ 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) {
264
+ throw sourceNodeError(node, sourceFile, "useId() must be assigned to one top-level const identifier in a component")
265
+ }
266
+ }
267
+ ts.forEachChild(node, visit)
268
+ }
269
+ visit(sourceFile)
270
+ }
271
+
272
+ function isReactCallbackDependency(node) {
273
+ node = unwrapExpression(node)
274
+ 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
275
+ }
276
+
277
+ function lowerReactMemoCollectionExpression(expression, factory) {
278
+ if (!expression) return undefined
279
+ const visit = node => {
280
+ if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "map" && node.arguments.length === 1) {
281
+ return factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("Array"), "from"), undefined, [visit(node.expression.expression), node.arguments[0]])
282
+ }
283
+ if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && ["filter", "flatMap", "slice", "toSorted"].includes(node.expression.name.text)) {
284
+ return factory.updateCallExpression(node, factory.updatePropertyAccessExpression(node.expression, visit(node.expression.expression), node.expression.name), node.typeArguments, node.arguments)
285
+ }
286
+ if (isArrayFromCall(node)) return factory.updateCallExpression(node, node.expression, node.typeArguments, [visit(node.arguments[0]), ...node.arguments.slice(1)])
287
+ return node
288
+ }
289
+ return visit(expression)
290
+ }
291
+
292
+ function reactMemoCollection(expression, states, importedCollections, sourceFile) {
293
+ const setters = new Map([...states].map(state => [state, state]))
294
+ const fail = (node, message) => { throw sourceNodeError(node, sourceFile, message) }
295
+ return analyzeCollectionPipeline(expression, { setters, fail, importedCollections, stateNames: states })
296
+ }
297
+
298
+ function reactMemoComponentExpression(identifier, sourceFile, factory, context) {
299
+ for (const statement of sourceFile.statements) {
300
+ if (ts.isFunctionDeclaration(statement) && statement.name?.text === identifier.text && statement.body) {
301
+ const clone = cloneAst(statement, factory, context)
302
+ return factory.createFunctionExpression(clone.modifiers?.filter(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword), clone.asteriskToken, clone.name, clone.typeParameters, clone.parameters, clone.type, clone.body)
303
+ }
304
+ if (!ts.isVariableStatement(statement)) continue
305
+ const declaration = statement.declarationList.declarations.find(entry => ts.isIdentifier(entry.name) && entry.name.text === identifier.text)
306
+ if (declaration?.initializer && (ts.isArrowFunction(declaration.initializer) || ts.isFunctionExpression(declaration.initializer))) return cloneAst(declaration.initializer, factory, context)
307
+ }
308
+ return undefined
309
+ }
310
+
311
+ function isPureReactMemoExpression(node) {
312
+ node = unwrapExpression(node)
313
+ 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
314
+ if (ts.isParenthesizedExpression(node)) return isPureReactMemoExpression(node.expression)
315
+ if (ts.isPrefixUnaryExpression(node)) return ![ts.SyntaxKind.PlusPlusToken, ts.SyntaxKind.MinusMinusToken].includes(node.operator) && isPureReactMemoExpression(node.operand)
316
+ if (ts.isBinaryExpression(node)) return node.operatorToken.kind < ts.SyntaxKind.FirstAssignment && isPureReactMemoExpression(node.left) && isPureReactMemoExpression(node.right)
317
+ if (ts.isConditionalExpression(node)) return isPureReactMemoExpression(node.condition) && isPureReactMemoExpression(node.whenTrue) && isPureReactMemoExpression(node.whenFalse)
318
+ if (ts.isTemplateExpression(node)) return node.templateSpans.every(span => isPureReactMemoExpression(span.expression))
319
+ return false
320
+ }
321
+
322
+ function reactMemoReferenceNames(root) {
323
+ const names = new Set()
324
+ const visit = node => {
325
+ if (ts.isIdentifier(node) && isReferenceIdentifier(node)) names.add(node.text)
326
+ ts.forEachChild(node, visit)
327
+ }
328
+ visit(root)
329
+ return names
330
+ }
331
+
332
+ return { normalizeReactMigrationSyntax, validateUseIdSyntax }
333
+ }
334
+
335
+ export function reactMemoExpression(callback) {
336
+ if (!ts.isBlock(callback.body)) return callback.body
337
+ if (callback.body.statements.length !== 1 || !ts.isReturnStatement(callback.body.statements[0])) return undefined
338
+ return callback.body.statements[0].expression
339
+ }
@@ -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
+ }
@@ -0,0 +1,118 @@
1
+ export function usesRouteDependencyRuntime({ plan, navigable, hasBindings, hasLists }) {
2
+ const hasDependencies = plan.effects.some(effect => effect.dependencies?.length)
3
+ return !navigable && hasDependencies && !plan.effects.some(effect => effect.owner) && !hasBindings && !hasLists && !plan.events.some(event => event.native)
4
+ }
5
+
6
+ export function planRouteCapabilities(plans, { routes = new Map(), navigationRouteCount = 0 } = {}) {
7
+ const commandEvents = new Set()
8
+ const nativeEvents = new Set()
9
+ const bindings = { count: 0, text: false, svgConditions: false }
10
+ const lists = {
11
+ count: 0,
12
+ styleCount: 0,
13
+ conditions: false,
14
+ svg: false,
15
+ deepConditions: false,
16
+ textRanges: false,
17
+ attributes: false,
18
+ events: false,
19
+ expressions: false,
20
+ expressionAttributes: false,
21
+ seeds: false,
22
+ effects: false,
23
+ rowHooks: false,
24
+ rowRefs: false,
25
+ complexRowState: false,
26
+ nested: false,
27
+ selectors: false,
28
+ calculated: false,
29
+ static: false,
30
+ indexes: false,
31
+ stableFastPaths: false,
32
+ generalRowHooks: false,
33
+ asyncParts: false,
34
+ mounts: false
35
+ }
36
+ const effects = { any: false, derivedDependencies: false, itemDependencies: false, captures: false, navigable: false, navigableOwners: false }
37
+
38
+ for (let index = 0; index < plans.length; index++) {
39
+ const plan = plans[index]
40
+ const route = routes.get(plan.route)
41
+ for (const event of plan.events) {
42
+ if (event.commands) commandEvents.add(event.event)
43
+ if (event.native) nativeEvents.add(event.event)
44
+ }
45
+ bindings.text ||= plan.bindings.some(binding => binding.target === "text")
46
+ bindings.svgConditions ||= plan.conditions.some(condition => condition.svg)
47
+ effects.any ||= plan.effects.length > 0
48
+ effects.derivedDependencies ||= plan.effects.some(effect => effect.dependencyExpressions?.length)
49
+ effects.itemDependencies ||= plan.effects.some(effect => effect.itemDependencies?.length)
50
+ effects.captures ||= plan.effects.some(effect => Object.keys(effect.scope).length)
51
+ effects.navigable ||= Boolean(route?.navigable && plan.effects.length)
52
+ effects.navigableOwners ||= Boolean(route?.navigable && plan.effects.some(effect => effect.owner))
53
+ for (const list of plan.lists) {
54
+ lists.conditions ||= Boolean(list.conditions)
55
+ lists.svg ||= Boolean(list.svg)
56
+ lists.deepConditions ||= Boolean(list.conditionHandlers)
57
+ lists.textRanges ||= Boolean(list.textRanges)
58
+ lists.attributes ||= Boolean(list.attributes)
59
+ lists.events ||= Boolean(list.events)
60
+ lists.expressions ||= Boolean(list.expressions)
61
+ lists.expressionAttributes ||= Boolean(list.expressionAttributes)
62
+ lists.seeds ||= Boolean(list.seed || list.valueSeed)
63
+ lists.effects ||= Boolean(list.effects)
64
+ lists.rowHooks ||= Boolean(list.rowStates?.length || list.rowRefs?.length)
65
+ lists.rowRefs ||= Boolean(list.rowRefs?.length)
66
+ lists.complexRowState ||= Boolean(list.rowStates?.some(state => state.initialValue !== null && typeof state.initialValue === "object"))
67
+ lists.nested ||= Boolean(list.ownerField)
68
+ lists.selectors ||= Boolean(list.selector)
69
+ lists.calculated ||= Boolean(list.source)
70
+ lists.static ||= Boolean(list.static)
71
+ lists.indexes ||= Boolean(list.indexed)
72
+ lists.stableFastPaths ||= !list.children && !list.ownerField && list.key !== null && !list.indexed && !list.reducer && !list.selector
73
+ lists.generalRowHooks ||= Boolean(list.ownerField && (list.rowStates?.length || list.rowRefs?.length))
74
+ lists.mounts ||= Boolean(list.mount)
75
+ }
76
+ }
77
+
78
+ const routeEntries = [...routes.values()]
79
+ const routeCounts = {
80
+ behaviors: routeEntries.filter(route => route.hasBehaviors).length,
81
+ regularBehaviors: routeEntries.filter(route => route.hasBehaviors && !route.usesDependencyRuntime).length,
82
+ regularStateSeeds: routeEntries.filter(route => route.hasStateSeed && !route.usesDependencyRuntime).length,
83
+ dependencyStateSeeds: routeEntries.filter(route => route.hasStateSeed && route.usesDependencyRuntime).length
84
+ }
85
+ bindings.count = routeEntries.filter(route => route.hasBindings).length
86
+ lists.count = routeEntries.filter(route => route.hasLists).length
87
+ lists.styleCount = routeEntries.filter(route => route.hasListStyles).length
88
+ lists.generalRowHooks ||= lists.rowRefs || lists.complexRowState
89
+ lists.asyncParts = lists.expressions || lists.expressionAttributes || lists.conditions
90
+ lists.mounts ||= lists.conditions || lists.nested
91
+
92
+ return {
93
+ routes: routeCounts,
94
+ events: { command: [...commandEvents].sort(), native: [...nativeEvents].sort(), hasNativeHandlers: nativeEvents.size > 0 },
95
+ bindings,
96
+ lists,
97
+ effects,
98
+ captures: { nestedState: hasNestedCaptureState(plans), setter: hasCaptureType(plans, "setter") },
99
+ runtime: {
100
+ shared: Boolean(bindings.count || lists.count || nativeEvents.size || navigationRouteCount),
101
+ dependency: routeEntries.some(route => route.usesDependencyRuntime)
102
+ }
103
+ }
104
+ }
105
+
106
+ function hasCaptureType(value, type) {
107
+ if (!value || typeof value !== "object") return false
108
+ if (value.type === type) return true
109
+ return (Array.isArray(value) ? value : Object.values(value)).some(entry => hasCaptureType(entry, type))
110
+ }
111
+
112
+ function hasNestedCaptureState(value, insideCapture = false) {
113
+ if (!value || typeof value !== "object") return false
114
+ if (value.type === "state") return insideCapture
115
+ if (value.type === "array") return value.value.some(entry => hasNestedCaptureState(entry, true))
116
+ if (value.type === "object") return value.value.some(([, entry]) => hasNestedCaptureState(entry, true))
117
+ return (Array.isArray(value) ? value : Object.values(value)).some(entry => hasNestedCaptureState(entry, false))
118
+ }