@kudzujs/core 0.8.22 → 0.8.23

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,2969 @@
1
+ import { readFile, realpath, stat } from "node:fs/promises"
2
+ import { dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path"
3
+ import ts from "typescript"
4
+ import { createComponentAnalysisSession } from "./analysis/component-analysis.mjs"
5
+ import { normalizeEffectAnimationFrameRefs } from "./animation-frame-pass.mjs"
6
+ import { bindingNames, containsJsx, effectReturns, functionVarDeclaresName, importDeclarationNames, isFunctionLike, isLocalConst, isReferenceIdentifier, isShadowedByParameter, isShadowedIdentifier, isUnshadowedGlobal, nearestFunction, referenceIdentifiers, referencesIdentifier, sourceLocation, sourceNodeError, statementDeclaresName, unwrapExpression } from "./ast-helpers.mjs"
7
+ import { normalizeMediaQueryExternalStores, normalizeNavigatorCapabilityConditions } from "./browser-signal-passes.mjs"
8
+ import { analyzeCollectionPipeline, collectionExpression, collectionParameters, isArrayFromCall, mutatingCollectionMethods as mutatingListMethods, pureCollectionMathMethods as pureMathMethods, pureCollectionMethods as pureListMethods } from "./collection-analysis.mjs"
9
+ import { normalizeCustomHookTimerRefs } from "./custom-hook-timer-pass.mjs"
10
+ import { captureNames, createDescriptorSession, createSemanticArtifact, nativeCaptureNames, referencedReducerDispatches, referencedStateNames } from "./descriptor-session.mjs"
11
+ import { analyzeEffectDependencies, validateEffectOwnedBrowserResources } from "./effect-analysis.mjs"
12
+ import { createHandlerCodegen } from "./handler-codegen.mjs"
13
+ import { createHandlerLowering } from "./handler-lowering.mjs"
14
+ import { createCommandSpecializer } from "./optimize/command-specialization.mjs"
15
+ import { applyNormalizationPasses } from "./normalization-pipeline.mjs"
16
+ import { assetPath, relativeModulePath, withBase } from "./path-helpers.mjs"
17
+ import { createReactMigrationPass, reactMemoExpression } from "./react-migration-pass.mjs"
18
+ import { normalizeRenderControlFlow } from "./render-control-pass.mjs"
19
+ import { createRouterPass } from "./router-pass.mjs"
20
+ import { parseSourceFile, resolveSourceImport, runtimeModuleReference } from "./source-graph.mjs"
21
+ import { createWorkerCompiler } from "./worker-compiler.mjs"
22
+ import { createZustandPass } from "./zustand-pass.mjs"
23
+
24
+ const root = process.cwd()
25
+ const sourceDirectory = join(root, "src")
26
+ const pagesDirectory = join(sourceDirectory, "pages")
27
+ const workDirectory = join(root, ".kudzu")
28
+ const staticAssetExtensions = new Set([".avif", ".gif", ".ico", ".jpeg", ".jpg", ".otf", ".png", ".svg", ".ttf", ".webp", ".woff", ".woff2"])
29
+
30
+ export function compileSource(file, sourceFiles, sourceIndex, staticFiles, cssModules, base) {
31
+ const importedAssets = new Set()
32
+ const source = sourceIndex.get(file)
33
+ const semantic = createSemanticArtifact(relative(root, file).replaceAll(sep, "/"))
34
+ const handlerPath = `handlers/${relative(sourceDirectory, file).replaceAll(sep, "/").replace(/\.(?:ts|tsx)$/, ".js")}`
35
+ const result = ts.transpileModule(source, {
36
+ fileName: file,
37
+ compilerOptions: {
38
+ target: ts.ScriptTarget.ES2022,
39
+ module: ts.ModuleKind.ESNext,
40
+ jsx: ts.JsxEmit.ReactJSX,
41
+ jsxImportSource: "@kudzujs/core"
42
+ },
43
+ transformers: { before: [createKudzuTransformer({ semantic, handlerUrl: assetPath(base, `assets/${handlerPath}`), file, sourceFiles, sourceIndex, staticFiles, importedAssets, cssModules, base })] },
44
+ reportDiagnostics: true
45
+ })
46
+
47
+ const errors = result.diagnostics?.filter(diagnostic => diagnostic.category === ts.DiagnosticCategory.Error) ?? []
48
+ if (errors.length) {
49
+ throw new Error(errors.map(error => ts.flattenDiagnosticMessageText(error.messageText, "\n")).join("\n"))
50
+ }
51
+ const packageReference = emittedPackageReference(result.outputText, file, new Set(["react", "react-router-dom"]))
52
+ if (packageReference) throw new Error(`${relative(root, file)} Runtime ${packageReference} module references are not supported`)
53
+
54
+ const output = compiledPath(file)
55
+ const { componentAnalysis, moduleIR } = semantic
56
+ const sourceResult = { file: relative(root, file).replaceAll(sep, "/"), componentAnalysis, moduleIR, buildModule: { path: relative(root, output).replaceAll(sep, "/"), code: result.outputText }, importedAssets: [...importedAssets].map(file => relative(root, file).replaceAll(sep, "/")).sort() }
57
+ const moduleHandlers = moduleIR.handlers.filter(handler => handler.kind === "module-export")
58
+ if (!moduleHandlers.length && !moduleIR.bindings.length) {
59
+ normalizeModulePaths(moduleIR)
60
+ return sourceResult
61
+ }
62
+ const moduleSource = printHandlerModule({ moduleIR, handlerPath })
63
+ const moduleResult = ts.transpileModule(moduleSource, {
64
+ compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext },
65
+ reportDiagnostics: true
66
+ })
67
+ const moduleErrors = moduleResult.diagnostics?.filter(diagnostic => diagnostic.category === ts.DiagnosticCategory.Error) ?? []
68
+ if (moduleErrors.length) throw new Error(moduleErrors.map(error => ts.flattenDiagnosticMessageText(error.messageText, "\n")).join("\n"))
69
+ normalizeModulePaths(moduleIR)
70
+ sourceResult.handlerModule = { path: handlerPath, code: moduleResult.outputText, hasNativeHandlers: moduleHandlers.some(handler => handler.role === "native"), hasEffects: moduleHandlers.some(handler => handler.role === "effect"), clientImports: moduleIR.clientModules, hasPackageImports: moduleIR.imports.some(entry => entry.package) }
71
+ return sourceResult
72
+ }
73
+
74
+ function normalizeModulePaths(moduleIR) {
75
+ const normalize = target => isAbsolute(target) ? relative(root, target).replaceAll(sep, "/") : target
76
+ moduleIR.imports = moduleIR.imports.map(entry => entry.package ? entry : { ...entry, target: normalize(entry.target) })
77
+ moduleIR.clientModules = moduleIR.clientModules.map(normalize)
78
+ }
79
+
80
+ function emittedPackageReference(source, file, packages) {
81
+ const sourceFile = ts.createSourceFile(file, source, ts.ScriptTarget.ES2022, true, ts.ScriptKind.JS)
82
+ let found
83
+ const visit = node => {
84
+ if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier) && packages.has(node.moduleSpecifier.text)) found = node.moduleSpecifier.text
85
+ if (ts.isCallExpression(node) && (node.expression.kind === ts.SyntaxKind.ImportKeyword || ts.isIdentifier(node.expression) && node.expression.text === "require") && ts.isStringLiteral(node.arguments[0]) && packages.has(node.arguments[0].text)) found = node.arguments[0].text
86
+ if (!found) ts.forEachChild(node, visit)
87
+ }
88
+ visit(sourceFile)
89
+ return found
90
+ }
91
+
92
+ export function reachableSourceFiles(entries, sourceFiles, sourceIndex) {
93
+ const reachable = new Set()
94
+ const queue = [...entries]
95
+ while (queue.length) {
96
+ const file = queue.pop()
97
+ if (reachable.has(file)) continue
98
+ reachable.add(file)
99
+ const sourceFile = parseSourceFile(file, sourceIndex.get(file))
100
+ const visit = node => {
101
+ const specifier = (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && runtimeModuleReference(node) && node.moduleSpecifier
102
+ if (specifier && ts.isStringLiteral(specifier) && specifier.text.startsWith(".") && !isStaticImport(specifier.text)) {
103
+ try { queue.push(resolveSourceImport(file, specifier.text, sourceFiles)) } catch {}
104
+ }
105
+ const worker = workerCompiler.candidate(node, sourceFile)
106
+ if (worker && ts.isStringLiteral(worker.url.arguments[0]) && worker.url.arguments[0].text.endsWith(".worker.ts")) {
107
+ try { queue.push(resolveSourceImport(file, worker.url.arguments[0].text, sourceFiles)) } catch {}
108
+ }
109
+ ts.forEachChild(node, visit)
110
+ }
111
+ visit(sourceFile)
112
+ }
113
+ return [...reachable].sort()
114
+ }
115
+
116
+ function normalizeClsxSyntax(sourceFile, factory, context) {
117
+ const names = new Set()
118
+ for (const statement of sourceFile.statements) {
119
+ if (!ts.isImportDeclaration(statement) || statement.importClause?.isTypeOnly || !ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text !== "clsx") continue
120
+ if (statement.importClause?.name) names.add(statement.importClause.name.text)
121
+ const bindings = statement.importClause?.namedBindings
122
+ if (bindings && ts.isNamedImports(bindings)) for (const entry of bindings.elements) if (!entry.isTypeOnly && (entry.propertyName ?? entry.name).text === "clsx") names.add(entry.name.text)
123
+ }
124
+ if (!names.size) return sourceFile
125
+
126
+ const lower = node => {
127
+ node = unwrapExpression(node)
128
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) || ts.isNumericLiteral(node)) return node
129
+ if (node.kind === ts.SyntaxKind.FalseKeyword || node.kind === ts.SyntaxKind.NullKeyword) return factory.createStringLiteral("")
130
+ if (ts.isConditionalExpression(node)) return factory.updateConditionalExpression(node, node.condition, node.questionToken, lower(node.whenTrue), node.colonToken, lower(node.whenFalse))
131
+ if (ts.isArrayLiteralExpression(node)) return combine(node.elements.map(lower))
132
+ if (ts.isObjectLiteralExpression(node)) return combine(node.properties.map(property => {
133
+ if (!ts.isPropertyAssignment(property) || property.name && ts.isComputedPropertyName(property.name)) throw sourceNodeError(property, sourceFile, "clsx() object arguments require ordinary key/value properties")
134
+ const name = property.name
135
+ const value = name && (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) ? name.text : undefined
136
+ if (value === undefined) throw sourceNodeError(property, sourceFile, "clsx() object keys must be identifiers or literals")
137
+ return factory.createConditionalExpression(property.initializer, undefined, factory.createStringLiteral(value), undefined, factory.createStringLiteral(""))
138
+ }))
139
+ throw sourceNodeError(node, sourceFile, "clsx() arguments must be string/number literals, literal arrays, literal objects, or conditionals")
140
+ }
141
+ const combine = entries => entries.length ? entries.reduce((result, entry) => factory.createBinaryExpression(factory.createBinaryExpression(result, factory.createToken(ts.SyntaxKind.PlusToken), factory.createStringLiteral(" ")), factory.createToken(ts.SyntaxKind.PlusToken), entry)) : factory.createStringLiteral("")
142
+
143
+ const visitor = node => {
144
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && names.has(node.expression.text) && !isShadowedIdentifier(node.expression, sourceFile)) return combine(node.arguments.map(lower))
145
+ if (ts.isIdentifier(node) && names.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, sourceFile) && !(ts.isCallExpression(node.parent) && node.parent.expression === node)) throw sourceNodeError(node, sourceFile, "clsx imports may only be called directly")
146
+ if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text === "clsx") {
147
+ const clause = node.importClause
148
+ if (!clause || clause.isTypeOnly) return node
149
+ let bindings = clause.namedBindings
150
+ if (bindings && ts.isNamedImports(bindings)) {
151
+ const elements = bindings.elements.filter(entry => entry.isTypeOnly || (entry.propertyName ?? entry.name).text !== "clsx")
152
+ bindings = elements.length ? factory.updateNamedImports(bindings, elements) : undefined
153
+ }
154
+ const defaultName = clause.name && names.has(clause.name.text) ? undefined : clause.name
155
+ if (!defaultName && !bindings) return undefined
156
+ return factory.updateImportDeclaration(node, node.modifiers, factory.updateImportClause(clause, clause.isTypeOnly, defaultName, bindings), node.moduleSpecifier, node.attributes)
157
+ }
158
+ return ts.visitEachChild(node, visitor, context)
159
+ }
160
+ return ts.visitNode(sourceFile, visitor)
161
+ }
162
+
163
+ function normalizeLazyStateInitializers(sourceFile, factory, context, file, sourceFiles, sourceIndex) {
164
+ const bindings = new Set()
165
+ for (const statement of sourceFile.statements) {
166
+ if (!ts.isImportDeclaration(statement) || statement.importClause?.isTypeOnly || !ts.isStringLiteral(statement.moduleSpecifier) || !["react", "@kudzujs/core"].includes(statement.moduleSpecifier.text)) continue
167
+ const named = statement.importClause?.namedBindings
168
+ if (named && ts.isNamedImports(named)) for (const entry of named.elements) {
169
+ const imported = (entry.propertyName ?? entry.name).text
170
+ if (!entry.isTypeOnly && ["useReducer", "useState"].includes(imported) && entry.name.text === imported) bindings.add(imported)
171
+ }
172
+ }
173
+ if (!bindings.size) return sourceFile
174
+ const imports = clientImportBindings(sourceFile, file, sourceFiles)
175
+ const visitor = node => {
176
+ if (bindings.has("useReducer") && ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "useReducer" && !isShadowedIdentifier(node.expression, sourceFile) && node.arguments.length === 3) {
177
+ const initialArg = node.arguments[1]
178
+ const initializer = node.arguments[2]
179
+ let declaration
180
+ if (ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer)) declaration = initializer
181
+ else if (ts.isIdentifier(initializer)) {
182
+ declaration = localComponentDeclaration(sourceFile, initializer.text)
183
+ const binding = imports.get(initializer.text)
184
+ if (!declaration && binding && binding.kind !== "namespace") {
185
+ try {
186
+ declaration = resolveComponentExport(binding.target, binding.kind === "default" ? "default" : binding.imported, target => parseSourceFile(target, sourceIndex.get(target)), sourceFiles)
187
+ } catch {}
188
+ }
189
+ }
190
+ if (!declaration || declaration.parameters.length !== 1 || !ts.isIdentifier(declaration.parameters[0].name) || declaration.parameters[0].initializer || declaration.parameters[0].dotDotDotToken || declaration.asteriskToken || declaration.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword) || ts.isFunctionExpression(initializer) && initializer.name) throw sourceNodeError(initializer, sourceFile, "Lazy useReducer() requires one inline, same-file, or relative-imported synchronous one-parameter initializer")
191
+ if (!isSerializableStateLiteral(initialArg)) throw sourceNodeError(initialArg, sourceFile, "Lazy useReducer() initial argument must be directly serializable")
192
+ const expression = reactMemoExpression(declaration)
193
+ const lowered = expression && substituteClone(expression, new Map([[declaration.parameters[0].name.text, initialArg]]), factory, context)
194
+ if (!lowered || !isSerializableStateLiteral(lowered)) throw sourceNodeError(initializer, sourceFile, "Lazy useReducer() initializer must directly return a serializable primitive, plain-object, or array literal derived only from its initial argument")
195
+ return factory.updateCallExpression(node, node.expression, node.typeArguments, [node.arguments[0], synthesizeSerializableStateLiteral(lowered, factory)])
196
+ }
197
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && bindings.has(node.expression.text) && !isShadowedIdentifier(node.expression, sourceFile) && node.arguments[0] && (ts.isArrowFunction(node.arguments[0]) || ts.isFunctionExpression(node.arguments[0]))) {
198
+ const initializer = node.arguments[0]
199
+ if (node.arguments.length !== 1 || initializer.parameters.length || initializer.asteriskToken || initializer.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword) || ts.isFunctionExpression(initializer) && initializer.name) throw sourceNodeError(initializer, sourceFile, "Lazy useState() requires one anonymous synchronous zero-parameter initializer")
200
+ const expression = ts.isBlock(initializer.body)
201
+ ? initializer.body.statements.length === 1 && ts.isReturnStatement(initializer.body.statements[0]) ? initializer.body.statements[0].expression : undefined
202
+ : initializer.body
203
+ if (!expression || !isSerializableStateLiteral(expression)) throw sourceNodeError(initializer.body, sourceFile, "Lazy useState() initializer must return one directly serializable primitive, plain-object, or array literal")
204
+ return factory.updateCallExpression(node, node.expression, node.typeArguments, [cloneAst(expression, factory, context)])
205
+ }
206
+ return ts.visitEachChild(node, visitor, context)
207
+ }
208
+ return ts.visitNode(sourceFile, visitor)
209
+ }
210
+
211
+ function normalizeCompilerSource(sourceFile, { base, context, file, importedCollections, importedStaticCollections, sourceFiles, sourceIndex }) {
212
+ const factory = context.factory
213
+ let customHookTimerStates = new Set()
214
+ sourceFile = applyNormalizationPasses(sourceFile, [
215
+ ...(importedStaticCollections ? [source => normalizeImportedStaticCollections(source, importedStaticCollections, factory, context)] : []),
216
+ source => normalizeReactRouterSyntax(source, factory, context, base),
217
+ source => normalizeClsxSyntax(source, factory, context),
218
+ source => normalizeMediaQueryExternalStores(source, factory, context),
219
+ source => normalizeReactMigrationSyntax(source, factory, context, importedCollections ?? importedSerializableCollectionNames(source, file, sourceFiles, sourceIndex)),
220
+ source => normalizeNavigatorCapabilityConditions(source, factory, context),
221
+ source => normalizeEffectAnimationFrameRefs(source, factory, context),
222
+ source => {
223
+ const result = normalizeCustomHookTimerRefs(source, factory, context)
224
+ customHookTimerStates = result.timerStates
225
+ return result.sourceFile
226
+ },
227
+ source => {
228
+ validateUseIdSyntax(source)
229
+ return source
230
+ },
231
+ source => normalizeLazyStateInitializers(source, factory, context, file, sourceFiles, sourceIndex),
232
+ source => normalizeZustandMigrationSyntax(source, factory, context),
233
+ source => normalizeRenderControlFlow(source, factory, context),
234
+ source => {
235
+ workerCompiler.rejectOrdinaryImports(source, file, sourceFiles)
236
+ return source
237
+ }
238
+ ])
239
+ return { sourceFile, customHookTimerStates }
240
+ }
241
+
242
+ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourceIndex, staticFiles, importedAssets, cssModules, base }) {
243
+ const { moduleIR } = semantic
244
+ return context => sourceFile => {
245
+ const hasLinkElements = /<link/i.test(sourceFile.text)
246
+ const importedStaticCollections = importedSerializableCollections(sourceFile, file, sourceFiles, sourceIndex)
247
+ const importedCollections = new Set(importedStaticCollections.keys())
248
+ const normalized = normalizeCompilerSource(sourceFile, { base, context, file, importedCollections, importedStaticCollections, sourceFiles, sourceIndex })
249
+ sourceFile = normalized.sourceFile
250
+ const { customHookTimerStates } = normalized
251
+ const factory = context.factory
252
+ const sourceName = source => relative(root, source.fileName).replaceAll(sep, "/")
253
+ const componentAnalysis = createComponentAnalysisSession(semantic.componentAnalysis)
254
+ const descriptors = createDescriptorSession({
255
+ semantic,
256
+ handlerUrl,
257
+ factory,
258
+ context,
259
+ compileEventCommand,
260
+ handlerLowering,
261
+ isPrimitiveLiteral: isPrimitiveDefaultLiteral,
262
+ sourceName,
263
+ rejectWorkerConstructions: expression => workerCompiler.rejectConstructions(expression, expression.getSourceFile(), "Relative TypeScript Worker construction is only supported directly inside an inline useEffect() callback")
264
+ })
265
+ const importBindings = clientImportBindings(sourceFile, file, sourceFiles)
266
+ const packageBindings = packageImportBindings(sourceFile)
267
+ for (const [name] of packageBindings) {
268
+ const references = referenceIdentifiers(sourceFile, name)
269
+ const invalid = references.find(reference => !insideJsxEventHandler(reference, sourceFile))
270
+ if (invalid) throw sourceNodeError(invalid, sourceFile, `Package import ${JSON.stringify(name)} may only be referenced directly inside JSX event handlers`)
271
+ }
272
+ const hasUseEffectImport = sourceFile.statements.some(statement => ts.isImportDeclaration(statement) && ["@kudzujs/core", "react"].includes(statement.moduleSpecifier.text) && statement.importClause?.namedBindings && ts.isNamedImports(statement.importClause.namedBindings) && statement.importClause.namedBindings.elements.some(entry => !entry.propertyName && entry.name.text === "useEffect"))
273
+ const importedSourceCache = new Map()
274
+ const importedSource = target => {
275
+ let result = importedSourceCache.get(target)
276
+ if (!result) {
277
+ result = normalizeCompilerSource(parseSourceFile(target, sourceIndex.get(target)), { base, context, file: target, sourceFiles, sourceIndex })
278
+ importedSourceCache.set(target, result)
279
+ }
280
+ return result.sourceFile
281
+ }
282
+ const importedCollectionTransforms = new Map()
283
+ const importedCalculationFunctions = new Map()
284
+ for (const [name, binding] of importBindings) {
285
+ if (binding.kind === "namespace") continue
286
+ try {
287
+ importedCollectionTransforms.set(name, resolveComponentExport(binding.target, binding.kind === "default" ? "default" : binding.imported, target => parseSourceFile(target, sourceIndex.get(target)), sourceFiles))
288
+ } catch {}
289
+ }
290
+ const settersByFunction = new Map()
291
+ const stateOwnersByFunction = new Map()
292
+ const localStateSettersByFunction = new Map()
293
+ const reducersByFunction = new Map()
294
+ const zustandStores = new Map()
295
+ const resolvedZustandStore = entry => {
296
+ const exportName = entry.kind === "default" ? "default" : entry.imported
297
+ const key = `${entry.target}:${exportName}`
298
+ if (zustandStores.has(key)) return zustandStores.get(key)
299
+ const targetSource = parseSourceFile(entry.target, sourceIndex.get(entry.target))
300
+ const store = analyzeZustandStores(targetSource).get(exportName)
301
+ zustandStores.set(key, store)
302
+ return store
303
+ }
304
+ const functions = new Map()
305
+ const customHookFunctionsByOwner = new Map()
306
+ const customHookPrivateFields = new WeakMap()
307
+ const components = new Map()
308
+ const contexts = new Set()
309
+ const customHooks = new Map()
310
+ const jsxLocalDeclarations = new Map()
311
+ const jsxLocalsByFunction = new Map()
312
+ const listLocalDeclarations = []
313
+ const listLocalUses = []
314
+ const analysisSource = node => {
315
+ const original = ts.getOriginalNode(node)
316
+ return original.pos >= 0 && original.end >= 0 ? { file: sourceName(original.getSourceFile()), start: original.getStart(), end: original.end } : undefined
317
+ }
318
+ const analyzedProps = owner => {
319
+ if (owner.parameters.length !== 1 || !ts.isObjectBindingPattern(owner.parameters[0].name)) return []
320
+ return owner.parameters[0].name.elements.map(element => ({
321
+ name: (element.propertyName ?? element.name).getText(),
322
+ local: element.name.getText(),
323
+ ...(element.dotDotDotToken ? { rest: true } : {}),
324
+ ...(element.initializer ? { hasDefault: true } : {})
325
+ }))
326
+ }
327
+ const ownerName = owner => owner.name?.text ?? (ts.isVariableDeclaration(owner.parent) && ts.isIdentifier(owner.parent.name) ? owner.parent.name.text : "anonymous")
328
+ const ensureOwner = (owner, kind = "component") => componentAnalysis.registerOwner(owner, { kind, name: ownerName(owner), props: analyzedProps(owner), source: analysisSource(owner) })
329
+ const registerState = (owner, state, setter, kind, node, externalOwner) => {
330
+ const ownerRecord = ensureOwner(owner)
331
+ const stateOwner = externalOwner ?? `owner:${ownerRecord.slot}`
332
+ const stateOwners = stateOwnersByFunction.get(owner) ?? new Map()
333
+ stateOwners.set(state, stateOwner)
334
+ stateOwnersByFunction.set(owner, stateOwners)
335
+ return componentAnalysis.registerState(owner, { name: state, setter, kind, ...(externalOwner ? { owner: externalOwner } : {}), source: analysisSource(node) })
336
+ }
337
+ const stateOwnersForNode = node => {
338
+ for (let current = node.parent; current; current = current.parent) {
339
+ if (isFunctionLike(current) && stateOwnersByFunction.has(current)) return stateOwnersByFunction.get(current)
340
+ }
341
+ return new Map()
342
+ }
343
+ const fallbackOwner = node => {
344
+ for (let current = node.parent; current; current = current.parent) {
345
+ const owner = isFunctionLike(current) ? componentAnalysis.owner(current) : undefined
346
+ if (owner) return `owner:${owner.slot}`
347
+ }
348
+ return "module"
349
+ }
350
+ let usesBehavior = false
351
+ let usesBinding = false
352
+ let usesConditional = false
353
+ let usesList = false
354
+ let usesListEffects = false
355
+ let usesListItem = false
356
+ let usesRowState = false
357
+ let usesRowRef = false
358
+ let usesComponentState = false
359
+ let usesComponentId = false
360
+ let usesComponentRef = false
361
+ let usesComponentEffects = false
362
+
363
+ const resolveContextHook = (returned, hookSource) => {
364
+ if (!hasFrameworkImport(hookSource, "useContext")) throw sourceNodeError(returned.expression, hookSource, "Relative Context hooks must call useContext imported from react or @kudzujs/core")
365
+ if (returned.arguments.length !== 1 || !ts.isIdentifier(returned.arguments[0])) throw sourceNodeError(returned, hookSource, "Relative Context hooks must directly return useContext(ContextIdentifier)")
366
+ const contextName = returned.arguments[0].text
367
+ let providerSource = hookSource
368
+ let providerContextName = contextName
369
+ const hookImports = clientImportBindings(hookSource, hookSource.fileName, sourceFiles)
370
+ if (hookImports.has(contextName)) {
371
+ const binding = hookImports.get(contextName)
372
+ if (binding.kind === "namespace" || binding.kind === "default") throw sourceNodeError(returned.arguments[0], hookSource, "Relative Context hooks require a named Context import")
373
+ providerSource = importedSource(binding.target)
374
+ providerContextName = binding.imported
375
+ }
376
+ const hasContext = hasFrameworkImport(providerSource, "createContext") && providerSource.statements.some(statement => ts.isVariableStatement(statement) && statement.declarationList.declarations.some(declaration => ts.isIdentifier(declaration.name) && declaration.name.text === providerContextName && declaration.initializer && ts.isCallExpression(declaration.initializer) && ts.isIdentifier(declaration.initializer.expression) && declaration.initializer.expression.text === "createContext"))
377
+ if (!hasContext) throw sourceNodeError(returned.arguments[0], hookSource, "Relative Context hooks require a local or named relative createContext() declaration")
378
+
379
+ const providers = []
380
+ const findProviders = node => {
381
+ if (ts.isJsxAttribute(node) && node.name.text === "value") {
382
+ const element = node.parent?.parent
383
+ const tag = ts.isJsxOpeningElement(element) || ts.isJsxSelfClosingElement(element) ? element.tagName : undefined
384
+ if (ts.isPropertyAccessExpression(tag) && tag.name.text === "Provider" && ts.isIdentifier(tag.expression) && tag.expression.text === providerContextName) providers.push(node)
385
+ }
386
+ ts.forEachChild(node, findProviders)
387
+ }
388
+ findProviders(providerSource)
389
+ if (providers.length !== 1) throw sourceNodeError(returned.arguments[0], hookSource, "Relative Context hooks require exactly one Provider value in the Context module")
390
+ const provider = providers[0]
391
+ const value = provider.initializer && ts.isJsxExpression(provider.initializer) && provider.initializer.expression ? unwrapExpression(provider.initializer.expression) : undefined
392
+ if (!value || !ts.isObjectLiteralExpression(value)) throw sourceNodeError(provider, providerSource, "Context Provider value must be one direct object literal")
393
+ const owner = nearestFunction(provider)
394
+ if (!owner) throw sourceNodeError(provider, providerSource, "Context Provider value must be returned by a component")
395
+ const stateOwner = `external:${sourceName(providerSource)}:${owner.getStart(providerSource)}`
396
+
397
+ const states = new Map()
398
+ const callbacks = new Map()
399
+ const hasUseState = hasFrameworkImport(providerSource, "useState")
400
+ const collectProviderBindings = node => {
401
+ if (ts.isVariableDeclaration(node) && nearestFunction(node) === owner) {
402
+ if (hasUseState && ts.isArrayBindingPattern(node.name) && node.initializer && ts.isCallExpression(node.initializer) && ts.isIdentifier(node.initializer.expression) && node.initializer.expression.text === "useState") {
403
+ const [state, setter] = node.name.elements
404
+ if (node.name.elements.length === 2 && state && setter && ts.isBindingElement(state) && ts.isBindingElement(setter) && ts.isIdentifier(state.name) && ts.isIdentifier(setter.name)) states.set(setter.name.text, state.name.text)
405
+ }
406
+ if (ts.isIdentifier(node.name) && node.initializer && (ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer))) callbacks.set(node.name.text, node.initializer)
407
+ }
408
+ ts.forEachChild(node, collectProviderBindings)
409
+ }
410
+ collectProviderBindings(owner.body)
411
+
412
+ const fields = new Set()
413
+ const stateFields = new Set([...states].flat())
414
+ for (const property of value.properties) {
415
+ if (!ts.isShorthandPropertyAssignment(property)) throw sourceNodeError(property, providerSource, "Context Provider values must use direct shorthand state, setter, or action fields")
416
+ const name = property.name.text
417
+ if (!stateFields.has(name) && !callbacks.has(name)) throw sourceNodeError(property, providerSource, `Context Provider field ${JSON.stringify(name)} must be a direct provider-owned state, setter, or action`)
418
+ fields.add(name)
419
+ }
420
+ for (const [setter, state] of states) {
421
+ if (fields.has(setter) !== fields.has(state)) throw sourceNodeError(value, providerSource, `Context Provider state ${JSON.stringify(state)} and setter ${JSON.stringify(setter)} must be exposed together`)
422
+ }
423
+ for (const [name, callback] of callbacks) {
424
+ if (!fields.has(name)) continue
425
+ if (callback.asteriskToken || callback.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) throw sourceNodeError(callback, providerSource, `Context action ${JSON.stringify(name)} must be synchronous`)
426
+ const capture = nativeCaptureNames(callback, states).values().next().value
427
+ if (capture) throw sourceNodeError(callback, providerSource, `Context action ${JSON.stringify(name)} cannot capture private binding ${JSON.stringify(capture)}`)
428
+ for (const state of referencedStateNames(callback.body, states, callback)) {
429
+ const setter = [...states].find(([, candidate]) => candidate === state)?.[0]
430
+ if (!setter || !fields.has(state) || !fields.has(setter)) throw sourceNodeError(callback, providerSource, `Context action ${JSON.stringify(name)} requires exposed state and setter fields for ${JSON.stringify(state)}`)
431
+ }
432
+ }
433
+ return { callbacks: new Map([...callbacks].filter(([name]) => fields.has(name))), context: true, fields, privateStates: new Set(), stateOwner, states }
434
+ }
435
+
436
+ const resolveCustomHook = (binding, call) => {
437
+ const exportName = binding.kind === "default" ? "default" : binding.imported
438
+ const key = `${binding.target}:${exportName}`
439
+ if (customHooks.has(key)) return customHooks.get(key)
440
+ const hook = resolveComponentExport(binding.target, exportName, importedSource, sourceFiles)
441
+ const hookSource = hook.getSourceFile()
442
+ if (hook.parameters.length || hook.asteriskToken || hook.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword) || !ts.isBlock(hook.body)) throw sourceNodeError(hook, hookSource, "Relative custom hooks must be synchronous zero-argument functions with a block body")
443
+ const returns = hook.body.statements.filter(ts.isReturnStatement)
444
+ const returned = returns.length === 1 && returns[0] === hook.body.statements.at(-1) && returns[0].expression ? unwrapExpression(returns[0].expression) : undefined
445
+ if (returned && ts.isCallExpression(returned) && ts.isIdentifier(returned.expression) && returned.expression.text === "useContext") {
446
+ const analysis = resolveContextHook(returned, hookSource)
447
+ customHooks.set(key, analysis)
448
+ return analysis
449
+ }
450
+ if (!returned || !ts.isObjectLiteralExpression(returned)) throw sourceNodeError(hook.body, hookSource, "Relative custom hooks must end with one direct object return or direct useContext(ContextIdentifier)")
451
+
452
+ const states = new Map()
453
+ const callbacks = new Map()
454
+ for (const statement of hook.body.statements) {
455
+ if (!ts.isVariableStatement(statement) || !(statement.declarationList.flags & ts.NodeFlags.Const)) continue
456
+ for (const declaration of statement.declarationList.declarations) {
457
+ if (ts.isArrayBindingPattern(declaration.name) && declaration.initializer && ts.isCallExpression(declaration.initializer) && ts.isIdentifier(declaration.initializer.expression) && declaration.initializer.expression.text === "useState") {
458
+ const [state, setter] = declaration.name.elements
459
+ if (declaration.name.elements.length === 2 && state && setter && ts.isBindingElement(state) && ts.isBindingElement(setter) && ts.isIdentifier(state.name) && ts.isIdentifier(setter.name)) states.set(setter.name.text, state.name.text)
460
+ }
461
+ if (ts.isIdentifier(declaration.name) && declaration.initializer && (ts.isArrowFunction(declaration.initializer) || ts.isFunctionExpression(declaration.initializer))) callbacks.set(declaration.name.text, declaration.initializer)
462
+ }
463
+ }
464
+ const fields = new Set()
465
+ for (const property of returned.properties) {
466
+ if (!ts.isShorthandPropertyAssignment(property)) throw sourceNodeError(property, hookSource, "Relative custom hooks must return direct shorthand bindings")
467
+ fields.add(property.name.text)
468
+ }
469
+ for (const [name, callback] of callbacks) {
470
+ const capture = nativeCaptureNames(callback, states).values().next().value
471
+ if (capture) throw sourceNodeError(callback, hookSource, `Relative custom hook callback ${JSON.stringify(name)} cannot capture private binding ${JSON.stringify(capture)}`)
472
+ }
473
+ const privateStates = new Set([...states.values()].filter(state => importedSourceCache.get(hookSource.fileName)?.customHookTimerStates.has(state)))
474
+ const analysis = { callbacks, fields, privateStates, states }
475
+ customHooks.set(key, analysis)
476
+ return analysis
477
+ }
478
+
479
+ const collect = node => {
480
+ if (ts.isVariableDeclaration(node) && node.initializer && ts.isCallExpression(node.initializer)) {
481
+ const callName = ts.isIdentifier(node.initializer.expression) ? node.initializer.expression.text : ""
482
+ if (callName && /^use[A-Z]/.test(callName) && importBindings.has(callName) && importBindings.get(callName).kind !== "namespace" && !resolvedZustandStore(importBindings.get(callName))) {
483
+ if (!isLocalConst(node) || !ts.isObjectBindingPattern(node.name) || node.initializer.arguments.length) throw sourceNodeError(node, sourceFile, "Relative custom hooks must initialize one top-level const object destructuring with no arguments")
484
+ const hook = resolveCustomHook(importBindings.get(callName), node.initializer)
485
+ const names = new Set()
486
+ for (const element of node.name.elements) {
487
+ if (element.dotDotDotToken || element.propertyName || element.initializer || !ts.isIdentifier(element.name)) throw sourceNodeError(element, sourceFile, "Relative custom hook results must use direct identifier shorthand without aliases, defaults, or rest")
488
+ const name = element.name.text
489
+ if (!hook.fields.has(name)) throw sourceNodeError(element, sourceFile, `Relative custom hook does not directly return ${JSON.stringify(name)}`)
490
+ names.add(name)
491
+ }
492
+ const owner = nearestFunction(node)
493
+ if (!owner) throw sourceNodeError(node, sourceFile, "Relative custom hooks cannot be used outside a Kudzu component")
494
+ const setters = settersByFunction.get(owner) ?? new Map()
495
+ const requiredContextStates = new Set()
496
+ if (hook.context) {
497
+ for (const name of names) {
498
+ const callback = hook.callbacks.get(name)
499
+ if (callback) for (const state of referencedStateNames(callback.body, hook.states, callback)) requiredContextStates.add(state)
500
+ }
501
+ }
502
+ for (const [setter, state] of hook.states) {
503
+ if (hook.context) {
504
+ if (names.has(setter) && !names.has(state)) throw sourceNodeError(node.name, sourceFile, `Relative Context setter ${JSON.stringify(setter)} requires state ${JSON.stringify(state)} to be destructured`)
505
+ if (!names.has(state) && !requiredContextStates.has(state)) continue
506
+ const localSetter = names.has(setter) || requiredContextStates.has(state) ? setter : `__kContextState_${state}`
507
+ setters.set(localSetter, state)
508
+ registerState(owner, state, localSetter, "context", node, hook.stateOwner)
509
+ if (requiredContextStates.has(state)) {
510
+ const fields = customHookPrivateFields.get(node) ?? []
511
+ for (const field of [state, setter]) {
512
+ if (names.has(field) || fields.includes(field)) continue
513
+ const conflict = owner.parameters.some(parameter => bindingNames(parameter.name).includes(field)) || owner.body.statements.some(statement => statement !== node.parent.parent && statementDeclaresName(statement, field))
514
+ if (conflict) throw sourceNodeError(node.name, sourceFile, `Context action state field ${JSON.stringify(field)} conflicts with a consumer binding`)
515
+ fields.push(field)
516
+ }
517
+ customHookPrivateFields.set(node, fields)
518
+ }
519
+ continue
520
+ }
521
+ if (hook.privateStates.has(state)) {
522
+ setters.set(setter, state)
523
+ registerState(owner, state, setter, "custom-hook", node)
524
+ const fields = customHookPrivateFields.get(node) ?? []
525
+ fields.push(state, setter)
526
+ customHookPrivateFields.set(node, fields)
527
+ continue
528
+ }
529
+ if (names.has(setter) !== names.has(state)) throw sourceNodeError(node.name, sourceFile, `Relative custom hook state ${JSON.stringify(state)} and setter ${JSON.stringify(setter)} must be destructured together`)
530
+ if (names.has(setter)) {
531
+ setters.set(setter, state)
532
+ registerState(owner, state, setter, "custom-hook", node)
533
+ }
534
+ }
535
+ settersByFunction.set(owner, setters)
536
+ for (const name of names) {
537
+ if (hook.callbacks.has(name)) {
538
+ const callbacks = customHookFunctionsByOwner.get(owner) ?? new Map()
539
+ callbacks.set(name, hook.callbacks.get(name))
540
+ customHookFunctionsByOwner.set(owner, callbacks)
541
+ if (hook.context) {
542
+ const reducers = reducersByFunction.get(owner) ?? new Map()
543
+ reducers.set(name, { contextAction: hook.callbacks.get(name), states: hook.states })
544
+ reducersByFunction.set(owner, reducers)
545
+ }
546
+ }
547
+ else if (![...hook.states].some(([setter, state]) => name === setter || name === state)) throw sourceNodeError(node.name, sourceFile, `Relative custom hook result ${JSON.stringify(name)} must be a direct useState value, setter, or callback`)
548
+ }
549
+ }
550
+ if (ts.isIdentifier(node.name) && callName && importBindings.has(callName) && importBindings.get(callName).kind !== "namespace") {
551
+ const storeImport = importBindings.get(callName)
552
+ const store = resolvedZustandStore(storeImport)
553
+ if (store) {
554
+ const selector = node.initializer.arguments[0]
555
+ if (node.initializer.arguments.length !== 1 || !selector || !ts.isArrowFunction(selector) || selector.parameters.length !== 1 || !ts.isIdentifier(selector.parameters[0].name) || !ts.isPropertyAccessExpression(unwrapExpression(selector.body)) || !ts.isIdentifier(unwrapExpression(selector.body).expression) || unwrapExpression(selector.body).expression.text !== selector.parameters[0].name.text) throw sourceNodeError(node.initializer, sourceFile, "Zustand selectors must be direct arrows such as state => state.quantities")
556
+ const selected = unwrapExpression(selector.body).name.text
557
+ const owner = nearestFunction(node)
558
+ if (!owner) throw sourceNodeError(node, sourceFile, "Zustand stores cannot be used outside a Kudzu component")
559
+ const setters = settersByFunction.get(owner) ?? new Map()
560
+ if (selected === store.field) {
561
+ const setter = `__kStoreState_${node.name.text}`
562
+ setters.set(setter, node.name.text)
563
+ registerState(owner, node.name.text, setter, "store", node)
564
+ }
565
+ else if (store.actions.has(selected)) {
566
+ setters.set(node.name.text, node.name.text)
567
+ registerState(owner, node.name.text, node.name.text, "store-action", node)
568
+ const reducers = reducersByFunction.get(owner) ?? new Map()
569
+ reducers.set(node.name.text, { state: node.name.text, store, action: selected })
570
+ reducersByFunction.set(owner, reducers)
571
+ } else throw sourceNodeError(unwrapExpression(selector.body).name, sourceFile, `Zustand store ${JSON.stringify(store.name)} has no supported property ${JSON.stringify(selected)}`)
572
+ settersByFunction.set(owner, setters)
573
+ }
574
+ }
575
+ if (callName === "useReducer") {
576
+ if (!ts.isArrayBindingPattern(node.name)) throw sourceNodeError(node.name, sourceFile, "useReducer() must use [state, dispatch] identifier destructuring")
577
+ const [stateElement, dispatchElement] = node.name.elements
578
+ if (node.name.elements.length !== 2 || !stateElement || !dispatchElement || !ts.isBindingElement(stateElement) || !ts.isBindingElement(dispatchElement) || !ts.isIdentifier(stateElement.name) || !ts.isIdentifier(dispatchElement.name)) throw sourceNodeError(node.name, sourceFile, "useReducer() must use [state, dispatch] identifier destructuring")
579
+ if (node.initializer.arguments.length !== 2) throw sourceNodeError(node.initializer, sourceFile, "useReducer() requires exactly a reducer and initial value")
580
+ const reducer = node.initializer.arguments[0]
581
+ if (!ts.isIdentifier(reducer) || !importBindings.has(reducer.text) || importBindings.get(reducer.text).kind === "namespace") throw sourceNodeError(reducer, sourceFile, "useReducer() reducers must be default or named imports from relative TypeScript modules")
582
+ const reducerImport = importBindings.get(reducer.text)
583
+ let reducerDeclaration
584
+ try {
585
+ reducerDeclaration = resolveComponentExport(reducerImport.target, reducerImport.kind === "default" ? "default" : reducerImport.imported, importedSource, sourceFiles)
586
+ } catch {
587
+ throw sourceNodeError(reducer, sourceFile, "useReducer() imports must resolve to a statically analyzable reducer function")
588
+ }
589
+ if (reducerDeclaration.parameters.length !== 2 || reducerDeclaration.asteriskToken || reducerDeclaration.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) throw sourceNodeError(reducer, sourceFile, "useReducer() reducers must be synchronous functions with exactly state and action parameters")
590
+ const owner = nearestFunction(node)
591
+ if (!owner) throw sourceNodeError(node, sourceFile, "useReducer() cannot be used outside a Kudzu component")
592
+ const setters = settersByFunction.get(owner) ?? new Map()
593
+ setters.set(dispatchElement.name.text, stateElement.name.text)
594
+ registerState(owner, stateElement.name.text, dispatchElement.name.text, "reducer", node)
595
+ settersByFunction.set(owner, setters)
596
+ const reducers = reducersByFunction.get(owner) ?? new Map()
597
+ reducers.set(dispatchElement.name.text, { state: stateElement.name.text, reducer: reducer.text, import: reducerImport })
598
+ reducersByFunction.set(owner, reducers)
599
+ }
600
+ if (ts.isArrayBindingPattern(node.name)) {
601
+ const [stateElement, setterElement] = node.name.elements
602
+ if (callName === "useState" && stateElement && setterElement && ts.isBindingElement(stateElement) && ts.isBindingElement(setterElement) && ts.isIdentifier(stateElement.name) && ts.isIdentifier(setterElement.name)) {
603
+ const owner = nearestFunction(node)
604
+ if (owner) {
605
+ const setters = settersByFunction.get(owner) ?? new Map()
606
+ setters.set(setterElement.name.text, stateElement.name.text)
607
+ registerState(owner, stateElement.name.text, setterElement.name.text, "state", node)
608
+ settersByFunction.set(owner, setters)
609
+ const localSetters = localStateSettersByFunction.get(owner) ?? new Set()
610
+ localSetters.add(setterElement.name.text)
611
+ localStateSettersByFunction.set(owner, localSetters)
612
+ }
613
+ }
614
+ }
615
+ }
616
+ if (ts.isFunctionDeclaration(node) && node.name) {
617
+ functions.set(node.name.text, node)
618
+ if (node.parent === sourceFile) {
619
+ components.set(node.name.text, { function: node, declaration: node })
620
+ ensureOwner(node)
621
+ }
622
+ }
623
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && (ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer))) {
624
+ functions.set(node.name.text, node.initializer)
625
+ if (node.parent?.parent?.parent === sourceFile) {
626
+ components.set(node.name.text, { function: node.initializer, declaration: node })
627
+ ensureOwner(node.initializer)
628
+ }
629
+ }
630
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isCallExpression(node.initializer) && ts.isIdentifier(node.initializer.expression)) {
631
+ const owner = nearestFunction(node)
632
+ if (owner && node.initializer.expression.text === "useRef" && node.initializer.arguments.length === 1 && node.initializer.arguments[0].kind === ts.SyntaxKind.NullKeyword) {
633
+ ensureOwner(owner)
634
+ componentAnalysis.registerRef(owner, { name: node.name.text, source: analysisSource(node) })
635
+ }
636
+ if (owner && node.initializer.expression.text === "useId" && node.initializer.arguments.length === 0) {
637
+ ensureOwner(owner)
638
+ componentAnalysis.registerId(owner, { name: node.name.text, source: analysisSource(node) })
639
+ }
640
+ }
641
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isCallExpression(node.initializer) && ts.isIdentifier(node.initializer.expression) && node.initializer.expression.text === "createContext") contexts.add(node.name.text)
642
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && isLocalConst(node)) {
643
+ const owner = nearestFunction(node)
644
+ const declarations = jsxLocalDeclarations.get(owner) ?? new Map()
645
+ const entries = declarations.get(node.name.text) ?? []
646
+ entries.push({ node, initializer: node.initializer })
647
+ declarations.set(node.name.text, entries)
648
+ jsxLocalDeclarations.set(owner, declarations)
649
+ }
650
+ ts.forEachChild(node, collect)
651
+ }
652
+ collect(sourceFile)
653
+ const functionsForNode = node => {
654
+ const callbacks = customHookFunctionsByOwner.get(nearestFunction(node))
655
+ return callbacks ? new Map([...functions, ...callbacks]) : functions
656
+ }
657
+ for (const [owner, declarations] of jsxLocalDeclarations) {
658
+ const names = new Set()
659
+ let changed = true
660
+ while (changed) {
661
+ changed = false
662
+ for (const [name, entries] of declarations) {
663
+ if (!names.has(name) && entries.some(({ initializer }) => isJsxLocalValue(initializer, names))) {
664
+ names.add(name)
665
+ changed = true
666
+ }
667
+ }
668
+ }
669
+ for (const name of names) {
670
+ const entries = declarations.get(name)
671
+ if (entries.length > 1) {
672
+ const position = sourceFile.getLineAndCharacterOfPosition(entries[1].node.getStart(sourceFile))
673
+ throw new Error(`${sourceFile.fileName}:${position.line + 1}:${position.character + 1} Block-scoped JSX local "${name}" must not shadow another local`)
674
+ }
675
+ }
676
+ jsxLocalsByFunction.set(owner, names)
677
+ }
678
+ for (const [owner, declarations] of jsxLocalDeclarations) {
679
+ const setters = settersByFunction.get(owner) ?? new Map()
680
+ for (const [name, entries] of declarations) {
681
+ for (const declaration of entries) {
682
+ const parts = keyedListParts(declaration.initializer, setters, declarations, (target, message) => { throw sourceNodeError(target, sourceFile, message) }, new Set(), importedCollections, factory, context, importedCollectionTransforms)
683
+ if (!parts) continue
684
+ const uses = []
685
+ const collectUses = node => {
686
+ if (ts.isJsxExpression(node) && node.initializer === undefined && ts.isIdentifier(node.expression) && node.expression.text === name && nearestFunction(node) === owner) uses.push(node)
687
+ ts.forEachChild(node, collectUses)
688
+ }
689
+ collectUses(owner.body)
690
+ const references = identifierReferenceCount(owner.body, name)
691
+ const position = sourceFile.getLineAndCharacterOfPosition(declaration.node.getStart(sourceFile))
692
+ if (uses.length > 1) throw new Error(`${sourceFile.fileName}:${position.line + 1}:${position.character + 1} Keyed list local "${name}" must be rendered exactly once`)
693
+ if (references !== uses.length) throw new Error(`${sourceFile.fileName}:${position.line + 1}:${position.character + 1} Keyed list local "${name}" may only be used as a JSX child`)
694
+ listLocalDeclarations.push(declaration.node)
695
+ if (uses.length) listLocalUses.push({ node: uses[0], parts })
696
+ }
697
+ }
698
+ }
699
+ const fail = (node, message) => {
700
+ throw sourceNodeError(node, sourceFile, message)
701
+ }
702
+ const validateImportedCalculation = (call, field) => {
703
+ const name = call.expression.text
704
+ let calculation = importedCalculationFunctions.get(name)
705
+ if (!calculation) {
706
+ const binding = importBindings.get(name)
707
+ try {
708
+ calculation = resolveComponentExport(binding.target, binding.kind === "default" ? "default" : binding.imported, importedSource, sourceFiles)
709
+ } catch {
710
+ fail(call.expression, "Reactive imported calculations must resolve to a directly exported relative TypeScript function")
711
+ }
712
+ importedCalculationFunctions.set(name, calculation)
713
+ }
714
+ if (calculation.asteriskToken || calculation.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) fail(call.expression, "Reactive imported calculations must be synchronous functions")
715
+ if (calculation.parameters.length !== call.arguments.length) fail(call, "Reactive imported calculations require one direct argument for each declared parameter")
716
+ const returns = ts.isBlock(calculation.body) ? [] : [unwrapExpression(calculation.body)]
717
+ const collectReturns = node => {
718
+ if (node !== calculation.body && isFunctionLike(node)) return
719
+ if (ts.isReturnStatement(node)) returns.push(node.expression ? unwrapExpression(node.expression) : null)
720
+ ts.forEachChild(node, collectReturns)
721
+ }
722
+ if (ts.isBlock(calculation.body)) collectReturns(calculation.body)
723
+ if (ts.isBlock(calculation.body) && !ts.isReturnStatement(calculation.body.statements.at(-1))) fail(call.expression, "Reactive imported calculations must end with an unconditional return")
724
+ if (!returns.length || returns.some(returned => !returned || !ts.isObjectLiteralExpression(returned))) fail(call.expression, "Reactive imported calculations must return a plain object")
725
+ const fieldExists = returns.every(returned => returned.properties.some(property => ts.isSpreadAssignment(property) || !ts.isComputedPropertyName(property.name) && property.name.text === field))
726
+ if (!fieldExists) fail(call.parent, `Reactive imported calculation does not return field ${JSON.stringify(field)}`)
727
+ }
728
+ const validateReactiveJsxExpression = (expression, allowedNames) => {
729
+ const value = unwrapExpression(expression)
730
+ const formatAccess = ts.isCallExpression(value) && !value.questionDotToken && ts.isPropertyAccessExpression(value.expression) && !value.expression.questionDotToken && value.expression.name.text === "format" ? value.expression : undefined
731
+ const formatter = formatAccess && unwrapExpression(formatAccess.expression)
732
+ const constructor = formatter && ts.isNewExpression(formatter) && ts.isPropertyAccessExpression(formatter.expression) && formatter.expression.name.text === "NumberFormat" && ts.isIdentifier(formatter.expression.expression) && formatter.expression.expression.text === "Intl" ? formatter : undefined
733
+ if (!constructor) {
734
+ const validate = node => {
735
+ const current = unwrapExpression(node)
736
+ if (ts.isPropertyAccessExpression(current) && ts.isCallExpression(unwrapExpression(current.expression))) {
737
+ const call = unwrapExpression(current.expression)
738
+ if (ts.isIdentifier(call.expression) && importBindings.has(call.expression.text) && importBindings.get(call.expression.text).kind !== "namespace") {
739
+ validateImportedCalculation(call, current.name.text)
740
+ for (const argument of call.arguments) collectionExpression(argument, { fail: (target, message) => fail(target, message.replace("Rendered collection", "Reactive imported calculation")), stateNames: allowedNames })
741
+ return factory.createNumericLiteral(0)
742
+ }
743
+ }
744
+ return ts.visitEachChild(current, validate, context)
745
+ }
746
+ const normalized = ts.visitNode(value, validate)
747
+ collectionExpression(normalized, { fail: (node, message) => fail(node, message.replace("Rendered collection", "Reactive JSX local")), stateNames: allowedNames })
748
+ return
749
+ }
750
+ const intl = constructor.expression.expression
751
+ if (!isUnshadowedGlobal(intl, sourceFile)) fail(intl, "Reactive JSX Intl.NumberFormat requires the unshadowed global Intl object")
752
+ if (constructor.arguments?.length !== 1 || !ts.isStringLiteral(constructor.arguments[0])) fail(constructor, "Reactive JSX Intl.NumberFormat requires exactly one static string locale")
753
+ const rounded = value.arguments.length === 1 ? unwrapExpression(value.arguments[0]) : undefined
754
+ const roundAccess = rounded && ts.isCallExpression(rounded) && !rounded.questionDotToken && rounded.arguments.length === 1 && ts.isPropertyAccessExpression(rounded.expression) && !rounded.expression.questionDotToken && rounded.expression.name.text === "round" && ts.isIdentifier(rounded.expression.expression) && rounded.expression.expression.text === "Math" ? rounded.expression : undefined
755
+ if (!roundAccess) fail(value, "Reactive JSX Intl.NumberFormat format() requires exactly Math.round(expression)")
756
+ if (!isUnshadowedGlobal(roundAccess.expression, sourceFile)) fail(roundAccess.expression, "Reactive JSX Intl.NumberFormat requires the unshadowed global Math object")
757
+ collectionExpression(rounded.arguments[0], { fail: (node, message) => fail(node, message.replace("Rendered collection", "Reactive JSX local")), stateNames: allowedNames })
758
+ }
759
+ const resolveReactiveJsxExpression = (expression, owner, setters) => {
760
+ const declarations = jsxLocalDeclarations.get(owner)
761
+ if (!declarations) return expression
762
+ const substitutions = new Map()
763
+ const resolving = []
764
+ const resolve = (name, reference) => {
765
+ if (substitutions.has(name)) return
766
+ const entries = declarations.get(name)
767
+ if (!entries?.length) return
768
+ if (jsxLocalsByFunction.get(owner)?.has(name)) return
769
+ if (entries.length !== 1 || entries[0].node.parent?.parent?.parent !== owner?.body) return
770
+ const cycle = resolving.indexOf(name)
771
+ if (cycle >= 0) fail(reference, `Reactive JSX local cycle: ${[...resolving.slice(cycle), name].join(" -> ")}`)
772
+ resolving.push(name)
773
+ const initializer = entries[0].initializer
774
+ const visit = node => {
775
+ if (ts.isIdentifier(node) && isReferenceIdentifier(node) && !isShadowedByParameter(node, initializer) && declarations.has(node.text)) resolve(node.text, node)
776
+ ts.forEachChild(node, visit)
777
+ }
778
+ visit(initializer)
779
+ substitutions.set(name, substituteClone(initializer, substitutions, factory, context))
780
+ resolving.pop()
781
+ }
782
+ const visit = node => {
783
+ if (ts.isIdentifier(node) && isReferenceIdentifier(node) && !isShadowedByParameter(node, expression) && declarations.has(node.text)) resolve(node.text, node)
784
+ ts.forEachChild(node, visit)
785
+ }
786
+ visit(expression)
787
+ if (!substitutions.size) return expression
788
+ const expanded = substituteClone(expression, substitutions, factory, context)
789
+ ts.setParentRecursive(expanded, false)
790
+ expanded.parent = expression.parent
791
+ const usedStates = referencedStateNames(expanded, setters)
792
+ if (!usedStates.size) return expression
793
+ const captures = captureNames(expanded, expanded, setters)
794
+ const allowedNames = new Set([...setters.values(), ...captures])
795
+ validateReactiveJsxExpression(expanded, allowedNames)
796
+ return expanded
797
+ }
798
+ const componentSpecializations = new WeakMap()
799
+ const setterHookHelpers = new WeakMap()
800
+ const expandedRowSpecializations = new WeakMap()
801
+ const nestedRowSpecializations = new Map()
802
+ const reducerComponentCalls = new WeakSet()
803
+ const rowHookCalls = []
804
+ const specializedDeclarations = new WeakSet()
805
+ const stateBackedComponentFunctions = new WeakSet()
806
+ const stateBackedComponentRoots = []
807
+ let specializedImportIndex = 0
808
+ const specialize = (call, component, label = "Keyed list", allowComponentRoot = false, ordinaryHooks = false, ordinaryStateNames = new Set(), ownership) => {
809
+ const result = specializeComponentCall(call, component, sourceFile, factory, context, fail, label, allowComponentRoot, ordinaryHooks, ordinaryStateNames)
810
+ const owner = nearestFunction(call)
811
+ const setters = ownership?.setters ?? settersForNode(call, settersByFunction)
812
+ const stateOwners = ownership?.stateOwners ?? stateOwnersForNode(call)
813
+ const callbacks = functionsForNode(call)
814
+ const propSignals = expression => {
815
+ const signals = new Set()
816
+ if (ts.isIdentifier(expression) && setters.has(expression.text)) signals.add(setters.get(expression.text))
817
+ const callback = ts.isIdentifier(expression) ? callbacks.get(expression.text) : undefined
818
+ for (const state of referencedStateNames((callback ?? expression).body ?? callback ?? expression, setters, callback ?? expression)) signals.add(state)
819
+ return [...signals].map(name => ({ name, owner: stateOwners.get(name) ?? (owner ? `owner:${ensureOwner(owner).slot}` : "module") }))
820
+ }
821
+ result.analysis = componentAnalysis.registerSpecialization({
822
+ kind: label,
823
+ ...(owner ? { owner: ensureOwner(owner).slot } : {}),
824
+ ...(analysisSource(call) ? { source: analysisSource(call) } : {}),
825
+ props: result.props.map(prop => {
826
+ const expression = result.propExpressions.get(prop.name)
827
+ const signals = expression ? propSignals(expression) : []
828
+ return { ...prop, ...(signals.length ? { signals } : {}) }
829
+ }),
830
+ states: [
831
+ ...result.rowStates.map(({ state, setter, source }) => ({ name: state, setter, kind: "row", ...(analysisSource(source) ? { source: analysisSource(source) } : {}) })),
832
+ ...result.ordinaryStates.map(({ state, setter, source }) => ({ name: state, setter, kind: "component", ...(analysisSource(source) ? { source: analysisSource(source) } : {}) }))
833
+ ],
834
+ refs: [...result.rowRefs.map(({ name, source }) => ({ name, kind: "row", ...(analysisSource(source) ? { source: analysisSource(source) } : {}) })), ...result.ordinaryRefs.map(({ name, source }) => ({ name, kind: "component", ...(analysisSource(source) ? { source: analysisSource(source) } : {}) }))],
835
+ ids: result.ordinaryIds.map(({ name, source }) => ({ name, ...(analysisSource(source) ? { source: analysisSource(source) } : {}) }))
836
+ })
837
+ for (const state of [...result.rowStates, ...result.ordinaryStates]) state.analysisOwner = `specialization:${result.analysis.slot}`
838
+ for (const ref of [...result.rowRefs, ...result.ordinaryRefs]) ref.analysisOwner = `specialization:${result.analysis.slot}`
839
+ return result
840
+ }
841
+ const registerRowHooks = (call, specialization) => {
842
+ if (!specialization.rowStates.length && !specialization.rowRefs.length) return
843
+ let owner
844
+ for (let current = call.parent; current; current = current.parent) {
845
+ if (isFunctionLike(current) && settersByFunction.has(current)) {
846
+ owner = current
847
+ break
848
+ }
849
+ }
850
+ if (!owner) owner = nearestFunction(call)
851
+ const setters = new Map(settersByFunction.get(owner))
852
+ const stateOwners = new Map(stateOwnersByFunction.get(owner))
853
+ for (const state of specialization.rowStates) {
854
+ setters.set(state.setter, state.state)
855
+ stateOwners.set(state.state, state.analysisOwner)
856
+ }
857
+ settersByFunction.set(owner, setters)
858
+ stateOwnersByFunction.set(owner, stateOwners)
859
+ rowHookCalls.push(call)
860
+ usesRowState ||= specialization.rowStates.length > 0
861
+ usesRowRef ||= specialization.rowRefs.length > 0
862
+ }
863
+ const mergeSpecializedImports = (root, componentSource, call, effects = []) => {
864
+ const componentImports = clientImportBindings(componentSource, componentSource.fileName, sourceFiles)
865
+ for (const name of runtimeImportNames(componentSource, false)) if (referenceIdentifiers(root, name).length) fail(call, "Imported specialized component handlers may only use relative TypeScript runtime imports")
866
+ const substitutions = new Map()
867
+ for (const statement of componentSource.statements) {
868
+ if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier) || !isStaticImport(statement.moduleSpecifier.text)) continue
869
+ const entry = staticImportEntry(statement, componentSource, componentSource.fileName, staticFiles, importedAssets, cssModules, base, factory)
870
+ if (!entry?.name) continue
871
+ if (referenceIdentifiers(root, entry.name).length) substitutions.set(entry.name, entry.value)
872
+ for (const effect of effects) {
873
+ if (effect.source.getSourceFile() !== componentSource) continue
874
+ if (!referenceIdentifiers(effect.call, entry.name).length) continue
875
+ ts.setParentRecursive(effect.call, false)
876
+ effect.call = substituteClone(effect.call, new Map([[entry.name, entry.value]]), factory, context)
877
+ synthesizeTree(effect.call)
878
+ }
879
+ }
880
+ for (const [name, entry] of componentImports) {
881
+ const references = referenceIdentifiers(root, name)
882
+ if (!references.length) continue
883
+ if (references.some(reference => !insideJsxEventHandler(reference, root))) fail(call, `Imported specialized component runtime import "${name}" may only be used inside event handlers`)
884
+ let local
885
+ do local = `__kDispatchImport${specializedImportIndex++}`
886
+ while (importBindings.has(local))
887
+ substitutions.set(name, factory.createIdentifier(local))
888
+ importBindings.set(local, { ...entry, local })
889
+ }
890
+ if (!substitutions.size) return root
891
+ const merged = substituteClone(root, substitutions, factory, context)
892
+ ts.setParentRecursive(merged, false)
893
+ merged.parent = root.parent
894
+ return merged
895
+ }
896
+ const expandReducerCallbacks = (root, componentSource, call) => {
897
+ const componentImports = clientImportBindings(componentSource, componentSource.fileName, sourceFiles)
898
+ const replacements = new WeakMap()
899
+ let count = 0
900
+ for (const [name, entry] of componentImports) {
901
+ if (entry.kind === "namespace") continue
902
+ const nestedCalls = jsxTagUses(root, name).filter(nestedCall => jsxCallHasReducerCallbackProp(nestedCall, reducersForNode(nestedCall, reducersByFunction)))
903
+ if (!nestedCalls.length) continue
904
+ const imported = entry.kind === "default" ? "default" : entry.imported
905
+ let nestedComponent
906
+ try {
907
+ nestedComponent = resolveComponentExport(entry.target, imported, importedSource, sourceFiles)
908
+ } catch {
909
+ fail(nestedCalls[0], "Reducer callback props require a component imported from a relative TypeScript module")
910
+ }
911
+ for (const nestedCall of nestedCalls) {
912
+ const nested = specialize(nestedCall, nestedComponent, "Reducer-callback")
913
+ if (nested.effects.length) fail(nestedCall, "Reducer-callback components cannot declare effects")
914
+ nested.root = mergeSpecializedImports(nested.root, nestedComponent.getSourceFile(), nestedCall, nested.effects)
915
+ synthesizeTree(nested.root)
916
+ replacements.set(nestedCall, nested.root)
917
+ count++
918
+ }
919
+ }
920
+ if (!count) return root
921
+ const expanded = replaceSpecializedCalls(root, replacements, context)
922
+ ts.setParentRecursive(expanded, false)
923
+ expanded.parent = root.parent
924
+ return expanded
925
+ }
926
+ const staticConditionValue = expression => {
927
+ const value = unwrapExpression(expression)
928
+ if (value.kind === ts.SyntaxKind.TrueKeyword) return true
929
+ if (value.kind === ts.SyntaxKind.FalseKeyword || value.kind === ts.SyntaxKind.NullKeyword || ts.isIdentifier(value) && value.text === "undefined") return false
930
+ if (ts.isStringLiteral(value) || ts.isNoSubstitutionTemplateLiteral(value)) return Boolean(value.text)
931
+ if (ts.isNumericLiteral(value)) return Number(value.text) !== 0
932
+ return undefined
933
+ }
934
+ const foldSetterStaticConditions = root => {
935
+ const visit = node => {
936
+ if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
937
+ const condition = staticConditionValue(node.left)
938
+ if (condition !== undefined) return condition ? ts.visitNode(node.right, visit) : node.left
939
+ }
940
+ if (ts.isConditionalExpression(node)) {
941
+ const condition = staticConditionValue(node.condition)
942
+ if (condition !== undefined) return ts.visitNode(condition ? node.whenTrue : node.whenFalse, visit)
943
+ }
944
+ return ts.visitEachChild(node, visit, context)
945
+ }
946
+ const folded = ts.visitNode(root, visit)
947
+ ts.setParentRecursive(folded, false)
948
+ folded.parent = root.parent
949
+ return folded
950
+ }
951
+ const expandSetterComponents = (root, componentSource, trail, aggregate, parentSetters, parentStateOwners) => {
952
+ root = foldSetterStaticConditions(root)
953
+ const replacements = new WeakMap()
954
+ let count = 0
955
+ const visit = (node, dynamic = false) => {
956
+ if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
957
+ visit(node.left, dynamic)
958
+ visit(node.right, true)
959
+ return
960
+ }
961
+ if (ts.isConditionalExpression(node)) {
962
+ visit(node.condition, dynamic)
963
+ visit(node.whenTrue, true)
964
+ visit(node.whenFalse, true)
965
+ return
966
+ }
967
+ const tag = jsxTagName(node)
968
+ if (tag && (!ts.isIdentifier(tag) || tag.text[0] !== tag.text[0].toLowerCase())) {
969
+ if (!ts.isIdentifier(tag)) fail(node, "Nested setter-callback components must use identifier JSX tags")
970
+ const name = tag.text
971
+ let component = localComponentDeclaration(componentSource, name)
972
+ let imported = false
973
+ if (!component) {
974
+ const binding = clientImportBindings(componentSource, componentSource.fileName, sourceFiles).get(name)
975
+ if (!binding || binding.kind === "namespace") fail(node, `Nested setter-callback component ${name} must be declared locally or imported from a relative TypeScript module`)
976
+ component = resolveComponentExport(binding.target, binding.kind === "default" ? "default" : binding.imported, importedSource, sourceFiles)
977
+ imported = true
978
+ }
979
+ if (trail.includes(component)) {
980
+ const chain = [...trail, component].map(entry => entry.name?.text || "anonymous").join(" -> ")
981
+ fail(node, `Nested setter-callback component cycle: ${chain}`)
982
+ }
983
+ const setters = new Map(parentSetters)
984
+ for (const state of aggregate.ordinaryStates) setters.set(state.setter, state.state)
985
+ const stateOwners = new Map(parentStateOwners)
986
+ for (const state of aggregate.ordinaryStates) stateOwners.set(state.state, state.analysisOwner)
987
+ if (jsxSetterCallbackProps(node, setters, functionsForNode(node), reducersForNode(node, reducersByFunction)).length) fail(node, "Setter callbacks cannot cross a second component boundary")
988
+ const nested = specialize(node, component, "Nested setter-callback", true, true, new Set(setters.values()), { setters, stateOwners })
989
+ if (dynamic && (nested.hookDeclarations.length || nested.effects.length)) fail(node, "Hookful nested setter-callback components require an unconditional or statically truthy render path")
990
+ nested.root = expandSetterComponents(nested.root, component.getSourceFile(), [...trail, component], nested, setters, stateOwners)
991
+ if (imported) synthesizeTree(nested.root = mergeSpecializedImports(nested.root, component.getSourceFile(), node, nested.effects))
992
+ aggregate.calculations.push(...nested.calculations)
993
+ aggregate.effects.push(...nested.effects)
994
+ aggregate.hookDeclarations.push(...nested.hookDeclarations)
995
+ aggregate.ordinaryStates.push(...nested.ordinaryStates)
996
+ aggregate.ordinaryRefs.push(...nested.ordinaryRefs)
997
+ aggregate.usesComponentId ||= nested.usesComponentId
998
+ replacements.set(node, nested.root)
999
+ count++
1000
+ return
1001
+ }
1002
+ ts.forEachChild(node, child => visit(child, dynamic))
1003
+ }
1004
+ visit(root)
1005
+ if (!count) return root
1006
+ const expanded = replaceSpecializedCalls(root, replacements, context)
1007
+ ts.setParentRecursive(expanded, false)
1008
+ expanded.parent = root.parent
1009
+ return expanded
1010
+ }
1011
+ for (const [name, component] of components) {
1012
+ const calls = jsxTagUses(sourceFile, name)
1013
+ const stateBackedCalls = calls.filter(call => isStateBackedListComponentCall(call, component.function, settersByFunction.get(nearestFunction(call)) ?? new Map()))
1014
+ if (!stateBackedCalls.length) continue
1015
+ if (isExportedDeclaration(component.declaration)) fail(component.declaration, `State-backed list component ${name} cannot be exported`)
1016
+ if (identifierReferenceCount(sourceFile, name) !== calls.length) fail(component.declaration, `State-backed list component ${name} may only be referenced as JSX`)
1017
+ if (stateBackedCalls.length !== calls.length) fail(component.declaration, `State-backed list component ${name} must receive its mapped prop from local state at every call`)
1018
+ for (const call of stateBackedCalls) {
1019
+ const specialization = specialize(call, component.function)
1020
+ if (specialization.effects.length) fail(call, "State-backed list components cannot declare effects")
1021
+ componentSpecializations.set(call, specialization)
1022
+ stateBackedComponentRoots.push(specialization.root)
1023
+ }
1024
+ specializedDeclarations.add(component.declaration)
1025
+ stateBackedComponentFunctions.add(component.function)
1026
+ }
1027
+ for (const [name, binding] of importBindings) {
1028
+ if (binding.kind === "namespace") continue
1029
+ const calls = jsxTagUses(sourceFile, name)
1030
+ if (!calls.some(call => jsxCallHasDirectStateProp(call, settersByFunction.get(nearestFunction(call)) ?? new Map()))) continue
1031
+ const imported = binding.kind === "default" ? "default" : binding.imported
1032
+ let component
1033
+ try {
1034
+ component = resolveComponentExport(binding.target, imported, importedSource, sourceFiles)
1035
+ } catch (error) {
1036
+ if (error.message.includes("does not export a statically analyzable keyed list component")) continue
1037
+ throw error
1038
+ }
1039
+ const stateBackedCalls = calls.filter(call => isStateBackedListComponentCall(call, component, settersByFunction.get(nearestFunction(call)) ?? new Map()))
1040
+ for (const call of stateBackedCalls) {
1041
+ const specialization = specialize(call, component)
1042
+ if (specialization.effects.length) fail(call, "State-backed list components cannot declare effects")
1043
+ componentSpecializations.set(call, specialization)
1044
+ stateBackedComponentRoots.push(specialization.root)
1045
+ }
1046
+ }
1047
+ const specializeSetterCallbacks = (call, component, callbackProps, imported) => {
1048
+ if (componentSpecializations.has(call)) fail(call, "Setter callback props cannot be combined with another component specialization")
1049
+ if (component.parameters.length !== 1 || !ts.isObjectBindingPattern(component.parameters[0].name)) fail(component, "Setter-callback components must use one destructured props parameter")
1050
+ for (const prop of callbackProps) {
1051
+ const element = component.parameters[0].name.elements.find(entry => !entry.dotDotDotToken && (entry.propertyName ?? entry.name).getText() === prop)
1052
+ if (!element || !ts.isIdentifier(element.name)) fail(call, `Setter-callback component must destructure callback prop ${JSON.stringify(prop)}`)
1053
+ const references = []
1054
+ const collectReferences = node => {
1055
+ if (ts.isIdentifier(node) && node.text === element.name.text && isReferenceIdentifier(node)) references.push(node)
1056
+ ts.forEachChild(node, collectReferences)
1057
+ }
1058
+ collectReferences(component.body)
1059
+ if (references.length !== 1) fail(element, `Setter-callback prop ${JSON.stringify(prop)} must be used exactly once in the component`)
1060
+ }
1061
+ const specialization = specialize(call, component, "Setter-callback", false, true, new Set(settersForNode(call, settersByFunction).values()))
1062
+ if (specialization.hookDeclarations.length || specialization.effects.length) {
1063
+ const substitutions = new Map()
1064
+ const attributes = ts.isJsxElement(call) ? call.openingElement.attributes : call.attributes
1065
+ for (const attribute of attributes.properties) {
1066
+ if (!ts.isJsxAttribute(attribute) || !callbackProps.includes(attribute.name.text) || !attribute.initializer || !ts.isJsxExpression(attribute.initializer) || !ts.isIdentifier(attribute.initializer.expression)) continue
1067
+ const callback = functionsForNode(attribute).get(attribute.initializer.expression.text)
1068
+ if (callback) substitutions.set(attribute.initializer.expression.text, callback)
1069
+ }
1070
+ if (substitutions.size) {
1071
+ specialization.root = substituteClone(specialization.root, substitutions, factory, context)
1072
+ for (const effect of specialization.effects) effect.call = substituteClone(effect.call, substitutions, factory, context)
1073
+ }
1074
+ }
1075
+ specialization.root = expandSetterComponents(specialization.root, component.getSourceFile(), [component], specialization, settersForNode(call, settersByFunction), stateOwnersForNode(call))
1076
+ if (imported) synthesizeTree(specialization.root = mergeSpecializedImports(specialization.root, component.getSourceFile(), call, specialization.effects))
1077
+ if (specialization.hookDeclarations.length || specialization.effects.length) {
1078
+ const owner = nearestFunction(call)
1079
+ const name = `KSetterComponent${Math.max(0, call.pos)}`
1080
+ const effectStatements = specialization.effects.map(entry => {
1081
+ const effectCall = factory.updateCallExpression(entry.call, factory.createIdentifier("__kComponentUseEffect"), entry.call.typeArguments, entry.call.arguments)
1082
+ synthesizeTree(effectCall)
1083
+ ts.setOriginalNode(effectCall, entry.source)
1084
+ return factory.createExpressionStatement(effectCall)
1085
+ })
1086
+ const helper = factory.createFunctionDeclaration(
1087
+ undefined,
1088
+ undefined,
1089
+ name,
1090
+ undefined,
1091
+ [],
1092
+ undefined,
1093
+ factory.createBlock([...specialization.hookDeclarations, ...effectStatements, factory.createReturnStatement(specialization.root)], true)
1094
+ )
1095
+ ts.setParentRecursive(helper, false)
1096
+ helper.parent = owner.body
1097
+ const helpers = setterHookHelpers.get(owner.body) ?? []
1098
+ helpers.push(helper)
1099
+ setterHookHelpers.set(owner.body, helpers)
1100
+ const setters = new Map(settersForNode(call, settersByFunction))
1101
+ for (const state of specialization.ordinaryStates) setters.set(state.setter, state.state)
1102
+ settersByFunction.set(helper, setters)
1103
+ const stateOwners = new Map(stateOwnersForNode(call))
1104
+ for (const state of specialization.ordinaryStates) stateOwners.set(state.state, state.analysisOwner)
1105
+ stateOwnersByFunction.set(helper, stateOwners)
1106
+ usesComponentState ||= specialization.ordinaryStates.length > 0
1107
+ usesComponentId ||= specialization.usesComponentId
1108
+ usesComponentRef ||= specialization.ordinaryRefs.length > 0
1109
+ usesComponentEffects ||= specialization.effects.length > 0
1110
+ specialization.root = factory.createJsxSelfClosingElement(factory.createIdentifier(name), undefined, factory.createJsxAttributes([]))
1111
+ ts.setParentRecursive(specialization.root, false)
1112
+ specialization.root.parent = call.parent
1113
+ }
1114
+ componentSpecializations.set(call, specialization)
1115
+ }
1116
+ for (const [name, component] of components) {
1117
+ for (const call of jsxTagUses(sourceFile, name)) {
1118
+ const callbackProps = jsxSetterCallbackProps(call, settersByFunction.get(nearestFunction(call)) ?? new Map(), functionsForNode(call), reducersForNode(call, reducersByFunction))
1119
+ if (callbackProps.length) specializeSetterCallbacks(call, component.function, callbackProps, false)
1120
+ }
1121
+ }
1122
+ for (const [name, binding] of importBindings) {
1123
+ if (binding.kind === "namespace") continue
1124
+ const calls = jsxTagUses(sourceFile, name)
1125
+ const callbackCalls = calls.map(call => ({ call, callbackProps: jsxSetterCallbackProps(call, settersByFunction.get(nearestFunction(call)) ?? new Map(), functionsForNode(call), reducersForNode(call, reducersByFunction)) })).filter(entry => entry.callbackProps.length)
1126
+ if (!callbackCalls.length) continue
1127
+ const imported = binding.kind === "default" ? "default" : binding.imported
1128
+ let component
1129
+ try {
1130
+ component = resolveComponentExport(binding.target, imported, importedSource, sourceFiles)
1131
+ } catch {
1132
+ fail(callbackCalls[0].call, "Setter callback props require a component imported from a relative TypeScript module")
1133
+ }
1134
+ for (const { call, callbackProps } of callbackCalls) specializeSetterCallbacks(call, component, callbackProps, true)
1135
+ }
1136
+ for (const [name, component] of components) {
1137
+ const calls = jsxTagUses(sourceFile, name)
1138
+ const dispatchCalls = calls.filter(call => jsxCallHasDirectReducerProp(call, reducersForNode(call, reducersByFunction)))
1139
+ if (!dispatchCalls.length) continue
1140
+ if (isExportedDeclaration(component.declaration)) fail(component.declaration, `Reducer-dispatch component ${name} cannot be exported`)
1141
+ if (identifierReferenceCount(sourceFile, name) !== calls.length) fail(component.declaration, `Reducer-dispatch component ${name} may only be referenced as JSX`)
1142
+ if (dispatchCalls.length !== calls.length) fail(component.declaration, `Reducer-dispatch component ${name} must receive a direct local reducer dispatch at every call`)
1143
+ for (const call of dispatchCalls) {
1144
+ if (componentSpecializations.has(call)) fail(call, "Reducer dispatch props cannot be combined with another component specialization")
1145
+ const specialization = specialize(call, component.function, "Reducer-dispatch")
1146
+ registerRowHooks(call, specialization)
1147
+ specialization.root = expandReducerCallbacks(specialization.root, component.function.getSourceFile(), call)
1148
+ componentSpecializations.set(call, specialization)
1149
+ reducerComponentCalls.add(call)
1150
+ }
1151
+ specializedDeclarations.add(component.declaration)
1152
+ }
1153
+ for (const [name, binding] of importBindings) {
1154
+ if (binding.kind === "namespace") continue
1155
+ const calls = jsxTagUses(sourceFile, name)
1156
+ const dispatchCalls = calls.filter(call => jsxCallHasDirectReducerProp(call, reducersForNode(call, reducersByFunction)))
1157
+ if (!dispatchCalls.length) continue
1158
+ const imported = binding.kind === "default" ? "default" : binding.imported
1159
+ let component
1160
+ try {
1161
+ component = resolveComponentExport(binding.target, imported, importedSource, sourceFiles)
1162
+ } catch {
1163
+ fail(dispatchCalls[0], `Reducer dispatch props require a component imported from a relative TypeScript module`)
1164
+ }
1165
+ const componentSource = component.getSourceFile()
1166
+ for (const call of dispatchCalls) {
1167
+ if (componentSpecializations.has(call)) fail(call, "Reducer dispatch props cannot be combined with another component specialization")
1168
+ const specialization = specialize(call, component, "Reducer-dispatch")
1169
+ registerRowHooks(call, specialization)
1170
+ specialization.root = expandReducerCallbacks(specialization.root, componentSource, call)
1171
+ specialization.root = mergeSpecializedImports(specialization.root, componentSource, call, specialization.effects)
1172
+ synthesizeTree(specialization.root)
1173
+ componentSpecializations.set(call, specialization)
1174
+ reducerComponentCalls.add(call)
1175
+ }
1176
+ }
1177
+ const rawRenderedLists = []
1178
+ const collectRenderedLists = node => {
1179
+ const specialization = componentSpecializations.get(node)
1180
+ if (specialization) {
1181
+ collectRenderedLists(specialization.root)
1182
+ return
1183
+ }
1184
+ if (ts.isJsxExpression(node) && node.initializer === undefined && node.expression && (ts.isJsxElement(node.parent) || ts.isJsxFragment(node.parent))) {
1185
+ const owner = nearestFunction(node)
1186
+ const setters = settersForNode(node, settersByFunction)
1187
+ const staticCollection = state => [...(localStateSettersByFunction.get(owner) ?? [])].some(setter => setters.get(setter) === state && !referenceIdentifiers(owner.body, setter).length)
1188
+ const calculatedCollection = expression => {
1189
+ const value = unwrapExpression(expression)
1190
+ if (!ts.isPropertyAccessExpression(value) || !ts.isIdentifier(value.expression)) return undefined
1191
+ const entries = jsxLocalDeclarations.get(nearestFunction(node))?.get(value.expression.text)
1192
+ if (!entries?.length) return undefined
1193
+ const initializer = entries.length === 1 ? unwrapExpression(entries[0].initializer) : undefined
1194
+ if (!initializer || !ts.isCallExpression(initializer) || !ts.isIdentifier(initializer.expression) || !importBindings.has(initializer.expression.text)) return undefined
1195
+ if (entries[0].node.parent?.parent?.parent !== nearestFunction(entries[0].node)?.body) fail(value.expression, `Calculated collection result "${value.expression.text}" must be one top-level immutable local`)
1196
+ validateImportedCalculation(initializer, value.name.text)
1197
+ const expanded = resolveReactiveJsxExpression(value, nearestFunction(node), setters)
1198
+ if (expanded === value || !referencedStateNames(expanded, setters).size) fail(value, "Calculated collection fields must directly depend on local state")
1199
+ return expanded
1200
+ }
1201
+ const parts = listLocalUses.find(entry => entry.node === node)?.parts ?? keyedListParts(node.expression, setters, jsxLocalDeclarations.get(owner), fail, new Set(), importedCollections, factory, context, importedCollectionTransforms, calculatedCollection, staticCollection)
1202
+ if (parts) {
1203
+ for (const declaration of parts.aliasDeclarations ?? []) if (!listLocalDeclarations.includes(declaration)) listLocalDeclarations.push(declaration)
1204
+ rawRenderedLists.push({ node, parts })
1205
+ }
1206
+ }
1207
+ ts.forEachChild(node, collectRenderedLists)
1208
+ }
1209
+ collectRenderedLists(sourceFile)
1210
+ const collectionAliasUses = rawRenderedLists.flatMap(({ parts }) => parts.aliasUses ?? [])
1211
+ const collectionAliasDeclarations = new Set(rawRenderedLists.flatMap(({ parts }) => parts.aliasDeclarations ?? []))
1212
+ for (const declaration of collectionAliasDeclarations) {
1213
+ const owner = nearestFunction(declaration)
1214
+ const unsupported = identifierReferences(owner.body, declaration.name.text).find(reference => !collectionAliasUses.includes(reference))
1215
+ if (unsupported) fail(unsupported, `Rendered collection alias "${declaration.name.text}" may only be used as a rendered collection source`)
1216
+ }
1217
+ const rejectUnsupportedRenderControl = node => {
1218
+ if (ts.isIfStatement(node) && containsRenderControl(node, jsxLocalsByFunction.get(nearestFunction(node)) ?? new Set())) {
1219
+ const setters = settersForNode(node, settersByFunction)
1220
+ if (referencedStateNames(node.expression, setters).size) {
1221
+ fail(node, "Reactive render if statements must use terminal returns or exhaustive adjacent JSX assignment")
1222
+ }
1223
+ }
1224
+ ts.forEachChild(node, rejectUnsupportedRenderControl)
1225
+ }
1226
+ rejectUnsupportedRenderControl(sourceFile)
1227
+ const listComponentNames = new Set(rawRenderedLists.flatMap(({ parts }) => {
1228
+ const tag = jsxTagName(parts.root)
1229
+ return tag && ts.isIdentifier(tag) && tag.text[0] === tag.text[0].toUpperCase() ? [tag.text] : []
1230
+ }))
1231
+ const keyedComponentCalls = new Set(rawRenderedLists.map(({ parts }) => parts.root))
1232
+ for (const call of rowHookCalls) if (!keyedComponentCalls.has(call)) fail(call, "Keyed row hooks are only supported in direct keyed map rows")
1233
+ for (const name of listComponentNames) {
1234
+ let component = components.get(name)
1235
+ const local = Boolean(component)
1236
+ if (!component) {
1237
+ const binding = importBindings.get(name)
1238
+ if (!binding || binding.kind === "namespace") fail(sourceFile, `Keyed list component ${name} must be declared locally or imported from a relative TypeScript module`)
1239
+ const imported = binding.kind === "default" ? "default" : binding.imported
1240
+ component = { function: resolveComponentExport(binding.target, imported, importedSource, sourceFiles), declaration: undefined }
1241
+ }
1242
+ const declaredCalls = jsxTagUses(sourceFile, name)
1243
+ if (local && identifierReferenceCount(sourceFile, name) !== declaredCalls.length) fail(component.declaration, `Keyed list component ${name} may only be referenced as JSX`)
1244
+ const calls = [...new Set([
1245
+ ...declaredCalls.filter(call => !stateBackedComponentFunctions.has(nearestFunction(call))),
1246
+ ...stateBackedComponentRoots.flatMap(root => jsxTagUses(root, name))
1247
+ ])]
1248
+ for (const call of calls) {
1249
+ const specialization = reducerComponentCalls.has(call)
1250
+ ? componentSpecializations.get(call)
1251
+ : specialize(call, component.function, "Keyed list", true)
1252
+ registerRowHooks(call, specialization)
1253
+ if (specialization.effects.length && !keyedComponentCalls.has(call)) fail(call, "Effectful keyed row components may only be used directly as keyed map rows")
1254
+ specialization.component = component.function
1255
+ specialization.componentSource = component.function.getSourceFile()
1256
+ specialization.imported = !local
1257
+ componentSpecializations.set(call, specialization)
1258
+ }
1259
+ if (local) specializedDeclarations.add(component.declaration)
1260
+ }
1261
+ const expandKeyedComponents = (root, componentSource, trail = [], aggregate) => {
1262
+ const replacements = new WeakMap()
1263
+ let count = 0
1264
+ const visit = (node, currentAggregate = aggregate) => {
1265
+ if (node !== root && ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "map" && containsJsx(node)) {
1266
+ const nestedAggregate = { calculations: [], effects: [], hookDeclarations: [], rowStates: [], rowRefs: [], specializations: [] }
1267
+ for (const argument of node.arguments) visit(argument, nestedAggregate)
1268
+ if (nestedAggregate.hookDeclarations.length || nestedAggregate.effects.length) nestedRowSpecializations.set(`${node.pos}:${node.end}`, nestedAggregate)
1269
+ return
1270
+ }
1271
+ const tag = jsxTagName(node)
1272
+ if (tag && (!ts.isIdentifier(tag) || tag.text[0] !== tag.text[0].toLowerCase())) {
1273
+ if (!ts.isIdentifier(tag)) fail(node, "Keyed list components must use identifier JSX tags")
1274
+ const name = tag.text
1275
+ let component = localComponentDeclaration(componentSource, name)
1276
+ let imported = false
1277
+ if (!component) {
1278
+ const binding = clientImportBindings(componentSource, componentSource.fileName, sourceFiles).get(name)
1279
+ if (!binding || binding.kind === "namespace") fail(node, `Keyed list component ${name} must be declared locally or imported from a relative TypeScript module`)
1280
+ component = resolveComponentExport(binding.target, binding.kind === "default" ? "default" : binding.imported, importedSource, sourceFiles)
1281
+ imported = true
1282
+ }
1283
+ if (trail.includes(component)) {
1284
+ const chain = [...trail, component].map(entry => entry.name?.text || "anonymous").join(" -> ")
1285
+ fail(node, `Keyed list component cycle: ${chain}`)
1286
+ }
1287
+ const specialization = specialize(node, component, "Keyed list", true)
1288
+ registerRowHooks(node, specialization)
1289
+ specialization.root = expandKeyedComponents(specialization.root, component.getSourceFile(), [...trail, component], specialization)
1290
+ if (imported) synthesizeTree(specialization.root = mergeSpecializedImports(specialization.root, component.getSourceFile(), node, specialization.effects))
1291
+ expandedRowSpecializations.set(specialization.root, specialization)
1292
+ if (currentAggregate) {
1293
+ currentAggregate.specializations ??= []
1294
+ currentAggregate.specializations.push(specialization.analysis.slot, ...(specialization.specializations ?? []))
1295
+ currentAggregate.effects.push(...specialization.effects)
1296
+ currentAggregate.hookDeclarations.push(...specialization.hookDeclarations)
1297
+ currentAggregate.rowStates.push(...specialization.rowStates)
1298
+ currentAggregate.rowRefs.push(...specialization.rowRefs)
1299
+ }
1300
+ replacements.set(node, specialization.root)
1301
+ count++
1302
+ return
1303
+ }
1304
+ ts.forEachChild(node, child => visit(child, currentAggregate))
1305
+ }
1306
+ visit(root)
1307
+ if (!count) return root
1308
+ const expanded = replaceSpecializedCalls(root, replacements, context)
1309
+ ts.setParentRecursive(expanded, false)
1310
+ expanded.parent = root.parent
1311
+ return expanded
1312
+ }
1313
+ const preparedRenderedLists = []
1314
+ const prepareListCallback = (callback, root, specialization) => {
1315
+ const statements = [...specialization.hookDeclarations]
1316
+ if (specialization.effects.length) {
1317
+ usesListEffects = true
1318
+ statements.push(...specialization.effects.map(entry => {
1319
+ const call = factory.updateCallExpression(entry.call, factory.createIdentifier("__kListUseEffect"), entry.call.typeArguments, entry.call.arguments)
1320
+ synthesizeTree(call)
1321
+ ts.setOriginalNode(call, entry.source)
1322
+ return factory.createExpressionStatement(call)
1323
+ }))
1324
+ }
1325
+ if (!statements.length) return callback
1326
+ const prepared = factory.updateArrowFunction(callback, callback.modifiers, callback.typeParameters, callback.parameters, callback.type, callback.equalsGreaterThanToken, factory.createBlock([...statements, factory.createReturnStatement(root)], true))
1327
+ ts.setParentRecursive(prepared, false)
1328
+ prepared.parent = callback.parent
1329
+ return prepared
1330
+ }
1331
+ for (const { node, parts: originalParts } of rawRenderedLists) {
1332
+ if (keyedListParentTag(node) === "table") throw new Error("Keyed table rows must be wrapped in <tbody>, <thead>, or <tfoot>")
1333
+ const specialization = componentSpecializations.get(originalParts.root) ?? { root: originalParts.root, calculations: [], effects: [], hookDeclarations: [], rowStates: [], rowRefs: [], ordinaryStates: [] }
1334
+ const componentSource = specialization.componentSource ?? sourceFile
1335
+ specialization.root = expandKeyedComponents(specialization.root, componentSource, specialization.component ? [specialization.component] : [], specialization)
1336
+ if (specialization.imported) synthesizeTree(specialization.root = mergeSpecializedImports(specialization.root, componentSource, originalParts.root, specialization.effects))
1337
+ if (specialization.root !== originalParts.root) componentSpecializations.set(originalParts.root, specialization)
1338
+ const root = specialization.root
1339
+ let callback = root === originalParts.root ? originalParts.callback : factory.updateArrowFunction(
1340
+ originalParts.callback,
1341
+ originalParts.callback.modifiers,
1342
+ originalParts.callback.typeParameters,
1343
+ originalParts.callback.parameters,
1344
+ originalParts.callback.type,
1345
+ originalParts.callback.equalsGreaterThanToken,
1346
+ root
1347
+ )
1348
+ if (callback !== originalParts.callback) {
1349
+ ts.setParentRecursive(callback, false)
1350
+ callback.parent = originalParts.callback.parent
1351
+ }
1352
+ callback = prepareListCallback(callback, root, specialization)
1353
+ const parts = {
1354
+ ...originalParts,
1355
+ root,
1356
+ callback,
1357
+ specializations: [specialization.analysis?.slot, ...(specialization.specializations ?? [])].filter(slot => slot !== undefined),
1358
+ rowStates: [...specialization.rowStates, ...specialization.ordinaryStates],
1359
+ rowRefs: specialization.rowRefs,
1360
+ analysisStateOwners: new Map([...stateOwnersForNode(originalParts.root), ...[...specialization.rowStates, ...specialization.ordinaryStates].map(state => [state.state, state.analysisOwner])])
1361
+ }
1362
+ for (const calculation of specialization.calculations) {
1363
+ ts.setParentRecursive(calculation, false)
1364
+ calculation.parent = callback
1365
+ validateListExpression(calculation, parts.item, originalParts.root, fail)
1366
+ }
1367
+ const analysis = validateKeyedList(parts, sourceFile, settersForNode(originalParts.root, settersByFunction), specialization.rowStates, componentSpecializations, expandedRowSpecializations, nestedRowSpecializations, factory, prepareListCallback)
1368
+ preparedRenderedLists.push({ node, parts, analysis })
1369
+ }
1370
+
1371
+ const compileRenderExpression = (expression, anchor) => {
1372
+ const parts = conditionalParts(expression)
1373
+ if (!parts) return ts.visitNode(expression, visitor)
1374
+ const setters = settersForNode(anchor, settersByFunction)
1375
+ const usedStates = referencedStateNames(parts.condition, setters)
1376
+ const captures = captureNames(parts.condition, parts.condition, setters)
1377
+ if (!usedStates.size && !captures.size) return ts.visitEachChild(expression, visitor, context)
1378
+ usesBehavior = true
1379
+ usesConditional = true
1380
+ return descriptors.compileConditional(
1381
+ parts.kind,
1382
+ parts.condition,
1383
+ compileRenderExpression(parts.truthy, anchor),
1384
+ compileRenderExpression(parts.falsy, anchor),
1385
+ setters
1386
+ )
1387
+ }
1388
+
1389
+ let activeStateOwners
1390
+ let activeKeyedBlock
1391
+ const visitWithStateOwners = (node, stateOwners) => {
1392
+ const previous = activeStateOwners
1393
+ activeStateOwners = new Map([...(previous ?? []), ...stateOwners])
1394
+ const result = ts.visitNode(node, visitor)
1395
+ activeStateOwners = previous
1396
+ return result
1397
+ }
1398
+ const keyedEntry = (entries, node) => entries.find(entry => entry.node === node)
1399
+ const compileKeyedBlock = (node, { parts: listParts, analysis }) => {
1400
+ usesBehavior = true
1401
+ usesList = true
1402
+ const blockSlot = moduleIR.keyedBlocks.length
1403
+ let listSource = listParts.state
1404
+ let collection = { kind: "signal", name: listParts.state?.text }
1405
+ if (listParts.calculation) {
1406
+ usesBinding = true
1407
+ listSource = descriptors.compileReactiveBinding(listParts.calculation, { setters: settersForNode(node, settersByFunction), importBindings, keyedBlock: blockSlot })
1408
+ const exportName = ts.isCallExpression(listSource) && ts.isStringLiteral(listSource.arguments[2]) ? listSource.arguments[2].text : undefined
1409
+ collection = { kind: "binding", ...(exportName ? { exportName } : {}) }
1410
+ }
1411
+ const derived = listParts.selector?.length ? descriptors.registerDerived("selector", listParts.selector, listParts.selectorStates, node) : undefined
1412
+ const parent = activeKeyedBlock?.block
1413
+ const rowStates = (listParts.rowStates ?? []).map(state => ({ name: state.state, setter: state.setter, owner: state.analysisOwner, ...(analysisSource(state.source) ? { source: analysisSource(state.source) } : {}) }))
1414
+ const rowRefs = (listParts.rowRefs ?? []).map(ref => ({ name: ref.name, owner: ref.analysisOwner, ...(analysisSource(ref.source) ? { source: analysisSource(ref.source) } : {}) }))
1415
+ const specializations = [...new Set([...(listParts.specializations ?? []), ...rowStates.map(state => state.owner), ...rowRefs.map(ref => ref.owner)].filter(value => value !== undefined).map(value => typeof value === "string" ? Number(value.slice(value.lastIndexOf(":") + 1)) : value))]
1416
+ const block = descriptors.registerKeyedBlock({
1417
+ ...(analysisSource(node) ? { source: analysisSource(node) } : {}),
1418
+ ...(parent ? { parent: parent.slot } : {}),
1419
+ children: [],
1420
+ collection,
1421
+ key: listParts.keyField,
1422
+ ...(listParts.ownerField ? { ownerField: listParts.ownerField } : {}),
1423
+ item: listParts.item,
1424
+ ...(listParts.index ? { index: listParts.index } : {}),
1425
+ indexed: listParts.indexed,
1426
+ static: Boolean(listParts.static),
1427
+ ...(derived ? { selector: derived.slot } : {}),
1428
+ selectorStates: [...(listParts.selectorStates ?? [])],
1429
+ specializations,
1430
+ rowStates,
1431
+ rowRefs
1432
+ })
1433
+ if (parent) parent.children.push(block.slot)
1434
+ const previous = activeKeyedBlock
1435
+ activeKeyedBlock = { analysis, block, parts: listParts }
1436
+ const callback = visitWithStateOwners(listParts.callback, listParts.analysisStateOwners ?? new Map())
1437
+ activeKeyedBlock = previous
1438
+ const arguments_ = [
1439
+ listSource,
1440
+ block.key === null ? factory.createNull() : factory.createStringLiteral(block.key),
1441
+ callback,
1442
+ factory.createStringLiteral(block.ownerField ?? ""),
1443
+ jsonExpression(derived?.selector ?? listParts.selector ?? [], factory),
1444
+ block.indexed ? factory.createTrue() : factory.createFalse()
1445
+ ]
1446
+ if (block.selectorStates.length || block.static) arguments_.push(factory.createArrayLiteralExpression(block.selectorStates.map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), factory.createIdentifier(name)]))))
1447
+ if (block.static) arguments_.push(factory.createTrue())
1448
+ return factory.updateJsxExpression(node, factory.createCallExpression(factory.createIdentifier("__kList"), undefined, arguments_))
1449
+ }
1450
+ const visitor = node => {
1451
+ if (ts.isVariableDeclaration(node) && ts.isObjectBindingPattern(node.name) && customHookPrivateFields.has(node)) {
1452
+ const privateFields = customHookPrivateFields.get(node)
1453
+ return factory.updateVariableDeclaration(node, factory.updateObjectBindingPattern(node.name, [
1454
+ ...node.name.elements,
1455
+ ...privateFields.map(name => factory.createBindingElement(undefined, undefined, name))
1456
+ ]), node.exclamationToken, node.type, node.initializer)
1457
+ }
1458
+ if (ts.isBlock(node) && setterHookHelpers.has(node)) {
1459
+ return ts.visitEachChild(factory.updateBlock(node, [...setterHookHelpers.get(node), ...node.statements]), visitor, context)
1460
+ }
1461
+ if (specializedDeclarations.has(node)) return node
1462
+ if (componentSpecializations.has(node)) {
1463
+ const specialization = componentSpecializations.get(node)
1464
+ const stateOwners = new Map([...stateOwnersForNode(node), ...[...specialization.rowStates, ...specialization.ordinaryStates].map(state => [state.state, state.analysisOwner])])
1465
+ return visitWithStateOwners(specialization.root, stateOwners)
1466
+ }
1467
+
1468
+ if (hasLinkElements && (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) && isStylesheetLink(node)) {
1469
+ fail(node, "Stylesheets must be placed under src/ or declared in kudzu.config styles so Kudzu can emit them in <head>")
1470
+ }
1471
+
1472
+ if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text === "react") {
1473
+ if (!node.importClause) fail(node, "Side-effect React imports are not supported because Kudzu does not load the React runtime")
1474
+ if (node.importClause.isTypeOnly) return node
1475
+ return factory.updateImportDeclaration(node, node.modifiers, node.importClause, factory.createStringLiteral("@kudzujs/core"), node.attributes)
1476
+ }
1477
+
1478
+ if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && packageBindings.size && importDeclarationNames(node).some(name => packageBindings.has(name))) return undefined
1479
+
1480
+ if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text.startsWith(".")) {
1481
+ if (!runtimeModuleReference(node)) return node
1482
+ if (isStaticImport(node.moduleSpecifier.text)) return staticImportEntry(node, sourceFile, file, staticFiles, importedAssets, cssModules, base, factory)?.replacement
1483
+ const target = resolveSourceImport(file, node.moduleSpecifier.text, sourceFiles)
1484
+ return factory.updateImportDeclaration(node, node.modifiers, node.importClause, factory.createStringLiteral(relativeModulePath(compiledPath(file), compiledPath(target))), node.attributes)
1485
+ }
1486
+
1487
+ if (ts.isExportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text.startsWith(".")) {
1488
+ if (!runtimeModuleReference(node)) return node
1489
+ const target = resolveSourceImport(file, node.moduleSpecifier.text, sourceFiles)
1490
+ return factory.updateExportDeclaration(node, node.modifiers, node.isTypeOnly, node.exportClause, factory.createStringLiteral(relativeModulePath(compiledPath(file), compiledPath(target))), node.attributes)
1491
+ }
1492
+
1493
+ const effectAlias = ts.isCallExpression(node) && ts.isIdentifier(node.expression) ? node.expression.text : undefined
1494
+ const listEffect = effectAlias === "__kListUseEffect"
1495
+ const specializedEffect = listEffect || effectAlias === "__kComponentUseEffect" ? (() => {
1496
+ const source = ts.getOriginalNode(node)
1497
+ const sourceFile = source.getSourceFile()
1498
+ return { source, sourceFile, imports: clientImportBindings(sourceFile, sourceFile.fileName, sourceFiles) }
1499
+ })() : undefined
1500
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && (hasUseEffectImport && effectAlias === "useEffect" || specializedEffect)) {
1501
+ const effectFail = (target, message) => {
1502
+ if (specializedEffect) throw sourceNodeError(specializedEffect.source, specializedEffect.sourceFile, message)
1503
+ fail(target, message)
1504
+ }
1505
+ if (node.arguments.length !== 2) effectFail(node, "useEffect() requires exactly a callback and literal dependency array")
1506
+ const [callbackArgument, dependencies] = node.arguments
1507
+ const effectOwner = nearestFunction(node)
1508
+ const resolveEffectFunction = expression => {
1509
+ if (!ts.isIdentifier(expression)) return undefined
1510
+ const entries = jsxLocalDeclarations.get(effectOwner)?.get(expression.text)
1511
+ if (entries?.length !== 1 || entries[0].node.parent?.parent?.parent !== effectOwner?.body) return undefined
1512
+ const initializer = entries[0].initializer
1513
+ return ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer) ? initializer : undefined
1514
+ }
1515
+ let callback = ts.isArrowFunction(callbackArgument) || ts.isFunctionExpression(callbackArgument) ? callbackArgument : resolveEffectFunction(callbackArgument)
1516
+ if (!callback) effectFail(callbackArgument, "useEffect() callback must be inline or one top-level const function")
1517
+ if (ts.isFunctionExpression(callback) && callback.name) effectFail(callback, "useEffect() callback function must be anonymous")
1518
+ if (callback.asteriskToken) effectFail(callback, "useEffect() callback cannot be a generator")
1519
+ if (callback.parameters.length) effectFail(callback, "useEffect() callback cannot declare parameters")
1520
+ if (!ts.isArrayLiteralExpression(dependencies)) effectFail(dependencies, "useEffect() dependencies must be a literal array")
1521
+ const setters = settersForNode(node, settersByFunction)
1522
+ const dependencyAnalysis = analyzeEffectDependencies({
1523
+ dependencies,
1524
+ node,
1525
+ listEffect,
1526
+ keyedItem: activeKeyedBlock?.parts.item,
1527
+ setters,
1528
+ localDeclarations: jsxLocalDeclarations.get(nearestFunction(node)),
1529
+ factory,
1530
+ fail: effectFail
1531
+ })
1532
+ const { dependencyItem, itemDependencies, ordinaryDependencies, entries: dependencyEntries, dependencyStates, substitutions: dependencySubstitutions, subscriptions: subscriptionDependencies, hasDerived: hasDerivedDependency } = dependencyAnalysis
1533
+ if (!effectOwner) fail(node, "useEffect() cannot be used outside a Kudzu component")
1534
+ if (!ts.isBlock(callback.body)) effectFail(callback, "useEffect() callback must use a block body")
1535
+ const cleanupSubstitutions = new Map()
1536
+ const collectNamedCleanups = current => {
1537
+ if (current !== callback && isFunctionLike(current)) return
1538
+ if (ts.isReturnStatement(current) && current.expression && ts.isIdentifier(unwrapExpression(current.expression))) {
1539
+ const cleanup = resolveEffectFunction(unwrapExpression(current.expression))
1540
+ if (cleanup) cleanupSubstitutions.set(unwrapExpression(current.expression).text, cleanup)
1541
+ }
1542
+ ts.forEachChild(current, collectNamedCleanups)
1543
+ }
1544
+ collectNamedCleanups(callback.body)
1545
+ if (cleanupSubstitutions.size) {
1546
+ callback = substituteClone(callback, cleanupSubstitutions, factory, context)
1547
+ ts.setParentRecursive(callback, false)
1548
+ callback.parent = callbackArgument.parent
1549
+ }
1550
+ const returns = effectReturns(callback)
1551
+ if (returns.invalid) effectFail(returns.invalid, "useEffect() return values must be inline cleanup functions")
1552
+ const invalidCleanup = returns.cleanups.find(cleanup => cleanup.parameters.length || cleanup.asteriskToken)
1553
+ if (invalidCleanup) effectFail(invalidCleanup, "useEffect() cleanup functions cannot declare parameters or be generators")
1554
+ if (returns.cleanup && callback.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) effectFail(callback, "useEffect() async callbacks cannot return cleanup functions")
1555
+ validateEffectOwnedBrowserResources(callback, returns, effectFail)
1556
+ const callbackSource = specializedEffect?.sourceFile ?? sourceFile
1557
+ const callbackFile = callbackSource.fileName
1558
+ let compiledCallback = dependencySubstitutions.size ? substituteClone(callback, dependencySubstitutions, factory, context) : callback
1559
+ if (compiledCallback !== callback) {
1560
+ ts.setParentRecursive(compiledCallback, false)
1561
+ compiledCallback.parent = callback.parent
1562
+ }
1563
+ let workers = []
1564
+ if (listEffect && callbackFile !== file) {
1565
+ const originalCallback = specializedEffect.source.arguments[0]
1566
+ workerCompiler.rejectConstructions(originalCallback, callbackSource, "Relative TypeScript Worker construction in imported keyed-row effects is not supported; construct the Worker in a directly compiled page or local component effect")
1567
+ } else {
1568
+ const rewritten = workerCompiler.rewriteEffect(compiledCallback, callbackFile, callbackSource, sourceFiles, factory, context)
1569
+ compiledCallback = rewritten.callback
1570
+ workers = rewritten.workers
1571
+ }
1572
+ const descriptor = descriptors.compileEffectCallback(compiledCallback, {
1573
+ setters,
1574
+ reducers: reducersForNode(node, reducersByFunction),
1575
+ importBindings: specializedEffect?.imports ?? importBindings,
1576
+ listItem: dependencyItem,
1577
+ keyedBlock: activeKeyedBlock?.block.slot,
1578
+ deferValues: true,
1579
+ snapshotNested: returns.cleanup,
1580
+ liveStates: customHookTimerStates
1581
+ })
1582
+ usesListItem ||= Boolean(itemDependencies.length && !listEffect)
1583
+ usesBehavior = true
1584
+ const derivedDependencies = hasDerivedDependency ? dependencyEntries.map(entry => entry.kind === "derived" ? descriptors.registerDerived("expression", entry.expression, entry.states, entry.source) : undefined) : []
1585
+ const effectSource = specializedEffect?.source ?? node
1586
+ const lexicalOwner = nearestFunction(effectSource)
1587
+ const effect = descriptors.registerEffect(descriptor, {
1588
+ cleanup: returns.cleanup,
1589
+ dependencies: hasDerivedDependency ? dependencyEntries.map((entry, index) => entry.kind === "derived" ? { kind: "derived", derived: derivedDependencies[index].slot, sources: [...entry.states] } : { kind: "signal", name: entry.name }) : ordinaryDependencies.map(dependency => ({ kind: "signal", name: dependency.text })),
1590
+ subscriptions: (hasDerivedDependency ? subscriptionDependencies : ordinaryDependencies).map(dependency => dependency.text),
1591
+ dependencyStates: [...dependencyStates.keys()],
1592
+ itemDependencies,
1593
+ ownership: {
1594
+ kind: activeKeyedBlock ? "keyed" : "component",
1595
+ ...(activeKeyedBlock ? { keyedBlock: activeKeyedBlock.block.slot } : {}),
1596
+ ...(lexicalOwner ? { component: { name: ownerName(lexicalOwner), ...(analysisSource(lexicalOwner) ? { source: analysisSource(lexicalOwner) } : {}) } } : {})
1597
+ },
1598
+ workers,
1599
+ ...(analysisSource(effectSource) ? { source: analysisSource(effectSource) } : {})
1600
+ })
1601
+ const dependencyExpressions = effect.dependencies.map(dependency => dependency.kind === "derived" ? moduleIR.derived[dependency.derived].expression : ["state", dependency.name])
1602
+ return factory.updateCallExpression(node, node.expression, node.typeArguments, [
1603
+ callback,
1604
+ factory.createArrayLiteralExpression(effect.subscriptions.map(name => factory.createIdentifier(name))),
1605
+ factory.createStringLiteral(handlerUrl),
1606
+ factory.createStringLiteral(effect.setup.exportName),
1607
+ descriptor.states,
1608
+ descriptor.scope,
1609
+ factory.createStringLiteral(specializedEffect ? sourceLocation(specializedEffect.source, specializedEffect.sourceFile) : sourceLocation(node, sourceFile)),
1610
+ effect.cleanup ? factory.createTrue() : factory.createFalse(),
1611
+ factory.createArrayLiteralExpression(effect.itemDependencies.map(field => factory.createStringLiteral(field))),
1612
+ hasDerivedDependency ? jsonExpression(dependencyExpressions, factory) : factory.createArrayLiteralExpression(),
1613
+ factory.createArrayLiteralExpression(effect.dependencyStates.map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), factory.createIdentifier(name)])))
1614
+ ])
1615
+ }
1616
+
1617
+ if (ts.isVariableDeclaration(node) && ts.isArrayBindingPattern(node.name) && node.initializer && ts.isCallExpression(node.initializer) && ts.isIdentifier(node.initializer.expression) && ((node.initializer.expression.text === "useState" || node.initializer.expression.text === "__kRowUseState" || node.initializer.expression.text === "__kComponentUseState") && node.initializer.arguments.length === 1 || node.initializer.expression.text === "useReducer" && node.initializer.arguments.length === 2)) {
1618
+ const stateElement = node.name.elements[0]
1619
+ if (!stateElement || !ts.isBindingElement(stateElement) || !ts.isIdentifier(stateElement.name)) return node
1620
+ const initializer = factory.updateCallExpression(node.initializer, node.initializer.expression, node.initializer.typeArguments, [
1621
+ ...node.initializer.arguments,
1622
+ factory.createStringLiteral(stateElement.name.text)
1623
+ ])
1624
+ return factory.updateVariableDeclaration(node, node.name, node.exclamationToken, node.type, initializer)
1625
+ }
1626
+
1627
+ if (ts.isVariableDeclaration(node) && listLocalDeclarations.includes(node)) {
1628
+ return factory.updateVariableDeclaration(node, node.name, node.exclamationToken, node.type, factory.createIdentifier("undefined"))
1629
+ }
1630
+
1631
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isCallExpression(node.initializer) && ts.isIdentifier(node.initializer.expression) && importBindings.has(node.initializer.expression.text)) {
1632
+ const setters = settersForNode(node, settersByFunction)
1633
+ const stateNames = new Set(setters.values())
1634
+ const rewrite = current => {
1635
+ if (ts.isShorthandPropertyAssignment(current) && stateNames.has(current.name.text)) return factory.createPropertyAssignment(current.name, factory.createPropertyAccessExpression(current.name, "value"))
1636
+ if (ts.isIdentifier(current) && stateNames.has(current.text) && isReferenceIdentifier(current)) return factory.createPropertyAccessExpression(current, "value")
1637
+ return ts.visitEachChild(current, rewrite, context)
1638
+ }
1639
+ if (referencedStateNames(node.initializer, setters).size) return factory.updateVariableDeclaration(node, node.name, node.exclamationToken, node.type, ts.visitNode(node.initializer, rewrite))
1640
+ }
1641
+
1642
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && jsxLocalsByFunction.get(nearestFunction(node))?.has(node.name.text) && referencesIdentifier(nearestFunction(node).body, node.name.text)) {
1643
+ const compiled = compileRenderExpression(node.initializer, node)
1644
+ if (compiled !== node.initializer) return factory.updateVariableDeclaration(node, node.name, node.exclamationToken, node.type, compiled)
1645
+ }
1646
+
1647
+ if (ts.isReturnStatement(node) && node.expression && isJsxLocalValue(node.expression, jsxLocalsByFunction.get(nearestFunction(node)) ?? new Set())) {
1648
+ const compiled = compileRenderExpression(node.expression, node)
1649
+ if (compiled !== node.expression) return factory.updateReturnStatement(node, compiled)
1650
+ }
1651
+
1652
+ const listCondition = ts.isJsxExpression(node) && node.expression ? keyedEntry(activeKeyedBlock?.analysis.conditions ?? [], node.expression) : undefined
1653
+ if (listCondition) {
1654
+ const entry = listCondition.value
1655
+ return factory.updateJsxExpression(node, descriptors.compileListConditional({
1656
+ ...entry,
1657
+ keyedBlock: activeKeyedBlock.block.slot,
1658
+ truthy: ts.visitNode(entry.truthy, visitor),
1659
+ falsy: ts.visitNode(entry.falsy, visitor)
1660
+ }))
1661
+ }
1662
+
1663
+ const listValue = ts.isJsxExpression(node) && node.expression ? keyedEntry(activeKeyedBlock?.analysis.values ?? [], node.expression) : undefined
1664
+ if (listValue) {
1665
+ return factory.updateJsxExpression(node, descriptors.compileListValue(node.expression, { ...listValue.value, keyedBlock: activeKeyedBlock.block.slot }))
1666
+ }
1667
+
1668
+ const attributeListValue = ts.isJsxAttribute(node) && node.initializer && ts.isJsxExpression(node.initializer) && node.initializer.expression ? keyedEntry(activeKeyedBlock?.analysis.values ?? [], node.initializer.expression) : undefined
1669
+ if (attributeListValue) {
1670
+ return factory.updateJsxAttribute(node, node.name, factory.createJsxExpression(undefined, descriptors.compileListValue(node.initializer.expression, { ...attributeListValue.value, keyedBlock: activeKeyedBlock.block.slot })))
1671
+ }
1672
+
1673
+ if (ts.isJsxExpression(node) && node.initializer === undefined && node.expression && (ts.isJsxElement(node.parent) || ts.isJsxFragment(node.parent))) {
1674
+ const renderedList = keyedEntry(preparedRenderedLists, node)
1675
+ const nestedList = keyedEntry(activeKeyedBlock?.analysis.nested ?? [], unwrapExpression(node.expression))
1676
+ if (renderedList || nestedList) return compileKeyedBlock(node, renderedList ?? nestedList)
1677
+ const conditional = conditionalParts(node.expression)
1678
+ if (conditional) {
1679
+ const compiled = compileRenderExpression(node.expression, node)
1680
+ if (compiled !== node.expression) return factory.updateJsxExpression(node, compiled)
1681
+ }
1682
+ const setters = settersForNode(node, settersByFunction)
1683
+ const expression = resolveReactiveJsxExpression(node.expression, nearestFunction(node), setters)
1684
+ const usedStates = referencedStateNames(expression, setters)
1685
+ const captures = captureNames(expression, expression, setters)
1686
+ if ((usedStates.size || captures.size) && !ts.isIdentifier(expression) && !containsJsx(expression)) {
1687
+ usesBehavior = true
1688
+ usesBinding = true
1689
+ return factory.updateJsxExpression(node, descriptors.compileReactiveBinding(expression, { setters, importBindings }))
1690
+ }
1691
+ }
1692
+
1693
+ if (ts.isJsxAttribute(node) && node.initializer && ts.isJsxExpression(node.initializer) && node.initializer.expression && !isContextProviderValue(node, contexts) && !/^on/i.test(node.name.text) && !["key", "ref", "dangerouslysetinnerhtml"].includes(node.name.text.toLowerCase())) {
1694
+ const sourceExpression = node.initializer.expression
1695
+ const setters = settersForNode(node, settersByFunction)
1696
+ const expression = resolveReactiveJsxExpression(sourceExpression, nearestFunction(node), setters)
1697
+ const usedStates = referencedStateNames(expression, setters)
1698
+ const captures = captureNames(expression, expression, setters)
1699
+ if ((usedStates.size || captures.size) && !ts.isIdentifier(expression)) {
1700
+ usesBehavior = true
1701
+ usesBinding = true
1702
+ const compiled = descriptors.compileReactiveBinding(expression, { setters, importBindings })
1703
+ return factory.updateJsxAttribute(node, node.name, factory.createJsxExpression(undefined, compiled))
1704
+ }
1705
+ }
1706
+
1707
+ if (ts.isJsxAttribute(node) && node.initializer && ts.isJsxExpression(node.initializer) && node.initializer.expression && /^on[A-Z]/.test(node.name.text)) {
1708
+ const setters = settersForNode(node, settersByFunction)
1709
+ const event = descriptors.compileEvent(node.initializer.expression, {
1710
+ owner: fallbackOwner(node),
1711
+ stateOwners: activeStateOwners ?? stateOwnersForNode(node),
1712
+ setters,
1713
+ reducers: reducersForNode(node, reducersByFunction),
1714
+ functions: functionsForNode(node),
1715
+ listItem: activeKeyedBlock ? { item: activeKeyedBlock.parts.item, index: activeKeyedBlock.parts.index } : undefined,
1716
+ keyedBlock: activeKeyedBlock?.block.slot,
1717
+ importBindings: new Map([...importBindings, ...packageBindings])
1718
+ })
1719
+ if (event) {
1720
+ usesBehavior = true
1721
+ return factory.updateJsxAttribute(node, node.name, factory.createJsxExpression(undefined, event))
1722
+ }
1723
+ if (ts.isIdentifier(node.initializer.expression) && isDestructuredParameter(node.initializer.expression, nearestFunction(node))) return node
1724
+ const position = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile))
1725
+ throw new Error(`${sourceFile.fileName}:${position.line + 1}:${position.character + 1} ${node.name.text} must reference a function`)
1726
+ }
1727
+
1728
+ return ts.visitEachChild(node, visitor, context)
1729
+ }
1730
+
1731
+ const transformed = ts.visitNode(sourceFile, visitor)
1732
+ descriptors.finalize()
1733
+ if (!usesBehavior) return transformed
1734
+
1735
+ const behaviorImports = [factory.createImportSpecifier(false, factory.createIdentifier("behavior"), factory.createIdentifier("__kBehavior"))]
1736
+ if (moduleIR.handlers.some(handler => handler.kind === "module-export" && handler.role === "native")) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("nativeBehavior"), factory.createIdentifier("__kNativeBehavior")))
1737
+ if (usesBinding) {
1738
+ behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("binding"), factory.createIdentifier("__kBinding")))
1739
+ behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("select"), factory.createIdentifier("__kSelect")))
1740
+ }
1741
+ if (usesConditional) {
1742
+ behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("conditional"), factory.createIdentifier("__kConditional")))
1743
+ behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("stateConditional"), factory.createIdentifier("__kStateConditional")))
1744
+ }
1745
+ if (usesList) {
1746
+ behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("list"), factory.createIdentifier("__kList")))
1747
+ behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("listExpression"), factory.createIdentifier("__kListExpression")))
1748
+ behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("listField"), factory.createIdentifier("__kListField")))
1749
+ behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("listItem"), factory.createIdentifier("__kListItem")))
1750
+ behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("listIndex"), factory.createIdentifier("__kListIndex")))
1751
+ behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("listConditional"), factory.createIdentifier("__kListConditional")))
1752
+ }
1753
+ if (usesListItem && !usesList) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("listItem"), factory.createIdentifier("__kListItem")))
1754
+ if (usesListEffects) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("useEffect"), factory.createIdentifier("__kListUseEffect")))
1755
+ if (usesRowState) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("useState"), factory.createIdentifier("__kRowUseState")))
1756
+ if (usesRowRef) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("useRef"), factory.createIdentifier("__kRowUseRef")))
1757
+ if (usesComponentState) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("useState"), factory.createIdentifier("__kComponentUseState")))
1758
+ if (usesComponentId) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("useId"), factory.createIdentifier("__kComponentUseId")))
1759
+ if (usesComponentRef) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("useRef"), factory.createIdentifier("__kComponentUseRef")))
1760
+ if (usesComponentEffects) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("useEffect"), factory.createIdentifier("__kComponentUseEffect")))
1761
+ if (usesBinding || usesConditional) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("bindingValue"), factory.createIdentifier("__kBindingValue")))
1762
+ const behaviorImport = factory.createImportDeclaration(
1763
+ undefined,
1764
+ factory.createImportClause(false, undefined, factory.createNamedImports(behaviorImports)),
1765
+ factory.createStringLiteral("@kudzujs/core")
1766
+ )
1767
+ return factory.updateSourceFile(transformed, [behaviorImport, ...transformed.statements])
1768
+ }
1769
+ }
1770
+
1771
+ function containsRenderControl(root, knownLocals) {
1772
+ let found = false
1773
+ const visit = node => {
1774
+ if (isFunctionLike(node) && node !== root) return
1775
+ if (ts.isReturnStatement(node) && node.expression && isJsxLocalValue(node.expression, knownLocals)) found = true
1776
+ if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken && containsJsx(node.right)) found = true
1777
+ if (!found) ts.forEachChild(node, visit)
1778
+ }
1779
+ visit(root)
1780
+ return found
1781
+ }
1782
+
1783
+ function keyedListParts(expression, setters, declarations, fail, aliases = new Set(), importedCollections = new Set(), factory = ts.factory, context, importedCollectionTransforms = new Map(), calculatedCollection, staticCollection) {
1784
+ const value = unwrapExpression(expression)
1785
+ const directFrom = isArrayFromCall(value) && value.arguments.length === 2 && containsJsx(value.arguments[1])
1786
+ if (!directFrom && (!ts.isCallExpression(value) || value.arguments.length !== 1 || !ts.isPropertyAccessExpression(value.expression) || value.expression.name.text !== "map")) return undefined
1787
+ let collection = analyzeCollectionPipeline(directFrom ? value.arguments[0] : value.expression.expression, {
1788
+ setters, declarations, fail, aliases, importedCollections, stateNames: new Set(setters.values()), importedCollectionTransforms, calculatedCollection, staticCollection
1789
+ })
1790
+ if (!collection?.state && !collection?.calculation) return undefined
1791
+ if (directFrom) collection.selector.push(["from", undefined])
1792
+ let callback = directFrom ? value.arguments[1] : value.arguments[0]
1793
+ const parameters = collectionParameters(callback, "Keyed list map", fail)
1794
+ let root = unwrapExpression(callback.body)
1795
+ if (ts.isBlock(root)) {
1796
+ if (!context || root.statements.length !== 2 || !ts.isVariableStatement(root.statements[0]) || (root.statements[0].declarationList.flags & ts.NodeFlags.Const) === 0 || root.statements[0].declarationList.declarations.length !== 1 || !ts.isReturnStatement(root.statements[1]) || !root.statements[1].expression) fail(root, "Block-bodied keyed list map callbacks require one computed child collection const and a final JSX return")
1797
+ const declaration = root.statements[0].declarationList.declarations[0]
1798
+ if (!ts.isIdentifier(declaration.name) || !declaration.initializer) fail(declaration, "Computed child collections must initialize one const identifier")
1799
+ const computed = analyzeCollectionPipeline(declaration.initializer, { fail, importedCollectionTransforms })
1800
+ if (!computed?.ownerField || computed.parentItem !== parameters.item) fail(declaration.initializer, `Computed child collections must start from ${parameters.item}.<field>`)
1801
+ const returned = root.statements[1].expression
1802
+ if (identifierReferenceCount(returned, declaration.name.text) !== 1) fail(declaration.name, `Computed child collection alias "${declaration.name.text}" must be used exactly once`)
1803
+ root = unwrapExpression(substituteClone(returned, new Map([[declaration.name.text, declaration.initializer]]), factory, context))
1804
+ callback = factory.updateArrowFunction(callback, callback.modifiers, callback.typeParameters, callback.parameters, callback.type, callback.equalsGreaterThanToken, root)
1805
+ ts.setParentRecursive(callback, false)
1806
+ callback.parent = value
1807
+ }
1808
+ const conditional = conditionalKeyedMapRoot(callback, root, parameters, collection, fail, new Set(setters.values()), factory, value)
1809
+ if (conditional) ({ callback, root, collection } = conditional)
1810
+ if (!ts.isJsxElement(root) && !ts.isJsxSelfClosingElement(root)) fail(callback.body, "Keyed list map callback must return one JSX element")
1811
+ const attributes = ts.isJsxElement(root) ? root.openingElement.attributes : root.attributes
1812
+ const key = attributes.properties.find(attribute => ts.isJsxAttribute(attribute) && ts.isIdentifier(attribute.name) && attribute.name.text === "key")
1813
+ const keyExpression = key && ts.isJsxAttribute(key) && key.initializer && ts.isJsxExpression(key.initializer) && key.initializer.expression
1814
+ const field = keyExpression && directProperty(keyExpression, parameters.item)
1815
+ const positional = Boolean(keyExpression && parameters.index && ts.isIdentifier(unwrapExpression(keyExpression)) && unwrapExpression(keyExpression).text === parameters.index)
1816
+ if (!field && !positional) fail(key ?? root, `Keyed list root must have key={${parameters.item}.<field>} or key={${parameters.index ?? "index"}}`)
1817
+ return { ...collection, static: collection.static && (!collection.localStatic || collection.selector.length > 0), callback, root, item: parameters.item, index: parameters.index, indexed: Boolean(parameters.index), keyField: positional ? null : field }
1818
+ }
1819
+
1820
+ function nestedKeyedListParts(expression, parentItem, fail) {
1821
+ const value = unwrapExpression(expression)
1822
+ if (!ts.isCallExpression(value) || value.arguments.length !== 1 || !ts.isPropertyAccessExpression(value.expression) || value.expression.name.text !== "map") return undefined
1823
+ let collection = analyzeCollectionPipeline(value.expression.expression, { fail })
1824
+ if (!collection?.ownerField || collection.parentItem !== parentItem) return undefined
1825
+ let callback = value.arguments[0]
1826
+ const parameters = collectionParameters(callback, "Nested keyed list map", fail)
1827
+ let root = unwrapExpression(callback.body)
1828
+ const conditional = conditionalKeyedMapRoot(callback, root, parameters, collection, fail, new Set(), ts.factory, value)
1829
+ if (conditional) ({ callback, root, collection } = conditional)
1830
+ if (!ts.isJsxElement(root) && !ts.isJsxSelfClosingElement(root)) fail(callback.body, "Nested keyed list map callback must return one JSX element")
1831
+ const attributes = ts.isJsxElement(root) ? root.openingElement.attributes : root.attributes
1832
+ const key = attributes.properties.find(attribute => ts.isJsxAttribute(attribute) && ts.isIdentifier(attribute.name) && attribute.name.text === "key")
1833
+ const keyExpression = key && ts.isJsxAttribute(key) && key.initializer && ts.isJsxExpression(key.initializer) && key.initializer.expression
1834
+ const keyField = keyExpression && directProperty(keyExpression, parameters.item)
1835
+ const positional = Boolean(keyExpression && parameters.index && ts.isIdentifier(unwrapExpression(keyExpression)) && unwrapExpression(keyExpression).text === parameters.index)
1836
+ if (!keyField && !positional) fail(key ?? root, `Nested keyed list root must have key={${parameters.item}.<field>} or key={${parameters.index ?? "index"}}`)
1837
+ return { ...collection, callback, root, item: parameters.item, index: parameters.index, indexed: Boolean(parameters.index), keyField: positional ? null : keyField }
1838
+ }
1839
+
1840
+ function conditionalKeyedMapRoot(callback, root, parameters, collection, fail, stateNames, factory, parent) {
1841
+ let condition
1842
+ let rendered
1843
+ if (ts.isBinaryExpression(root) && root.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken && (ts.isJsxElement(unwrapExpression(root.right)) || ts.isJsxSelfClosingElement(unwrapExpression(root.right)))) {
1844
+ condition = root.left
1845
+ rendered = unwrapExpression(root.right)
1846
+ } else if (ts.isConditionalExpression(root) && (ts.isJsxElement(unwrapExpression(root.whenTrue)) || ts.isJsxSelfClosingElement(unwrapExpression(root.whenTrue)))) {
1847
+ if (unwrapExpression(root.whenFalse).kind !== ts.SyntaxKind.NullKeyword) fail(root.whenFalse, "Conditional keyed map callbacks require condition ? <Element> : null")
1848
+ condition = root.condition
1849
+ rendered = unwrapExpression(root.whenTrue)
1850
+ } else {
1851
+ return undefined
1852
+ }
1853
+ if (parameters.index) fail(callback.parameters[1], "Conditional keyed map callbacks cannot use a map index because filtering changes index semantics; use an explicit filter(...).map((item, index) => ...) when a filtered index is intended")
1854
+ const selectorStates = new Set(collection.selectorStates)
1855
+ const selector = collectionExpression(condition, { parameters, fail, stateNames, selectorStates })
1856
+ const normalized = factory.updateArrowFunction(callback, callback.modifiers, callback.typeParameters, callback.parameters, callback.type, callback.equalsGreaterThanToken, rendered)
1857
+ ts.setParentRecursive(normalized, false)
1858
+ normalized.parent = parent
1859
+ return { callback: normalized, root: rendered, collection: { ...collection, selector: [...collection.selector, ["filter", selector]], selectorStates } }
1860
+ }
1861
+
1862
+ function jsonExpression(value, factory) {
1863
+ return factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("JSON"), "parse"), undefined, [factory.createStringLiteral(JSON.stringify(value))])
1864
+ }
1865
+
1866
+ function isStateBackedListComponentCall(call, component, setters) {
1867
+ if (component.parameters.length !== 1 || !ts.isObjectBindingPattern(component.parameters[0].name)) return false
1868
+ const attributes = ts.isJsxElement(call) ? call.openingElement.attributes : call.attributes
1869
+ const stateNames = new Set(setters.values())
1870
+ const mappedProps = new Set()
1871
+ for (const element of component.parameters[0].name.elements) {
1872
+ if (!ts.isIdentifier(element.name)) continue
1873
+ const prop = (element.propertyName ?? element.name).getText()
1874
+ const attribute = attributes.properties.find(entry => ts.isJsxAttribute(entry) && entry.name.getText() === prop)
1875
+ const value = attribute?.initializer && ts.isJsxExpression(attribute.initializer) ? unwrapExpression(attribute.initializer.expression) : undefined
1876
+ if (value && ts.isIdentifier(value) && stateNames.has(value.text)) mappedProps.add(element.name.text)
1877
+ }
1878
+ if (!mappedProps.size) return false
1879
+ const returned = ts.isBlock(component.body)
1880
+ ? [...component.body.statements].reverse().find(ts.isReturnStatement)?.expression
1881
+ : component.body
1882
+ if (!returned || !containsJsx(returned)) return false
1883
+ let found = false
1884
+ const visit = node => {
1885
+ if (found || node !== returned && isFunctionLike(node)) return
1886
+ if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "map" && ts.isIdentifier(node.expression.expression) && mappedProps.has(node.expression.expression.text)) {
1887
+ found = true
1888
+ return
1889
+ }
1890
+ ts.forEachChild(node, visit)
1891
+ }
1892
+ visit(returned)
1893
+ return found
1894
+ }
1895
+
1896
+ function jsxCallHasDirectStateProp(call, setters) {
1897
+ const attributes = ts.isJsxElement(call) ? call.openingElement.attributes : call.attributes
1898
+ const stateNames = new Set(setters.values())
1899
+ return attributes.properties.some(attribute => {
1900
+ const value = ts.isJsxAttribute(attribute) && attribute.initializer && ts.isJsxExpression(attribute.initializer) ? unwrapExpression(attribute.initializer.expression) : undefined
1901
+ return value && ts.isIdentifier(value) && stateNames.has(value.text)
1902
+ })
1903
+ }
1904
+
1905
+ function jsxSetterCallbackProps(call, setters, functions, reducers) {
1906
+ const attributes = ts.isJsxElement(call) ? call.openingElement.attributes : call.attributes
1907
+ return attributes.properties.flatMap(attribute => {
1908
+ if (!ts.isJsxAttribute(attribute) || !/^on[A-Z]/.test(attribute.name.text) || !attribute.initializer || !ts.isJsxExpression(attribute.initializer) || !attribute.initializer.expression) return []
1909
+ const value = unwrapExpression(attribute.initializer.expression)
1910
+ if (ts.isIdentifier(value) && setters.has(value.text)) return [attribute.name.text]
1911
+ const callback = ts.isArrowFunction(value) || ts.isFunctionExpression(value) ? value : ts.isIdentifier(value) ? functions.get(value.text) : undefined
1912
+ return callback && !nativeCaptureNames(callback, setters).size && !referencedReducerDispatches(callback.body, reducers, callback).size && referencedStateNames(callback.body, setters, callback).size ? [attribute.name.text] : []
1913
+ })
1914
+ }
1915
+
1916
+ function jsxCallHasDirectReducerProp(call, reducers) {
1917
+ const attributes = ts.isJsxElement(call) ? call.openingElement.attributes : call.attributes
1918
+ return attributes.properties.some(attribute => {
1919
+ const value = ts.isJsxAttribute(attribute) && attribute.initializer && ts.isJsxExpression(attribute.initializer) ? unwrapExpression(attribute.initializer.expression) : undefined
1920
+ return value && ts.isIdentifier(value) && reducers.has(value.text)
1921
+ })
1922
+ }
1923
+
1924
+ function jsxCallHasReducerCallbackProp(call, reducers) {
1925
+ const attributes = ts.isJsxElement(call) ? call.openingElement.attributes : call.attributes
1926
+ return attributes.properties.some(attribute => {
1927
+ const value = ts.isJsxAttribute(attribute) && attribute.initializer && ts.isJsxExpression(attribute.initializer) ? unwrapExpression(attribute.initializer.expression) : undefined
1928
+ return value && referencedReducerDispatches(value, reducers, value).size
1929
+ })
1930
+ }
1931
+
1932
+ function runtimeImportNames(sourceFile, relative) {
1933
+ const names = new Set()
1934
+ for (const statement of sourceFile.statements) {
1935
+ if (!ts.isImportDeclaration(statement) || !statement.importClause || statement.importClause.isTypeOnly || !ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text.startsWith(".") !== relative || isStaticImport(statement.moduleSpecifier.text)) continue
1936
+ const clause = statement.importClause
1937
+ if (clause.name) names.add(clause.name.text)
1938
+ if (clause.namedBindings && ts.isNamespaceImport(clause.namedBindings)) names.add(clause.namedBindings.name.text)
1939
+ if (clause.namedBindings && ts.isNamedImports(clause.namedBindings)) for (const entry of clause.namedBindings.elements) if (!entry.isTypeOnly) names.add(entry.name.text)
1940
+ }
1941
+ return names
1942
+ }
1943
+
1944
+ function insideJsxEventHandler(node, root) {
1945
+ for (let current = node.parent; current && current !== root.parent; current = current.parent) {
1946
+ if (ts.isJsxAttribute(current) && /^on[A-Z]/.test(current.name.text)) return true
1947
+ }
1948
+ return false
1949
+ }
1950
+
1951
+ function validateKeyedList(parts, sourceFile, setters, rowStates, componentSpecializations, expandedRowSpecializations, nestedRowSpecializations, factory, prepareListCallback) {
1952
+ const fail = (node, message) => {
1953
+ throw sourceNodeError(node, sourceFile, message)
1954
+ }
1955
+ const analysis = { values: [], conditions: [], nested: [] }
1956
+ const root = parts.root
1957
+ const item = parts.item
1958
+ const nestedDiagnostic = "Nested keyed list collections must be a direct property of the parent item"
1959
+ const validateElement = node => {
1960
+ const tag = ts.isJsxElement(node) ? node.openingElement.tagName : node.tagName
1961
+ if (!ts.isIdentifier(tag) || tag.text[0] !== tag.text[0].toLowerCase()) fail(node, "Keyed list items must use intrinsic JSX elements")
1962
+ }
1963
+ const visit = node => {
1964
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "useId") fail(node, "useId() is not supported in keyed rows")
1965
+ if (ts.isJsxFragment(node)) fail(node, "Fragments are not supported in keyed lists")
1966
+ if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) validateElement(node)
1967
+ if (node !== root && ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "map" && containsJsx(node)) fail(node, nestedDiagnostic)
1968
+ if (ts.isJsxSpreadAttribute(node) && referencesIdentifier(node.expression, item)) fail(node, "Keyed list item spreads are not supported")
1969
+ if (ts.isJsxAttribute(node) && /^on[A-Z]/.test(node.name.text)) {
1970
+ return
1971
+ }
1972
+ if (ts.isJsxExpression(node) && node.expression) {
1973
+ const expression = unwrapExpression(node.expression)
1974
+ if (containsJsx(expression) && ts.isCallExpression(expression) && ts.isPropertyAccessExpression(expression.expression) && expression.expression.name.text === "map") {
1975
+ const nested = nestedKeyedListParts(expression, item, fail)
1976
+ if (!nested) fail(expression, nestedDiagnostic)
1977
+ if (["__proto__", "constructor", "prototype"].includes(nested.ownerField)) fail(expression, `Nested keyed list owner property "${nested.ownerField}" is not supported`)
1978
+ if (referenceIdentifiers(nested.callback, item).length) fail(nested.root, "Nested keyed list rows cannot capture the parent item")
1979
+ const specialization = componentSpecializations.get(nested.root) ?? expandedRowSpecializations.get(nested.root) ?? nestedRowSpecializations.get(`${expression.pos}:${expression.end}`)
1980
+ const root = specialization?.root ?? nested.root
1981
+ let callback = root === nested.root ? nested.callback : factory.updateArrowFunction(
1982
+ nested.callback,
1983
+ nested.callback.modifiers,
1984
+ nested.callback.typeParameters,
1985
+ nested.callback.parameters,
1986
+ nested.callback.type,
1987
+ nested.callback.equalsGreaterThanToken,
1988
+ root
1989
+ )
1990
+ if (callback !== nested.callback) {
1991
+ ts.setParentRecursive(callback, false)
1992
+ callback.parent = nested.callback.parent
1993
+ }
1994
+ callback = prepareListCallback(callback, root, specialization ?? { hookDeclarations: [], effects: [] })
1995
+ const specializedStates = [...(specialization?.rowStates ?? []), ...(specialization?.ordinaryStates ?? [])]
1996
+ const nestedParts = {
1997
+ ...nested,
1998
+ root,
1999
+ callback,
2000
+ state: parts.state,
2001
+ nested: true,
2002
+ specializations: [specialization?.analysis?.slot, ...(specialization?.specializations ?? [])].filter(slot => slot !== undefined),
2003
+ rowStates: specializedStates,
2004
+ rowRefs: specialization?.rowRefs ?? [],
2005
+ analysisStateOwners: new Map([...(parts.analysisStateOwners ?? []), ...specializedStates.map(state => [state.state, state.analysisOwner])])
2006
+ }
2007
+ for (const calculation of specialization?.calculations ?? []) {
2008
+ ts.setParentRecursive(calculation, false)
2009
+ calculation.parent = callback
2010
+ validateListExpression(calculation, nested.item, nested.root, fail)
2011
+ }
2012
+ const nestedAnalysis = validateKeyedList(nestedParts, sourceFile, setters, specialization?.rowStates ?? [], componentSpecializations, expandedRowSpecializations, nestedRowSpecializations, factory, prepareListCallback)
2013
+ analysis.nested.push({ node: expression, parts: nestedParts, analysis: nestedAnalysis })
2014
+ return
2015
+ }
2016
+ const condition = conditionalParts(expression)
2017
+ if (condition && containsJsx(expression)) {
2018
+ if (rowStates.some(rowState => referencedStateNames(condition.condition, setters).has(rowState.state))) {
2019
+ visit(condition.truthy)
2020
+ visit(condition.falsy)
2021
+ return
2022
+ }
2023
+ if (!referencesIdentifier(condition.condition, item) && !(parts.index && referencesIdentifier(condition.condition, parts.index))) fail(node, "Keyed list item conditions must read the item or index")
2024
+ validateListExpression(condition.condition, item, node, fail, parts.index)
2025
+ analysis.conditions.push({ node: node.expression, value: { ...condition, item, index: parts.index } })
2026
+ visit(condition.truthy)
2027
+ visit(condition.falsy)
2028
+ return
2029
+ }
2030
+ const field = directProperty(expression, item)
2031
+ const isRootKey = ts.isJsxAttribute(node.parent) && node.parent.name.text === "key"
2032
+ if (field && ["__proto__", "constructor", "prototype"].includes(field)) fail(node, `Keyed list item property "${field}" is not supported`)
2033
+ if (field && ts.isJsxAttribute(node.parent) && ["ref", "dangerouslysetinnerhtml"].includes(node.parent.name.text.toLowerCase())) fail(node, `Keyed list item ${node.parent.name.text} is not supported`)
2034
+ if (isRootKey) return
2035
+ if (field) {
2036
+ analysis.values.push({ node: node.expression, value: { field } })
2037
+ return
2038
+ }
2039
+ if (referencesIdentifier(expression, item) || parts.index && referencesIdentifier(expression, parts.index)) {
2040
+ const states = referencedStateNames(expression, setters)
2041
+ for (const rowState of rowStates) states.delete(rowState.state)
2042
+ if (parts.nested && states.size) fail(node, "Nested keyed list item expressions cannot read parent state")
2043
+ validateListExpression(expression, item, node, fail, parts.index, states)
2044
+ if (ts.isJsxAttribute(node.parent) && ["ref", "dangerouslysetinnerhtml"].includes(node.parent.name.text.toLowerCase())) fail(node, `Keyed list item ${node.parent.name.text} is not supported`)
2045
+ analysis.values.push({ node: node.expression, value: { item, index: parts.index, states } })
2046
+ return
2047
+ }
2048
+ }
2049
+ ts.forEachChild(node, visit)
2050
+ }
2051
+ visit(root)
2052
+ return analysis
2053
+ }
2054
+
2055
+ function directConstObjectLiteral(expression, call) {
2056
+ expression = unwrapExpression(expression)
2057
+ if (ts.isObjectLiteralExpression(expression)) return expression
2058
+ if (!ts.isIdentifier(expression)) return
2059
+ const scopes = []
2060
+ for (let current = call.parent; current; current = current.parent) {
2061
+ if (isFunctionLike(current) && ts.isBlock(current.body)) scopes.push(current.body)
2062
+ if (ts.isSourceFile(current)) scopes.push(current)
2063
+ }
2064
+ for (const scope of scopes) {
2065
+ const declarations = []
2066
+ for (const statement of scope.statements) {
2067
+ if (!ts.isVariableStatement(statement)) continue
2068
+ for (const declaration of statement.declarationList.declarations) {
2069
+ if (ts.isIdentifier(declaration.name) && declaration.name.text === expression.text) declarations.push({ declaration, constant: (statement.declarationList.flags & ts.NodeFlags.Const) !== 0 })
2070
+ }
2071
+ }
2072
+ if (!declarations.length) continue
2073
+ if (declarations.length !== 1 || !declarations[0].constant || !declarations[0].declaration.initializer || declarations[0].declaration.end >= call.pos) return
2074
+ const initializer = unwrapExpression(declarations[0].declaration.initializer)
2075
+ if (ts.isObjectLiteralExpression(initializer)) return initializer
2076
+ return
2077
+ }
2078
+ }
2079
+
2080
+ function specializedSpreadEntries(expression, call, fail, label, seen = new Set()) {
2081
+ const object = directConstObjectLiteral(expression, call)
2082
+ if (!object) fail(expression, `${label} component prop spreads must use an inline object literal or one direct const object literal declared in the calling component`)
2083
+ if (seen.has(object)) fail(expression, `${label} component prop spreads cannot be circular`)
2084
+ seen.add(object)
2085
+ const entries = []
2086
+ for (const property of object.properties) {
2087
+ if (ts.isSpreadAssignment(property)) {
2088
+ entries.push(...specializedSpreadEntries(property.expression, call, fail, label, seen))
2089
+ continue
2090
+ }
2091
+ if (ts.isShorthandPropertyAssignment(property)) {
2092
+ entries.push([property.name.text, property.name, property])
2093
+ continue
2094
+ }
2095
+ if (!ts.isPropertyAssignment(property) || ts.isComputedPropertyName(property.name) || !ts.isIdentifier(property.name) && !ts.isStringLiteral(property.name) && !ts.isNumericLiteral(property.name)) {
2096
+ fail(property, `${label} component prop spreads must contain only direct properties`)
2097
+ }
2098
+ entries.push([property.name.text, property.initializer, property])
2099
+ }
2100
+ seen.delete(object)
2101
+ return entries
2102
+ }
2103
+
2104
+ function specializedCallChildren(call, factory) {
2105
+ if (!ts.isJsxElement(call)) return []
2106
+ return call.children.flatMap(child => {
2107
+ if (ts.isJsxText(child)) {
2108
+ const lines = child.text.split(/\r\n|\n|\r/)
2109
+ const text = lines.length === 1
2110
+ ? child.text
2111
+ : lines.map((line, index) => {
2112
+ let text = line.replace(/\t/g, " ")
2113
+ if (index) text = text.trimStart()
2114
+ if (index < lines.length - 1) text = text.trimEnd()
2115
+ return text
2116
+ }).filter(Boolean).join(" ")
2117
+ return text ? [factory.createStringLiteral(text)] : []
2118
+ }
2119
+ if (ts.isJsxExpression(child)) return child.expression ? [child.expression] : []
2120
+ return [child]
2121
+ })
2122
+ }
2123
+
2124
+ function flattenForwardedComponentChildren(root, factory, context) {
2125
+ const forwarded = expression => {
2126
+ const value = unwrapExpression(expression)
2127
+ if (ts.isJsxElement(value) || ts.isJsxSelfClosingElement(value)) return [value]
2128
+ if (ts.isJsxFragment(value)) return [...value.children]
2129
+ if (ts.isArrayLiteralExpression(value) && !value.elements.some(ts.isSpreadElement)) {
2130
+ return value.elements.flatMap(element => {
2131
+ if (ts.isJsxFragment(element)) return [...element.children]
2132
+ if (ts.isJsxElement(element) || ts.isJsxSelfClosingElement(element)) return [element]
2133
+ return [factory.createJsxExpression(undefined, element)]
2134
+ })
2135
+ }
2136
+ }
2137
+ const visit = node => {
2138
+ if (ts.isJsxElement(node)) {
2139
+ const children = node.children.flatMap(child => {
2140
+ const values = ts.isJsxExpression(child) && child.expression ? forwarded(child.expression) : undefined
2141
+ return (values ?? [child]).map(entry => ts.visitNode(entry, visit))
2142
+ })
2143
+ return factory.updateJsxElement(node, ts.visitNode(node.openingElement, visit), children, ts.visitNode(node.closingElement, visit))
2144
+ }
2145
+ return ts.visitEachChild(node, visit, context)
2146
+ }
2147
+ return ts.visitNode(root, visit)
2148
+ }
2149
+
2150
+ function expandSpecializedRest(root, returned, component, rest, entries, factory, context, fail, label) {
2151
+ const sourceRoot = unwrapExpression(returned)
2152
+ const sourceTag = jsxTagName(sourceRoot)
2153
+ if (!sourceTag || !ts.isIdentifier(sourceTag) || sourceTag.text[0] !== sourceTag.text[0].toLowerCase()) {
2154
+ fail(returned, `${label} component rest props must be forwarded exactly once to the direct intrinsic root`)
2155
+ }
2156
+ const sourceAttributes = ts.isJsxElement(sourceRoot) ? sourceRoot.openingElement.attributes : sourceRoot.attributes
2157
+ const spreads = sourceAttributes.properties.filter(attribute => ts.isJsxSpreadAttribute(attribute) && ts.isIdentifier(unwrapExpression(attribute.expression)) && unwrapExpression(attribute.expression).text === rest.name)
2158
+ const references = referenceIdentifiers(component.body, rest.name)
2159
+ if (spreads.length !== 1 || references.length !== 1 || unwrapExpression(spreads[0].expression) !== references[0]) {
2160
+ fail(rest.node, `${label} component rest props must be forwarded exactly once to the direct intrinsic root`)
2161
+ }
2162
+ for (const [name] of entries) {
2163
+ if (["__proto__", "constructor", "prototype"].includes(name)) fail(rest.node, `${label} component rest prop ${JSON.stringify(name)} is not supported`)
2164
+ if (name === "children") fail(rest.node, `${label} component rest props cannot forward children; destructure children explicitly`)
2165
+ }
2166
+ const attributes = ts.isJsxElement(root) ? root.openingElement.attributes : root.attributes
2167
+ const expanded = attributes.properties.flatMap(attribute => {
2168
+ if (!ts.isJsxSpreadAttribute(attribute) || !ts.isIdentifier(unwrapExpression(attribute.expression)) || unwrapExpression(attribute.expression).text !== rest.name) return [attribute]
2169
+ return entries.map(([name, value]) => factory.createJsxAttribute(factory.createIdentifier(name), factory.createJsxExpression(undefined, cloneAst(value, factory, context))))
2170
+ })
2171
+ const last = new Map()
2172
+ expanded.forEach((attribute, index) => {
2173
+ if (ts.isJsxAttribute(attribute)) last.set(attribute.name.text, index)
2174
+ })
2175
+ const properties = expanded.filter((attribute, index) => !ts.isJsxAttribute(attribute) || last.get(attribute.name.text) === index)
2176
+ if (ts.isJsxSelfClosingElement(root)) return factory.updateJsxSelfClosingElement(root, root.tagName, root.typeArguments, factory.updateJsxAttributes(attributes, properties))
2177
+ const opening = factory.updateJsxOpeningElement(root.openingElement, root.openingElement.tagName, root.openingElement.typeArguments, factory.updateJsxAttributes(attributes, properties))
2178
+ return factory.updateJsxElement(root, opening, root.children, root.closingElement)
2179
+ }
2180
+
2181
+ function specializeComponentCall(call, component, sourceFile, factory, context, fail, label = "Keyed list", allowComponentRoot = false, ordinaryHooks = false, ordinaryStateNames = new Set()) {
2182
+ if (component.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword) || component.asteriskToken) fail(component, `${label} components must be synchronous`)
2183
+ if (component.parameters.length !== 1 || !ts.isObjectBindingPattern(component.parameters[0].name)) fail(component, `${label} components must use one destructured props parameter`)
2184
+ const callAttributes = ts.isJsxElement(call) ? call.openingElement.attributes : call.attributes
2185
+ const props = new Map()
2186
+ const directProps = new Set()
2187
+ let key
2188
+ for (const attribute of callAttributes.properties) {
2189
+ if (ts.isJsxSpreadAttribute(attribute)) {
2190
+ for (const [name, value, property] of specializedSpreadEntries(attribute.expression, call, fail, label)) {
2191
+ if (["__proto__", "constructor", "prototype"].includes(name)) fail(property, `${label} component prop spread property ${JSON.stringify(name)} is not supported`)
2192
+ if (name === "key") fail(property, `${label} component prop spreads cannot declare key`)
2193
+ props.set(name, value)
2194
+ }
2195
+ continue
2196
+ }
2197
+ const name = attribute.name.text
2198
+ if (directProps.has(name) || name === "key" && key) fail(attribute, `Duplicate ${label.toLowerCase()} component prop "${name}"`)
2199
+ const value = !attribute.initializer
2200
+ ? factory.createTrue()
2201
+ : ts.isStringLiteral(attribute.initializer)
2202
+ ? factory.createStringLiteral(attribute.initializer.text)
2203
+ : ts.isJsxExpression(attribute.initializer) && attribute.initializer.expression
2204
+ ? attribute.initializer.expression
2205
+ : factory.createIdentifier("undefined")
2206
+ if (name === "key") key = attribute
2207
+ else {
2208
+ props.set(name, value)
2209
+ directProps.add(name)
2210
+ }
2211
+ }
2212
+ const children = specializedCallChildren(call, factory)
2213
+ if (children.length) {
2214
+ if (directProps.has("children")) fail(call, `Duplicate ${label.toLowerCase()} component prop "children"`)
2215
+ props.set("children", children.length === 1 ? children[0] : factory.createArrayLiteralExpression(children))
2216
+ }
2217
+ const substitutions = new Map()
2218
+ const acceptedProps = new Set()
2219
+ let rest
2220
+ const elements = component.parameters[0].name.elements
2221
+ for (const [index, element] of elements.entries()) {
2222
+ if (element.dotDotDotToken) {
2223
+ if (!ts.isIdentifier(element.name) || element.propertyName || element.initializer || index !== elements.length - 1) fail(element, `${label} component rest props must be one final identifier binding`)
2224
+ rest = { name: element.name.text, node: element }
2225
+ continue
2226
+ }
2227
+ if (!ts.isIdentifier(element.name)) fail(element, `${label} component props cannot use nested destructuring`)
2228
+ if (element.initializer && !isSerializableStateLiteral(element.initializer)) fail(element.initializer, `${label} component prop defaults must be directly serializable primitive, plain-object, or array literals`)
2229
+ const prop = (element.propertyName ?? element.name).text
2230
+ acceptedProps.add(prop)
2231
+ substitutions.set(element.name.text, props.has(prop) ? props.get(prop) : element.initializer ?? factory.createIdentifier("undefined"))
2232
+ }
2233
+ const restEntries = [...props].filter(([prop]) => !acceptedProps.has(prop))
2234
+ if (!rest) for (const [prop] of restEntries) fail(call, `Unknown ${label.toLowerCase()} component prop "${prop}"`)
2235
+ const propAnalysis = elements.map(element => ({
2236
+ name: (element.propertyName ?? element.name).getText(),
2237
+ local: element.name.getText(),
2238
+ provided: element.dotDotDotToken ? restEntries.length > 0 : props.has((element.propertyName ?? element.name).text),
2239
+ ...(element.dotDotDotToken ? { rest: true } : {}),
2240
+ ...(element.initializer ? { hasDefault: true, defaultApplied: !props.has((element.propertyName ?? element.name).text) } : {})
2241
+ }))
2242
+
2243
+ let returned
2244
+ const calculations = []
2245
+ const effectCalls = []
2246
+ const hookDeclarations = []
2247
+ const rowStates = []
2248
+ const rowRefs = []
2249
+ const ordinaryStates = []
2250
+ const ordinaryRefs = []
2251
+ const ordinaryIds = []
2252
+ if (!ts.isBlock(component.body)) {
2253
+ returned = component.body
2254
+ } else {
2255
+ const statements = [...component.body.statements]
2256
+ const last = statements.pop()
2257
+ if (!last || !ts.isReturnStatement(last) || !last.expression) fail(component.body, `${label} component must end with one JSX return`)
2258
+ for (const statement of statements) {
2259
+ if (ts.isExpressionStatement(statement) && ts.isCallExpression(statement.expression) && ts.isIdentifier(statement.expression.expression) && statement.expression.expression.text === "useEffect") {
2260
+ effectCalls.push(statement.expression)
2261
+ continue
2262
+ }
2263
+ if (!ts.isVariableStatement(statement) || (statement.declarationList.flags & ts.NodeFlags.Const) === 0 || statement.declarationList.declarations.length !== 1) fail(statement, `${label} component locals must be single const declarations`)
2264
+ const declaration = statement.declarationList.declarations[0]
2265
+ if (declaration.initializer && ts.isCallExpression(declaration.initializer) && ts.isIdentifier(declaration.initializer.expression) && declaration.initializer.expression.text === "useState") {
2266
+ const hookLabel = ordinaryHooks ? "Setter-callback component" : "Keyed row"
2267
+ const initialArgument = declaration.initializer.arguments[0]
2268
+ const propReceiver = ordinaryHooks && initialArgument && ts.isCallExpression(initialArgument) && initialArgument.arguments.length === 0 && !initialArgument.questionDotToken && ts.isPropertyAccessExpression(initialArgument.expression) && !initialArgument.expression.questionDotToken && initialArgument.expression.name.text === "toString" && ts.isIdentifier(initialArgument.expression.expression) ? initialArgument.expression.expression : undefined
2269
+ const substitutedProp = propReceiver ? substitutions.get(propReceiver.text) : undefined
2270
+ const propStringInitializer = substitutedProp && ts.isIdentifier(unwrapExpression(substitutedProp)) && ordinaryStateNames.has(unwrapExpression(substitutedProp).text)
2271
+ if (declaration.initializer.arguments.length !== 1 || !isSerializableStateLiteral(initialArgument) && !propStringInitializer) throw sourceNodeError(declaration.initializer, component.getSourceFile(), `${hookLabel} useState() must use one directly serializable primitive, plain object, or array initial value${ordinaryHooks ? " or direct primitive state prop.toString()" : ""}; other dynamic initializers are not supported`)
2272
+ if (!ts.isArrayBindingPattern(declaration.name) || declaration.name.elements.length !== 2 || declaration.name.elements.some(element => !element || !ts.isBindingElement(element) || !ts.isIdentifier(element.name) || element.initializer || element.dotDotDotToken)) throw sourceNodeError(declaration.name, component.getSourceFile(), `${hookLabel} useState() must use [state, setter] identifier destructuring`)
2273
+ const suffix = `${Math.max(0, call.pos)}_${ordinaryHooks ? ordinaryStates.length : rowStates.length}`
2274
+ const state = ordinaryHooks ? `__kComponentState${suffix}` : `__kRowState${suffix}`
2275
+ const setter = ordinaryHooks ? `__kComponentSetter${suffix}` : `__kRowSetter${suffix}`
2276
+ substitutions.set(declaration.name.elements[0].name.text, factory.createIdentifier(state))
2277
+ substitutions.set(declaration.name.elements[1].name.text, factory.createIdentifier(setter))
2278
+ const binding = factory.createArrayBindingPattern([
2279
+ factory.createBindingElement(undefined, undefined, factory.createIdentifier(state)),
2280
+ factory.createBindingElement(undefined, undefined, factory.createIdentifier(setter))
2281
+ ])
2282
+ const initialValue = propStringInitializer ? substituteClone(initialArgument, substitutions, factory, context) : cloneAst(initialArgument, factory, context)
2283
+ synthesizeTree(initialValue)
2284
+ const initializer = factory.createCallExpression(factory.createIdentifier(ordinaryHooks ? "__kComponentUseState" : "__kRowUseState"), undefined, [initialValue])
2285
+ hookDeclarations.push(factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(binding, undefined, undefined, initializer)], ts.NodeFlags.Const)))
2286
+ if (ordinaryHooks) ordinaryStates.push({ state, setter, source: declaration })
2287
+ else rowStates.push({ state, setter, source: declaration })
2288
+ continue
2289
+ }
2290
+ if (declaration.initializer && ts.isCallExpression(declaration.initializer) && ts.isIdentifier(declaration.initializer.expression) && declaration.initializer.expression.text === "useRef") {
2291
+ const hookLabel = ordinaryHooks ? "Setter-callback component" : "Keyed row"
2292
+ if (declaration.initializer.arguments.length !== 1 || declaration.initializer.arguments[0].kind !== ts.SyntaxKind.NullKeyword) throw sourceNodeError(declaration.initializer, component.getSourceFile(), `${hookLabel} useRef() must use the direct initial value null`)
2293
+ if (!ts.isIdentifier(declaration.name)) throw sourceNodeError(declaration.name, component.getSourceFile(), `${hookLabel} useRef() must be assigned to one identifier`)
2294
+ const refs = ordinaryHooks ? ordinaryRefs : rowRefs
2295
+ const name = `${ordinaryHooks ? "__kComponentRef" : "__kRowRef"}${Math.max(0, call.pos)}_${refs.length}`
2296
+ substitutions.set(declaration.name.text, factory.createIdentifier(name))
2297
+ const initializer = factory.createCallExpression(factory.createIdentifier(ordinaryHooks ? "__kComponentUseRef" : "__kRowUseRef"), declaration.initializer.typeArguments?.map(type => cloneAst(type, factory, context)), [factory.createNull()])
2298
+ hookDeclarations.push(factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(factory.createIdentifier(name), undefined, undefined, initializer)], ts.NodeFlags.Const)))
2299
+ refs.push({ name, source: declaration })
2300
+ continue
2301
+ }
2302
+ if (declaration.initializer && ts.isCallExpression(declaration.initializer) && ts.isIdentifier(declaration.initializer.expression) && declaration.initializer.expression.text === "useId") {
2303
+ if (!ordinaryHooks) throw sourceNodeError(declaration.initializer, component.getSourceFile(), "useId() is not supported in keyed row components")
2304
+ if (declaration.initializer.arguments.length || !ts.isIdentifier(declaration.name)) throw sourceNodeError(declaration.initializer, component.getSourceFile(), "Setter-callback component useId() must initialize one top-level const identifier without arguments")
2305
+ const name = `__kComponentId${Math.max(0, call.pos)}_${hookDeclarations.length}`
2306
+ substitutions.set(declaration.name.text, factory.createIdentifier(name))
2307
+ const initializer = factory.createCallExpression(factory.createIdentifier("__kComponentUseId"), undefined, [])
2308
+ hookDeclarations.push(factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(factory.createIdentifier(name), undefined, undefined, initializer)], ts.NodeFlags.Const)))
2309
+ ordinaryIds.push({ name, source: declaration })
2310
+ continue
2311
+ }
2312
+ if (!ts.isIdentifier(declaration.name) || !declaration.initializer) fail(declaration, `${label} component locals must be initialized identifiers`)
2313
+ const calculation = substituteClone(declaration.initializer, substitutions, factory, context)
2314
+ calculations.push({ name: declaration.name.text, expression: calculation })
2315
+ substitutions.set(declaration.name.text, calculation)
2316
+ }
2317
+ returned = last.expression
2318
+ }
2319
+ let unsupportedHook
2320
+ const findUnsupportedHook = node => {
2321
+ if (unsupportedHook) return
2322
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && ["useState", "useRef", "useId"].includes(node.expression.text)) unsupportedHook = node
2323
+ ts.forEachChild(node, findUnsupportedHook)
2324
+ }
2325
+ findUnsupportedHook(returned)
2326
+ for (const calculation of calculations) findUnsupportedHook(calculation.expression)
2327
+ if (unsupportedHook) throw sourceNodeError(unsupportedHook, component.getSourceFile(), `${ordinaryHooks ? "Setter-callback component" : "Keyed row"} ${unsupportedHook.expression.text}() must be one top-level const declaration`)
2328
+ let root = unwrapExpression(flattenForwardedComponentChildren(substituteClone(returned, substitutions, factory, context), factory, context))
2329
+ if (rest) root = expandSpecializedRest(root, returned, component, rest, restEntries, factory, context, fail, label)
2330
+ if (!ts.isJsxElement(root) && !ts.isJsxSelfClosingElement(root)) fail(returned, `${label} component must return one JSX element`)
2331
+ const tag = jsxTagName(root)
2332
+ if (!ts.isIdentifier(tag) || !allowComponentRoot && tag.text[0] !== tag.text[0].toLowerCase()) fail(returned, `${label} component must directly return an intrinsic JSX element`)
2333
+ const rootAttributes = ts.isJsxElement(root) ? root.openingElement.attributes : root.attributes
2334
+ if (rootAttributes.properties.some(attribute => ts.isJsxAttribute(attribute) && attribute.name.text === "key")) fail(root, `${label} component intrinsic root cannot declare key`)
2335
+ if (key) root = addJsxAttribute(root, cloneAst(key, factory, context), factory)
2336
+ ts.setParentRecursive(root, false)
2337
+ root.parent = call.parent
2338
+ const effects = effectCalls.map(source => ({ source, call: substituteClone(source, substitutions, factory, context) }))
2339
+ return {
2340
+ root,
2341
+ calculations: calculations
2342
+ .filter(calculation => label !== "Reducer-dispatch" || !isFunctionLike(calculation.expression) || !isEventOnlyComponentLocal(returned, calculation.name))
2343
+ .map(calculation => calculation.expression),
2344
+ effects,
2345
+ hookDeclarations,
2346
+ rowStates,
2347
+ rowRefs,
2348
+ ordinaryStates,
2349
+ ordinaryRefs,
2350
+ ordinaryIds,
2351
+ propExpressions: props,
2352
+ props: propAnalysis,
2353
+ usesComponentId: ordinaryIds.length > 0
2354
+ }
2355
+ }
2356
+
2357
+ function isSerializableStateLiteral(node) {
2358
+ const value = unwrapExpression(node)
2359
+ if (isPrimitiveDefaultLiteral(value)) return true
2360
+ if (ts.isArrayLiteralExpression(value)) return value.elements.every(element => !ts.isSpreadElement(element) && !ts.isOmittedExpression(element) && isSerializableStateLiteral(element))
2361
+ if (!ts.isObjectLiteralExpression(value)) return false
2362
+ return value.properties.every(property => ts.isPropertyAssignment(property) && !ts.isComputedPropertyName(property.name) && property.name.text !== "__proto__" && isSerializableStateLiteral(property.initializer))
2363
+ }
2364
+
2365
+ function synthesizeSerializableStateLiteral(node, factory) {
2366
+ node = unwrapExpression(node)
2367
+ if (ts.isStringLiteral(node)) return factory.createStringLiteral(node.text)
2368
+ if (ts.isNumericLiteral(node)) return factory.createNumericLiteral(node.text)
2369
+ if (node.kind === ts.SyntaxKind.TrueKeyword) return factory.createTrue()
2370
+ if (node.kind === ts.SyntaxKind.FalseKeyword) return factory.createFalse()
2371
+ if (node.kind === ts.SyntaxKind.NullKeyword) return factory.createNull()
2372
+ if (ts.isPrefixUnaryExpression(node)) return factory.createPrefixUnaryExpression(node.operator, synthesizeSerializableStateLiteral(node.operand, factory))
2373
+ if (ts.isArrayLiteralExpression(node)) return factory.createArrayLiteralExpression(node.elements.map(element => synthesizeSerializableStateLiteral(element, factory)))
2374
+ return factory.createObjectLiteralExpression(node.properties.map(property => {
2375
+ const name = ts.isIdentifier(property.name) ? factory.createIdentifier(property.name.text) : ts.isNumericLiteral(property.name) ? factory.createNumericLiteral(property.name.text) : factory.createStringLiteral(property.name.text)
2376
+ return factory.createPropertyAssignment(name, synthesizeSerializableStateLiteral(property.initializer, factory))
2377
+ }))
2378
+ }
2379
+
2380
+ function isPrimitiveDefaultLiteral(node) {
2381
+ return ts.isStringLiteral(node) || ts.isNumericLiteral(node) ||
2382
+ (ts.isPrefixUnaryExpression(node) && (node.operator === ts.SyntaxKind.PlusToken || node.operator === ts.SyntaxKind.MinusToken) && ts.isNumericLiteral(node.operand)) ||
2383
+ node.kind === ts.SyntaxKind.TrueKeyword || node.kind === ts.SyntaxKind.FalseKeyword || node.kind === ts.SyntaxKind.NullKeyword
2384
+ }
2385
+
2386
+ function isEventOnlyComponentLocal(root, name) {
2387
+ let found = false
2388
+ let eventOnly = true
2389
+ const visit = node => {
2390
+ if (ts.isIdentifier(node) && node.text === name && isReferenceIdentifier(node)) {
2391
+ found = true
2392
+ let parent = node.parent
2393
+ while (parent && parent !== root) {
2394
+ if (ts.isJsxAttribute(parent)) {
2395
+ if (!/^on[A-Z]/.test(parent.name.text)) eventOnly = false
2396
+ return
2397
+ }
2398
+ parent = parent.parent
2399
+ }
2400
+ eventOnly = false
2401
+ return
2402
+ }
2403
+ ts.forEachChild(node, visit)
2404
+ }
2405
+ visit(root)
2406
+ return found && eventOnly
2407
+ }
2408
+
2409
+ function substituteClone(root, substitutions, factory, context) {
2410
+ const visit = (node, shadowed = new Set()) => {
2411
+ if (ts.isTypeNode(node)) return cloneAst(node, factory, context)
2412
+ if (ts.isShorthandPropertyAssignment(node) && substitutions.has(node.name.text) && !shadowed.has(node.name.text)) {
2413
+ return factory.createPropertyAssignment(cloneAst(node.name, factory, context), cloneAst(substitutions.get(node.name.text), factory, context))
2414
+ }
2415
+ if (ts.isIdentifier(node) && substitutions.has(node.text) && !shadowed.has(node.text) && isReferenceIdentifier(node) && !isJsxSyntaxIdentifier(node)) {
2416
+ return cloneAst(substitutions.get(node.text), factory, context)
2417
+ }
2418
+ const nextShadowed = isFunctionLike(node)
2419
+ ? new Set([...shadowed, ...node.parameters.flatMap(parameter => bindingNames(parameter.name))])
2420
+ : shadowed
2421
+ const clone = factory.cloneNode(node)
2422
+ ts.setTextRange(clone, node)
2423
+ ts.setOriginalNode(clone, node)
2424
+ return ts.visitEachChild(clone, child => visit(child, nextShadowed), context)
2425
+ }
2426
+ return visit(root)
2427
+ }
2428
+
2429
+ function replaceSpecializedCalls(root, replacements, context) {
2430
+ const visit = node => replacements.get(node) ?? ts.visitEachChild(node, visit, context)
2431
+ return ts.visitNode(root, visit)
2432
+ }
2433
+
2434
+ function cloneAst(root, factory, context) {
2435
+ const visit = node => {
2436
+ const clone = factory.cloneNode(node)
2437
+ ts.setTextRange(clone, node)
2438
+ ts.setOriginalNode(clone, node)
2439
+ return ts.visitEachChild(clone, visit, context)
2440
+ }
2441
+ return visit(root)
2442
+ }
2443
+
2444
+ function synthesizeTree(root) {
2445
+ const visit = node => {
2446
+ ts.setTextRange(node, { pos: -1, end: -1 })
2447
+ ts.setOriginalNode(node, undefined)
2448
+ ts.forEachChild(node, visit)
2449
+ }
2450
+ visit(root)
2451
+ return root
2452
+ }
2453
+
2454
+ function addJsxAttribute(root, attribute, factory) {
2455
+ if (ts.isJsxSelfClosingElement(root)) {
2456
+ return factory.updateJsxSelfClosingElement(root, root.tagName, root.typeArguments, factory.updateJsxAttributes(root.attributes, [attribute, ...root.attributes.properties]))
2457
+ }
2458
+ const opening = factory.updateJsxOpeningElement(root.openingElement, root.openingElement.tagName, root.openingElement.typeArguments, factory.updateJsxAttributes(root.openingElement.attributes, [attribute, ...root.openingElement.attributes.properties]))
2459
+ return factory.updateJsxElement(root, opening, root.children, root.closingElement)
2460
+ }
2461
+
2462
+ function jsxTagName(node) {
2463
+ return ts.isJsxElement(node) ? node.openingElement.tagName : ts.isJsxSelfClosingElement(node) ? node.tagName : undefined
2464
+ }
2465
+
2466
+ function isStylesheetLink(node) {
2467
+ const element = ts.isJsxElement(node) ? node.openingElement : node
2468
+ if (!ts.isIdentifier(element.tagName) || element.tagName.text.toLowerCase() !== "link") return false
2469
+ const attribute = element.attributes.properties.find(property => ts.isJsxAttribute(property) && property.name.getText().toLowerCase() === "rel")
2470
+ if (!attribute?.initializer) return false
2471
+ const value = ts.isStringLiteral(attribute.initializer)
2472
+ ? attribute.initializer.text
2473
+ : ts.isJsxExpression(attribute.initializer) && attribute.initializer.expression && (ts.isStringLiteral(attribute.initializer.expression) || ts.isNoSubstitutionTemplateLiteral(attribute.initializer.expression))
2474
+ ? attribute.initializer.expression.text
2475
+ : undefined
2476
+ return value?.toLowerCase().split(/\s+/).includes("stylesheet") ?? false
2477
+ }
2478
+
2479
+ function isContextProviderValue(node, contexts) {
2480
+ if (node.name.text !== "value") return false
2481
+ const element = node.parent?.parent
2482
+ const tag = ts.isJsxOpeningElement(element) || ts.isJsxSelfClosingElement(element) ? element.tagName : undefined
2483
+ return ts.isPropertyAccessExpression(tag) && tag.name.text === "Provider" && ts.isIdentifier(tag.expression) && contexts.has(tag.expression.text)
2484
+ }
2485
+
2486
+ function isJsxSyntaxIdentifier(node) {
2487
+ const parent = node.parent
2488
+ return (ts.isJsxOpeningElement(parent) || ts.isJsxClosingElement(parent) || ts.isJsxSelfClosingElement(parent)) && parent.tagName === node || ts.isJsxAttribute(parent) && parent.name === node
2489
+ }
2490
+
2491
+ function isDestructuredParameter(identifier, fn) {
2492
+ return fn?.parameters.some(parameter => ts.isObjectBindingPattern(parameter.name) && parameter.name.elements.some(element => ts.isIdentifier(element.name) && element.name.text === identifier.text)) ?? false
2493
+ }
2494
+
2495
+ function isExportedDeclaration(node) {
2496
+ const statement = ts.isVariableDeclaration(node) ? node.parent?.parent : node
2497
+ return statement?.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword || modifier.kind === ts.SyntaxKind.DefaultKeyword) ?? false
2498
+ }
2499
+
2500
+ function jsxTagUses(root, name) {
2501
+ const uses = []
2502
+ const visit = node => {
2503
+ const tag = ts.isJsxElement(node) ? node.openingElement.tagName : ts.isJsxSelfClosingElement(node) ? node.tagName : undefined
2504
+ if (tag && ts.isIdentifier(tag) && tag.text === name) uses.push(node)
2505
+ ts.forEachChild(node, visit)
2506
+ }
2507
+ visit(root)
2508
+ return uses
2509
+ }
2510
+
2511
+ const pureListGlobals = new Set(["Boolean", "Infinity", "Math", "NaN", "Number", "String", "undefined"])
2512
+ const assignmentOperators = new Set([
2513
+ ts.SyntaxKind.EqualsToken, ts.SyntaxKind.PlusEqualsToken, ts.SyntaxKind.MinusEqualsToken,
2514
+ ts.SyntaxKind.AsteriskEqualsToken, ts.SyntaxKind.AsteriskAsteriskEqualsToken, ts.SyntaxKind.SlashEqualsToken,
2515
+ ts.SyntaxKind.PercentEqualsToken, ts.SyntaxKind.LessThanLessThanEqualsToken, ts.SyntaxKind.GreaterThanGreaterThanEqualsToken,
2516
+ ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken, ts.SyntaxKind.AmpersandEqualsToken, ts.SyntaxKind.BarEqualsToken,
2517
+ ts.SyntaxKind.CaretEqualsToken, ts.SyntaxKind.BarBarEqualsToken, ts.SyntaxKind.AmpersandAmpersandEqualsToken,
2518
+ ts.SyntaxKind.QuestionQuestionEqualsToken
2519
+ ])
2520
+
2521
+ function validateListExpression(expression, item, source, fail, index, states = new Set()) {
2522
+ const visit = node => {
2523
+ if (ts.isTypeNode(node)) return
2524
+ if (ts.isElementAccessExpression(node) && referencesIdentifier(node.expression, item)) {
2525
+ const key = node.argumentExpression
2526
+ if (!ts.isStringLiteral(key) && !ts.isNumericLiteral(key)) fail(source, "Derived keyed list item computed properties require a direct string or numeric literal key")
2527
+ if (ts.isStringLiteral(key) && ["__proto__", "constructor", "prototype"].includes(key.text)) fail(source, `Derived keyed list item property "${key.text}" is not supported`)
2528
+ }
2529
+ if (ts.isPropertyAccessExpression(node) && ["__proto__", "constructor", "prototype"].includes(node.name.text) || ts.isElementAccessExpression(node) && ts.isStringLiteral(node.argumentExpression) && ["__proto__", "constructor", "prototype"].includes(node.argumentExpression.text)) {
2530
+ fail(source, "Derived keyed list item expressions cannot read __proto__, prototype, or constructor")
2531
+ }
2532
+ if (ts.isBinaryExpression(node) && assignmentOperators.has(node.operatorToken.kind) || ts.isPostfixUnaryExpression(node) || ts.isPrefixUnaryExpression(node) && [ts.SyntaxKind.PlusPlusToken, ts.SyntaxKind.MinusMinusToken].includes(node.operator)) {
2533
+ fail(source, "Derived keyed list item expressions must be pure; assignments and updates are not supported")
2534
+ }
2535
+ if (ts.isDeleteExpression(node) || ts.isAwaitExpression(node) || ts.isNewExpression(node) || ts.isYieldExpression(node)) {
2536
+ fail(source, "Derived keyed list item expressions must be synchronous and side-effect free; delete, await, yield, and new are not supported")
2537
+ }
2538
+ if (ts.isArrowFunction(node) || ts.isFunctionExpression(node) || ts.isTaggedTemplateExpression(node)) {
2539
+ fail(source, "Derived keyed list item expressions cannot create or invoke arbitrary functions")
2540
+ }
2541
+ if (ts.isCallExpression(node)) {
2542
+ if (ts.isPropertyAccessExpression(node.expression)) {
2543
+ const method = node.expression.name.text
2544
+ if (mutatingListMethods.has(method)) fail(source, `Derived keyed list item expressions cannot call mutating method "${method}"`)
2545
+ const receiver = node.expression.expression
2546
+ const mathCall = ts.isIdentifier(receiver) && receiver.text === "Math" && pureMathMethods.has(method)
2547
+ if (!mathCall && !pureListMethods.has(method)) fail(source, `Derived keyed list item expressions cannot call arbitrary method "${method}"`)
2548
+ } else if (!ts.isIdentifier(node.expression) || !["Boolean", "Number", "String"].includes(node.expression.text)) {
2549
+ fail(source, "Derived keyed list item expressions cannot call arbitrary functions")
2550
+ }
2551
+ }
2552
+ if (ts.isIdentifier(node) && isReferenceIdentifier(node) && !isJsxSyntaxIdentifier(node) && node.text !== item && node.text !== index && !states.has(node.text) && !pureListGlobals.has(node.text)) {
2553
+ fail(source, `Derived keyed list item expression identifier "${node.text}" is not allowed`)
2554
+ }
2555
+ ts.forEachChild(node, visit)
2556
+ }
2557
+ visit(expression)
2558
+ }
2559
+
2560
+ function directProperty(expression, objectName) {
2561
+ const value = unwrapExpression(expression)
2562
+ if (!ts.isPropertyAccessExpression(value) || !ts.isIdentifier(value.expression)) return undefined
2563
+ if (objectName !== undefined && value.expression.text !== objectName) return undefined
2564
+ return value.name.text
2565
+ }
2566
+
2567
+ function keyedListParentTag(node) {
2568
+ for (let current = node.parent; current; current = current.parent) {
2569
+ if (ts.isJsxElement(current)) return current.openingElement.tagName.getText().toLowerCase()
2570
+ }
2571
+ return undefined
2572
+ }
2573
+
2574
+ function identifierReferenceCount(root, name) {
2575
+ return identifierReferences(root, name).length
2576
+ }
2577
+
2578
+ function identifierReferences(root, name) {
2579
+ const references = []
2580
+ const visit = node => {
2581
+ if (ts.isIdentifier(node) && node.text === name && isReferenceIdentifier(node) && !ts.isJsxClosingElement(node.parent)) references.push(node)
2582
+ ts.forEachChild(node, visit)
2583
+ }
2584
+ visit(root)
2585
+ return references
2586
+ }
2587
+
2588
+ function isJsxLocalValue(expression, known) {
2589
+ const value = unwrapExpression(expression)
2590
+ if (ts.isJsxElement(value) || ts.isJsxSelfClosingElement(value) || ts.isJsxFragment(value)) return true
2591
+ if (ts.isIdentifier(value)) return known.has(value.text)
2592
+ const parts = conditionalParts(value)
2593
+ return Boolean(parts && (isJsxLocalValue(parts.truthy, known) || isJsxLocalValue(parts.falsy, known)))
2594
+ }
2595
+
2596
+ function conditionalParts(expression) {
2597
+ const unwrap = node => ts.isParenthesizedExpression(node) ? unwrap(node.expression) : node
2598
+ const value = unwrap(expression)
2599
+ if (ts.isBinaryExpression(value) && value.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
2600
+ return { kind: "and", condition: value.left, truthy: unwrap(value.right), falsy: factoryNull() }
2601
+ }
2602
+ if (ts.isConditionalExpression(value)) {
2603
+ return { kind: "ternary", condition: value.condition, truthy: unwrap(value.whenTrue), falsy: unwrap(value.whenFalse) }
2604
+ }
2605
+ return undefined
2606
+ }
2607
+
2608
+ function factoryNull() {
2609
+ return ts.factory.createNull()
2610
+ }
2611
+
2612
+ function settersForNode(node, settersByFunction) {
2613
+ for (let current = node.parent; current; current = current.parent) {
2614
+ if (!ts.isFunctionDeclaration(current) && !ts.isFunctionExpression(current) && !ts.isArrowFunction(current)) continue
2615
+ const setters = settersByFunction.get(current)
2616
+ if (setters) return setters
2617
+ }
2618
+ return new Map()
2619
+ }
2620
+
2621
+ function reducersForNode(node, reducersByFunction) {
2622
+ for (let current = node.parent; current; current = current.parent) {
2623
+ if (!ts.isFunctionDeclaration(current) && !ts.isFunctionExpression(current) && !ts.isArrowFunction(current)) continue
2624
+ const reducers = reducersByFunction.get(current)
2625
+ if (reducers) return reducers
2626
+ }
2627
+ return new Map()
2628
+ }
2629
+
2630
+ function clientImportBindings(sourceFile, file, sourceFiles) {
2631
+ const bindings = new Map()
2632
+ for (const node of sourceFile.statements) {
2633
+ if (!ts.isImportDeclaration(node) || !node.importClause || node.importClause.isTypeOnly || !ts.isStringLiteral(node.moduleSpecifier) || !node.moduleSpecifier.text.startsWith(".") || isStaticImport(node.moduleSpecifier.text)) continue
2634
+ let target
2635
+ try {
2636
+ target = resolveSourceImport(file, node.moduleSpecifier.text, sourceFiles)
2637
+ } catch (error) {
2638
+ throw sourceNodeError(node.moduleSpecifier, sourceFile, error.message)
2639
+ }
2640
+ if (node.importClause.name) bindings.set(node.importClause.name.text, { kind: "default", local: node.importClause.name.text, target })
2641
+ const named = node.importClause.namedBindings
2642
+ if (named && ts.isNamespaceImport(named)) bindings.set(named.name.text, { kind: "namespace", local: named.name.text, target })
2643
+ if (named && ts.isNamedImports(named)) {
2644
+ for (const entry of named.elements) {
2645
+ if (!entry.isTypeOnly) bindings.set(entry.name.text, { kind: "named", imported: (entry.propertyName ?? entry.name).text, local: entry.name.text, target })
2646
+ }
2647
+ }
2648
+ }
2649
+ return bindings
2650
+ }
2651
+
2652
+ function hasFrameworkImport(sourceFile, name) {
2653
+ return sourceFile.statements.some(node => {
2654
+ if (!ts.isImportDeclaration(node) || node.importClause?.isTypeOnly || !ts.isStringLiteral(node.moduleSpecifier) || !["react", "@kudzujs/core"].includes(node.moduleSpecifier.text)) return false
2655
+ const bindings = node.importClause?.namedBindings
2656
+ return bindings && ts.isNamedImports(bindings) && bindings.elements.some(entry => !entry.isTypeOnly && entry.name.text === name && (entry.propertyName ?? entry.name).text === name)
2657
+ })
2658
+ }
2659
+
2660
+ function packageImportBindings(sourceFile) {
2661
+ const bindings = new Map()
2662
+ const rejectDynamic = node => {
2663
+ if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
2664
+ const specifier = node.arguments.length === 1 && ts.isStringLiteralLike(node.arguments[0]) ? node.arguments[0].text : null
2665
+ if (specifier === null) throw sourceNodeError(node, sourceFile, "Dynamic import specifiers are not supported")
2666
+ if (!specifier.startsWith(".")) throw sourceNodeError(node, sourceFile, `Dynamic package import ${JSON.stringify(specifier)} is not supported`)
2667
+ }
2668
+ ts.forEachChild(node, rejectDynamic)
2669
+ }
2670
+ rejectDynamic(sourceFile)
2671
+ for (const node of sourceFile.statements) {
2672
+ if (!ts.isImportDeclaration(node) || !ts.isStringLiteral(node.moduleSpecifier)) continue
2673
+ const target = node.moduleSpecifier.text
2674
+ if (!node.importClause) {
2675
+ if (!target.startsWith(".") && !["react", "react-router-dom", "@kudzujs/core"].includes(target) && !target.startsWith("@kudzujs/core/")) throw sourceNodeError(node, sourceFile, `Side-effect package import ${JSON.stringify(target)} is not supported`)
2676
+ continue
2677
+ }
2678
+ if (node.importClause.isTypeOnly) continue
2679
+ if (target.startsWith(".") || target.startsWith("node:") || target === "react" || target === "react-router-dom" || target === "@kudzujs/core" || target.startsWith("@kudzujs/core/")) continue
2680
+ if (node.importClause.name) bindings.set(node.importClause.name.text, { kind: "default", local: node.importClause.name.text, target, package: true })
2681
+ const named = node.importClause.namedBindings
2682
+ if (named && ts.isNamespaceImport(named)) bindings.set(named.name.text, { kind: "namespace", local: named.name.text, target, package: true })
2683
+ if (named && ts.isNamedImports(named)) for (const entry of named.elements) if (!entry.isTypeOnly) bindings.set(entry.name.text, { kind: "named", imported: (entry.propertyName ?? entry.name).text, local: entry.name.text, target, package: true })
2684
+ }
2685
+ return bindings
2686
+ }
2687
+
2688
+ function importedSerializableCollectionNames(sourceFile, file, sourceFiles, sourceIndex) {
2689
+ return new Set(importedSerializableCollections(sourceFile, file, sourceFiles, sourceIndex).keys())
2690
+ }
2691
+
2692
+ function importedSerializableCollections(sourceFile, file, sourceFiles, sourceIndex) {
2693
+ const collections = new Map()
2694
+ for (const [name, binding] of clientImportBindings(sourceFile, file, sourceFiles)) {
2695
+ if (binding.kind !== "named") continue
2696
+ const imported = parseSourceFile(binding.target, sourceIndex.get(binding.target))
2697
+ for (const statement of imported.statements) {
2698
+ if (!ts.isVariableStatement(statement) || !(statement.declarationList.flags & ts.NodeFlags.Const) || !statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword)) continue
2699
+ const declaration = statement.declarationList.declarations.find(entry => ts.isIdentifier(entry.name) && entry.name.text === binding.imported)
2700
+ if (declaration?.initializer && ts.isArrayLiteralExpression(unwrapExpression(declaration.initializer)) && isSerializableStateLiteral(declaration.initializer)) collections.set(name, unwrapExpression(declaration.initializer))
2701
+ }
2702
+ }
2703
+ return collections
2704
+ }
2705
+
2706
+ function normalizeImportedStaticCollections(sourceFile, collections, factory, context) {
2707
+ if (!collections.size) return sourceFile
2708
+ const visitor = node => {
2709
+ if (ts.isPropertyAccessExpression(node) && node.name.text === "map" && ts.isIdentifier(node.expression) && collections.has(node.expression.text) && !isShadowedIdentifier(node.expression, sourceFile)) {
2710
+ return factory.updatePropertyAccessExpression(node, synthesizeTree(cloneAst(collections.get(node.expression.text), factory, context)), node.name)
2711
+ }
2712
+ return ts.visitEachChild(node, visitor, context)
2713
+ }
2714
+ return ts.visitNode(sourceFile, visitor)
2715
+ }
2716
+
2717
+ function resolveComponentExport(file, exportName, getSource, sourceFiles, trail = []) {
2718
+ const key = `${file}:${exportName}`
2719
+ if (trail.includes(key)) throw new Error(`Imported keyed list component re-export cycle: ${[...trail, key].map(entry => relative(root, entry.slice(0, entry.lastIndexOf(":")))).join(" -> ")}`)
2720
+ const sourceFile = getSource(file)
2721
+ const nextTrail = [...trail, key]
2722
+
2723
+ for (const statement of sourceFile.statements) {
2724
+ if (ts.isFunctionDeclaration(statement)) {
2725
+ const isDefault = statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.DefaultKeyword)
2726
+ const isExported = statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword)
2727
+ if (exportName === "default" && isDefault || exportName !== "default" && isExported && statement.name?.text === exportName) return statement
2728
+ }
2729
+ if (ts.isVariableStatement(statement) && statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword) && exportName !== "default") {
2730
+ const declaration = statement.declarationList.declarations.find(entry => ts.isIdentifier(entry.name) && entry.name.text === exportName)
2731
+ if (declaration?.initializer && (ts.isArrowFunction(declaration.initializer) || ts.isFunctionExpression(declaration.initializer))) return declaration.initializer
2732
+ }
2733
+ if (exportName === "default" && ts.isExportAssignment(statement) && !statement.isExportEquals && ts.isIdentifier(statement.expression)) {
2734
+ const component = localComponentDeclaration(sourceFile, statement.expression.text)
2735
+ if (component) return component
2736
+ }
2737
+ if (ts.isExportDeclaration(statement) && ts.isNamedExports(statement.exportClause)) {
2738
+ const entry = statement.exportClause.elements.find(element => !element.isTypeOnly && element.name.text === exportName)
2739
+ if (!entry) continue
2740
+ const imported = (entry.propertyName ?? entry.name).text
2741
+ if (statement.moduleSpecifier && ts.isStringLiteral(statement.moduleSpecifier)) {
2742
+ if (!statement.moduleSpecifier.text.startsWith(".")) throw sourceNodeError(statement, sourceFile, "Imported keyed list components must use relative TypeScript re-exports")
2743
+ const target = resolveSourceImport(file, statement.moduleSpecifier.text, sourceFiles)
2744
+ return resolveComponentExport(target, imported, getSource, sourceFiles, nextTrail)
2745
+ }
2746
+ const component = localComponentDeclaration(sourceFile, imported)
2747
+ if (component) return component
2748
+ }
2749
+ }
2750
+ throw new Error(`${relative(root, file)} does not export a statically analyzable keyed list component named ${JSON.stringify(exportName)}`)
2751
+ }
2752
+
2753
+ function localComponentDeclaration(sourceFile, name) {
2754
+ for (const statement of sourceFile.statements) {
2755
+ if (ts.isFunctionDeclaration(statement) && statement.name?.text === name) return statement
2756
+ if (ts.isVariableStatement(statement)) {
2757
+ const declaration = statement.declarationList.declarations.find(entry => ts.isIdentifier(entry.name) && entry.name.text === name)
2758
+ if (declaration?.initializer && (ts.isArrowFunction(declaration.initializer) || ts.isFunctionExpression(declaration.initializer))) return declaration.initializer
2759
+ }
2760
+ }
2761
+ return undefined
2762
+ }
2763
+
2764
+ export async function collectClientModules(entries, sourceFiles) {
2765
+ const modules = new Set()
2766
+ const queue = [...new Set(entries)]
2767
+ while (queue.length) {
2768
+ const file = queue.shift()
2769
+ if (modules.has(file)) continue
2770
+ const source = await readFile(file, "utf8")
2771
+ const sourceFile = parseSourceFile(file, source)
2772
+ workerCompiler.rejectConstructions(sourceFile, sourceFile, "Relative TypeScript Worker construction is only supported directly inside an inline useEffect() callback, not imported client helpers")
2773
+ if (containsJsx(sourceFile)) throw new Error(`${relative(root, file)} Imported client helpers must not contain JSX`)
2774
+ rejectUnsupportedClientImports(sourceFile, file)
2775
+ modules.add(file)
2776
+ for (const node of sourceFile.statements) {
2777
+ if ((!ts.isImportDeclaration(node) && !ts.isExportDeclaration(node)) || !node.moduleSpecifier || !ts.isStringLiteral(node.moduleSpecifier) || !runtimeModuleReference(node)) continue
2778
+ if (!node.moduleSpecifier.text.startsWith(".")) throw new Error(`${relative(root, file)} Imported client helpers may only use relative runtime imports`)
2779
+ if (isStaticImport(node.moduleSpecifier.text)) continue
2780
+ queue.push(resolveSourceImport(file, node.moduleSpecifier.text, sourceFiles))
2781
+ }
2782
+ }
2783
+ const outputs = new Map()
2784
+ for (const file of modules) {
2785
+ const output = clientModulePath(file)
2786
+ if (outputs.has(output)) throw new Error(`${relative(root, file)} and ${relative(root, outputs.get(output))} emit the same client module path`)
2787
+ outputs.set(output, file)
2788
+ }
2789
+ return [...modules].sort()
2790
+ }
2791
+
2792
+ export async function compileClientModule(file, sourceFiles, staticFiles, cssModules, base) {
2793
+ const importedAssets = new Set()
2794
+ const source = await readFile(file, "utf8")
2795
+ const transformer = context => sourceFile => {
2796
+ const factory = context.factory
2797
+ const visitor = node => {
2798
+ if (ts.isImportDeclaration(node) && runtimeModuleReference(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text.startsWith(".")) {
2799
+ if (isStaticImport(node.moduleSpecifier.text)) return staticImportEntry(node, sourceFile, file, staticFiles, importedAssets, cssModules, base, factory)?.replacement
2800
+ const target = resolveSourceImport(file, node.moduleSpecifier.text, sourceFiles)
2801
+ return factory.updateImportDeclaration(node, node.modifiers, node.importClause, factory.createStringLiteral(relativeModulePath(clientModulePath(file), clientModulePath(target))), node.attributes)
2802
+ }
2803
+ if (ts.isExportDeclaration(node) && runtimeModuleReference(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text.startsWith(".")) {
2804
+ const target = resolveSourceImport(file, node.moduleSpecifier.text, sourceFiles)
2805
+ return factory.updateExportDeclaration(node, node.modifiers, node.isTypeOnly, node.exportClause, factory.createStringLiteral(relativeModulePath(clientModulePath(file), clientModulePath(target))), node.attributes)
2806
+ }
2807
+ return ts.visitEachChild(node, visitor, context)
2808
+ }
2809
+ return ts.visitNode(sourceFile, visitor)
2810
+ }
2811
+ const result = ts.transpileModule(source, {
2812
+ fileName: file,
2813
+ compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext },
2814
+ transformers: { before: [transformer] },
2815
+ reportDiagnostics: true
2816
+ })
2817
+ const errors = result.diagnostics?.filter(diagnostic => diagnostic.category === ts.DiagnosticCategory.Error) ?? []
2818
+ if (errors.length) throw new Error(errors.map(error => ts.flattenDiagnosticMessageText(error.messageText, "\n")).join("\n"))
2819
+ return { file: relative(root, file).replaceAll(sep, "/"), path: clientModulePath(file), code: result.outputText, importedAssets: [...importedAssets].map(file => relative(root, file).replaceAll(sep, "/")).sort() }
2820
+ }
2821
+
2822
+ function staticImportExtension(specifier) {
2823
+ return extname(specifier.split(/[?#]/, 1)[0]).toLowerCase()
2824
+ }
2825
+
2826
+ function isStaticImport(specifier) {
2827
+ const extension = staticImportExtension(specifier)
2828
+ return extension === ".css" || staticAssetExtensions.has(extension)
2829
+ }
2830
+
2831
+ function resolveStaticImport(importer, specifier, staticFiles) {
2832
+ const target = resolve(dirname(importer), specifier.split(/[?#]/, 1)[0])
2833
+ if (!staticFiles.has(target)) throw new Error(`${relative(root, importer)} Relative asset import ${JSON.stringify(specifier)} must resolve to an existing regular file under src/`)
2834
+ return target
2835
+ }
2836
+
2837
+ export async function safeStaticFiles(files) {
2838
+ const sourceRoot = await realpath(sourceDirectory)
2839
+ const entries = await Promise.all(files.map(async file => {
2840
+ try {
2841
+ const target = await realpath(file)
2842
+ const path = relative(sourceRoot, target)
2843
+ if (path === ".." || path.startsWith(`..${sep}`) || isAbsolute(path) || !(await stat(target)).isFile()) return undefined
2844
+ return file
2845
+ } catch {
2846
+ return undefined
2847
+ }
2848
+ }))
2849
+ return new Set(entries.filter(Boolean))
2850
+ }
2851
+
2852
+ export function orderSourceStyles(cssFiles, sourceFiles, sourceIndex, staticFiles) {
2853
+ const ordered = []
2854
+ const seenStyles = new Set()
2855
+ const seenSources = new Set()
2856
+ const sourceSet = new Set(sourceFiles)
2857
+ const visit = file => {
2858
+ if (seenSources.has(file)) return
2859
+ seenSources.add(file)
2860
+ const sourceFile = parseSourceFile(file, sourceIndex.get(file))
2861
+ for (const statement of sourceFile.statements) {
2862
+ if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier) || !statement.moduleSpecifier.text.startsWith(".")) continue
2863
+ const specifier = statement.moduleSpecifier.text
2864
+ if (staticImportExtension(specifier) === ".css") {
2865
+ let target
2866
+ try { target = resolveStaticImport(file, specifier, staticFiles) } catch { continue }
2867
+ if (!seenStyles.has(target)) {
2868
+ seenStyles.add(target)
2869
+ ordered.push(target)
2870
+ }
2871
+ continue
2872
+ }
2873
+ if (isStaticImport(specifier)) continue
2874
+ try { visit(resolveSourceImport(file, specifier, sourceSet)) } catch {}
2875
+ }
2876
+ }
2877
+ for (const file of sourceFiles.filter(file => file.startsWith(`${pagesDirectory}${sep}`) && file.endsWith(".tsx"))) visit(file)
2878
+ for (const file of sourceFiles) visit(file)
2879
+ return [...ordered, ...cssFiles.filter(file => !seenStyles.has(file))]
2880
+ }
2881
+
2882
+ function staticImportEntry(node, sourceFile, file, staticFiles, importedAssets, cssModules, base, factory) {
2883
+ const specifier = node.moduleSpecifier.text
2884
+ if (specifier.includes("\\") || specifier.includes("#")) throw sourceNodeError(node.moduleSpecifier, sourceFile, "Static asset imports require forward-slash paths without hash suffixes")
2885
+ const queryIndex = specifier.indexOf("?")
2886
+ const query = queryIndex === -1 ? "" : specifier.slice(queryIndex + 1)
2887
+ if (query && query !== "url") throw sourceNodeError(node.moduleSpecifier, sourceFile, "Static asset imports support only the ?url query")
2888
+ let target
2889
+ try {
2890
+ target = resolveStaticImport(file, specifier, staticFiles)
2891
+ } catch (error) {
2892
+ throw sourceNodeError(node.moduleSpecifier, sourceFile, error.message)
2893
+ }
2894
+ if (node.attributes) throw sourceNodeError(node.attributes, sourceFile, "Static asset import attributes are not supported")
2895
+ const extension = staticImportExtension(specifier)
2896
+ if (query === "url") {
2897
+ if (!node.importClause?.name || node.importClause.isTypeOnly || node.importClause.namedBindings) throw sourceNodeError(node, sourceFile, "Static assets require one default import")
2898
+ if (extension !== ".css") importedAssets.add(target)
2899
+ const value = factory.createStringLiteral(assetPath(base, `assets/${relative(sourceDirectory, target).replaceAll(sep, "/")}`))
2900
+ return staticImportReplacement(node.importClause.name.text, value, factory)
2901
+ }
2902
+ if (extension === ".css") {
2903
+ const classes = cssModules.get(target)
2904
+ if (!node.importClause) return undefined
2905
+ if (!classes || !node.importClause.name || node.importClause.isTypeOnly || node.importClause.namedBindings) {
2906
+ const message = classes ? "CSS Modules require one default import" : "CSS imports must be side-effect imports"
2907
+ throw sourceNodeError(node.importClause, sourceFile, message)
2908
+ }
2909
+ const value = factory.createObjectLiteralExpression(Object.entries(classes).sort(([left], [right]) => left.localeCompare(right)).map(([name, scoped]) => factory.createPropertyAssignment(factory.createStringLiteral(name), factory.createStringLiteral(scoped))))
2910
+ return staticImportReplacement(node.importClause.name.text, value, factory)
2911
+ }
2912
+ if (!node.importClause?.name || node.importClause.isTypeOnly || node.importClause.namedBindings) throw sourceNodeError(node, sourceFile, "Static assets require one default import")
2913
+ importedAssets.add(target)
2914
+ const value = factory.createStringLiteral(assetPath(base, `assets/${relative(sourceDirectory, target).replaceAll(sep, "/")}`))
2915
+ return staticImportReplacement(node.importClause.name.text, value, factory)
2916
+ }
2917
+
2918
+ function staticImportReplacement(name, value, factory) {
2919
+ return {
2920
+ name,
2921
+ value,
2922
+ replacement: factory.createVariableStatement(undefined, factory.createVariableDeclarationList([
2923
+ factory.createVariableDeclaration(name, undefined, undefined, value)
2924
+ ], ts.NodeFlags.Const))
2925
+ }
2926
+ }
2927
+
2928
+ function rejectUnsupportedClientImports(sourceFile, file) {
2929
+ const visit = node => {
2930
+ if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) throw new Error(`${relative(root, file)} Dynamic imports are not supported in imported client helpers`)
2931
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "require") throw new Error(`${relative(root, file)} require() is not supported in imported client helpers`)
2932
+ ts.forEachChild(node, visit)
2933
+ }
2934
+ visit(sourceFile)
2935
+ }
2936
+
2937
+ export function layoutExportError(file, source) {
2938
+ const sourceFile = parseSourceFile(file, source)
2939
+ for (const statement of sourceFile.statements) {
2940
+ if (ts.isExportDeclaration(statement) && statement.exportClause && ts.isNamedExports(statement.exportClause)) {
2941
+ const specifier = statement.exportClause.elements.find(entry => entry.name.text === "layout")
2942
+ if (specifier) return sourceNodeError(specifier, sourceFile, "layout export must be a function")
2943
+ }
2944
+ if (ts.isVariableStatement(statement) && statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword)) {
2945
+ const declaration = statement.declarationList.declarations.find(entry => ts.isIdentifier(entry.name) && entry.name.text === "layout")
2946
+ if (declaration) return sourceNodeError(declaration, sourceFile, "layout export must be a function")
2947
+ }
2948
+ if (ts.isFunctionDeclaration(statement) && statement.name?.text === "layout") return sourceNodeError(statement, sourceFile, "layout export must be a function")
2949
+ }
2950
+ return new Error(`${relative(root, file)} layout export must be a function`)
2951
+ }
2952
+
2953
+ export function clientModulePath(file) {
2954
+ return `modules/${relative(sourceDirectory, file).replaceAll(sep, "/").replace(/\.(?:ts|tsx)$/, ".js")}`
2955
+ }
2956
+
2957
+ export function compiledPath(file) {
2958
+ return join(workDirectory, relative(sourceDirectory, file)).replace(/\.(?:ts|tsx)$/, ".mjs")
2959
+ }
2960
+
2961
+ const compileEventCommand = createCommandSpecializer({ isPrimitiveLiteral: isPrimitiveDefaultLiteral })
2962
+ const { analyzeZustandStores, normalizeZustandMigrationSyntax } = createZustandPass({ isSerializableStateLiteral, nativeCaptureNames, sourceDirectory })
2963
+ const workerCompiler = createWorkerCompiler({ root, sourceDirectory, assetPath, parseSourceFile, resolveSourceImport, runtimeModuleReference })
2964
+ const handlerLowering = createHandlerLowering({ cloneAst, synthesizeTree })
2965
+ const printHandlerModule = createHandlerCodegen({
2966
+ resolveClientImport: (entry, handlerPath) => entry.package ? entry.target : relativeModulePath(handlerPath, clientModulePath(entry.target))
2967
+ })
2968
+ const { normalizeReactMigrationSyntax, validateUseIdSyntax } = createReactMigrationPass({ cloneAst, jsxTagName })
2969
+ const normalizeReactRouterSyntax = createRouterPass({ withBase })