@kudzujs/core 0.8.61 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/MIGRATION_ROADMAP.md +37 -1
  2. package/PERFORMANCE.md +79 -1
  3. package/README.md +2 -2
  4. package/RELEASES.md +58 -0
  5. package/bin/kudzu.mjs +10 -1
  6. package/docs/next-architecture/0.9-baseline.md +1199 -0
  7. package/docs/next-architecture/0.9-benchmark-contracts.md +507 -0
  8. package/docs/next-architecture/0.9-component-property-contract.md +89 -0
  9. package/docs/next-architecture/0.9-compression-ledger.md +227 -0
  10. package/docs/next-architecture/0.9-final-proof-audit.md +176 -0
  11. package/docs/next-architecture/0.9-implementation-plan.md +1819 -0
  12. package/docs/next-architecture/0.9-resource-lifecycle.md +118 -0
  13. package/docs/next-architecture/0.9-semantic-compression.md +384 -0
  14. package/docs/next-architecture/README.md +16 -12
  15. package/docs/next-architecture/compiler-current-architecture.md +7 -7
  16. package/docs/next-architecture/large-application-ai-native-roadmap.md +6 -4
  17. package/docs/next-architecture/versioning.md +3 -2
  18. package/framework/README.md +3 -1
  19. package/framework/binding-runtime.js +4 -4
  20. package/framework/build.mjs +135 -30
  21. package/framework/compiler/ast-helpers.mjs +5 -0
  22. package/framework/compiler/browser-signal-passes.mjs +2 -7
  23. package/framework/compiler/collection-analysis.mjs +4 -0
  24. package/framework/compiler/descriptor-session.mjs +36 -12
  25. package/framework/compiler/effect-analysis.mjs +28 -8
  26. package/framework/compiler/effect-codegen.mjs +79 -36
  27. package/framework/compiler/effect-private-ref-pass.mjs +4 -8
  28. package/framework/compiler/handler-lowering.mjs +12 -7
  29. package/framework/compiler/ir/module-ir.mjs +26 -4
  30. package/framework/compiler/list-runtime-codegen.mjs +4 -2
  31. package/framework/compiler/optimize/command-specialization.mjs +4 -7
  32. package/framework/compiler/outside-click-pass.mjs +79 -0
  33. package/framework/compiler/react-migration-pass.mjs +27 -1
  34. package/framework/compiler/route-artifact-report.mjs +4 -3
  35. package/framework/compiler/route-build-record.mjs +12 -0
  36. package/framework/compiler/route-capability-planner.mjs +3 -3
  37. package/framework/compiler/route-ir.mjs +27 -11
  38. package/framework/compiler/runtime-codegen.mjs +2 -2
  39. package/framework/compiler/source-compiler.mjs +407 -74
  40. package/framework/core.d.ts +1 -0
  41. package/framework/core.mjs +18 -5
  42. package/framework/dependency-runtime.js +1 -1
  43. package/framework/effect-runtime.js +2 -2
  44. package/framework/list-runtime.js +67 -24
  45. package/framework/native-runtime.js +12 -9
  46. package/framework/runtime.js +1 -1
  47. package/framework/serialization.js +13 -6
  48. package/framework/shared-runtime.js +14 -12
  49. package/package.json +1 -1
@@ -1,5 +1,6 @@
1
1
  import { readFile, realpath, stat } from "node:fs/promises"
2
2
  import { dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path"
3
+ import { transform, transformSync } from "esbuild"
3
4
  import ts from "typescript"
4
5
  import { createBindingIndex } from "./analysis/binding-index.mjs"
5
6
  import { createComponentAnalysisSession } from "./analysis/component-analysis.mjs"
@@ -13,6 +14,7 @@ import { analyzeEffectDependencies, validateEffectOwnedBrowserResources } from "
13
14
  import { createHandlerCodegen } from "./handler-codegen.mjs"
14
15
  import { createHandlerLowering } from "./handler-lowering.mjs"
15
16
  import { registerSharedAction, registerSharedState } from "./ir/module-ir.mjs"
17
+ import { analyzeOutsideClickHook, normalizeOutsideClickHooks } from "./outside-click-pass.mjs"
16
18
  import { createCommandSpecializer } from "./optimize/command-specialization.mjs"
17
19
  import { applyNormalizationPasses } from "./normalization-pipeline.mjs"
18
20
  import { assetPath, relativeModulePath, withBase } from "./path-helpers.mjs"
@@ -27,16 +29,24 @@ const { root, sourceDirectory, pagesDirectory, workDirectory, workerCompiler, mo
27
29
  const buildDirectory = project.buildDirectory ?? workDirectory
28
30
  const { ordinaryRuntimeDependencies, resolveSourceImport, runtimeModuleReference } = project.graph
29
31
  const parseSourceFile = (file, source) => modules.read(file, source).sourceFile
32
+ const importFreeModules = new Map()
30
33
  const staticAssetExtensions = new Set([".avif", ".gif", ".ico", ".jpeg", ".jpg", ".otf", ".png", ".svg", ".ttf", ".webp", ".woff", ".woff2"])
31
34
 
32
- function compileSource(file, sourceFiles, sourceIndex, staticFiles, cssModules, base) {
35
+ async function compileSourceAsync(file, sourceFiles, sourceIndex, staticFiles, cssModules, base) {
36
+ const source = sourceIndex.get(file)
37
+ const output = importFreeTypeScriptModule(file, source) ? (await transform(source, { loader: "ts", format: "esm", target: "es2022", sourcefile: file })).code : undefined
38
+ return compileSource(file, sourceFiles, sourceIndex, staticFiles, cssModules, base, output)
39
+ }
40
+
41
+ function compileSource(file, sourceFiles, sourceIndex, staticFiles, cssModules, base, importFreeOutput) {
33
42
  const importedAssets = new Set()
34
43
  const source = sourceIndex.get(file)
35
44
  const semantic = createSemanticArtifact(relative(root, file).replaceAll(sep, "/"))
36
45
  const handlerPath = `handlers/${relative(sourceDirectory, file).replaceAll(sep, "/").replace(/\.(?:ts|tsx)$/, ".js")}`
37
- const plain = plainTypeScriptModule(file, source, sourceFiles)
46
+ const importFreePlain = importFreeTypeScriptModule(file, source)
47
+ const plain = importFreePlain || plainTypeScriptModule(file, source, sourceFiles)
38
48
  if (plain && counters) counters.plainModules = (counters.plainModules ?? 0) + 1
39
- const result = ts.transpileModule(source, {
49
+ const result = importFreePlain ? { outputText: importFreeOutput ?? transformSync(source, { loader: "ts", format: "esm", target: "es2022", sourcefile: file }).code } : ts.transpileModule(source, {
40
50
  fileName: file,
41
51
  compilerOptions: {
42
52
  target: ts.ScriptTarget.ES2022,
@@ -86,6 +96,15 @@ function plainTypeScriptModule(file, source, sourceFiles) {
86
96
  return true
87
97
  }
88
98
 
99
+ function importFreeTypeScriptModule(file, source) {
100
+ if (!file.endsWith(".ts")) return false
101
+ const cached = importFreeModules.get(file)
102
+ if (cached?.source === source) return cached.result
103
+ const result = !source.includes(".worker.ts") && ts.preProcessFile(source, true, true).importedFiles.length === 0
104
+ importFreeModules.set(file, { source, result })
105
+ return result
106
+ }
107
+
89
108
  function createPlainModuleTransformer(file, sourceFiles) {
90
109
  return context => sourceFile => context.factory.updateSourceFile(sourceFile, sourceFile.statements.map(statement => {
91
110
  if ((!ts.isImportDeclaration(statement) && !ts.isExportDeclaration(statement)) || !runtimeModuleReference(statement) || !statement.moduleSpecifier || !ts.isStringLiteral(statement.moduleSpecifier) || !statement.moduleSpecifier.text.startsWith(".")) return statement
@@ -125,7 +144,9 @@ function reachableSourceFiles(entries, sourceFiles, sourceIndex) {
125
144
  if (visited.has(file)) continue
126
145
  visited.add(file)
127
146
  reachable.add(file)
128
- const sourceFile = parseSourceFile(file, sourceIndex.get(file))
147
+ const source = sourceIndex.get(file)
148
+ if (importFreeTypeScriptModule(file, source)) continue
149
+ const sourceFile = parseSourceFile(file, source)
129
150
  if (owner === "ordinary") for (const target of ordinaryRuntimeDependencies(file, sourceFile, sourceFiles, isStaticImport)) queue.push({ file: target, owner: target.endsWith(".worker.ts") ? "worker" : owner })
130
151
  const visit = node => {
131
152
  if (owner === "worker") {
@@ -305,6 +326,7 @@ function normalizeCompilerSource(sourceFile, { base, context, file, importedColl
305
326
  source => normalizeReactMigrationSyntax(source, factory, context, importedCollections ?? importedSerializableCollectionNames(source, file, sourceFiles, sourceIndex)),
306
327
  source => normalizeNavigatorCapabilityConditions(source, factory, context),
307
328
  source => normalizeParameterizedDebounceHooks(source, factory, context),
329
+ source => normalizeOutsideClickHooks(source, factory, context),
308
330
  source => normalizeEffectPrivateRefs(source, factory, context),
309
331
  source => {
310
332
  const result = normalizeCustomHookTimerRefs(source, factory, context)
@@ -330,7 +352,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
330
352
  const { moduleIR } = semantic
331
353
  return context => sourceFile => {
332
354
  const hasLinkElements = /<link/i.test(sourceFile.text)
333
- const importedStaticCollections = importedSerializableCollections(sourceFile, file, sourceFiles, sourceIndex)
355
+ const importedStaticCollections = importedSerializableCollections(sourceFile, file, sourceFiles, sourceIndex, true)
334
356
  const importedCollections = new Set(importedStaticCollections.keys())
335
357
  const normalized = normalizeCompilerSource(sourceFile, { base, context, file, importedCollections, importedStaticCollections, sourceFiles, sourceIndex })
336
358
  sourceFile = normalized.sourceFile
@@ -393,8 +415,16 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
393
415
  }
394
416
  const importedCollectionTransforms = new Map()
395
417
  const importedCalculationFunctions = new Map()
418
+ const validatedEffectCalculations = new WeakSet()
419
+ const transformImports = new Set()
420
+ const collectTransformImports = node => {
421
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) transformImports.add(node.expression.text)
422
+ if ((ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) && ts.isIdentifier(node.tagName)) transformImports.add(node.tagName.text)
423
+ ts.forEachChild(node, collectTransformImports)
424
+ }
425
+ collectTransformImports(sourceFile)
396
426
  for (const [name, binding] of importBindings) {
397
- if (binding.kind === "namespace") continue
427
+ if (binding.kind === "namespace" || !transformImports.has(name)) continue
398
428
  try {
399
429
  importedCollectionTransforms.set(name, resolveComponentExport(binding.target, binding.kind === "default" ? "default" : binding.imported, target => parseSourceFile(target, sourceIndex.get(target)), sourceFiles))
400
430
  } catch {}
@@ -422,6 +452,8 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
422
452
  const customHooks = new Map()
423
453
  const parameterizedDebounceCalls = new WeakSet()
424
454
  const resolvedParameterizedDebounceHooks = new Map()
455
+ const outsideClickCalls = new WeakMap()
456
+ const resolvedOutsideClickHooks = new Map()
425
457
  const jsxLocalDeclarations = new Map()
426
458
  const jsxLocalsByFunction = new Map()
427
459
  const listLocalDeclarations = []
@@ -616,6 +648,15 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
616
648
  resolvedParameterizedDebounceHooks.set(key, analysis)
617
649
  return analysis
618
650
  }
651
+ const resolveOutsideClickHook = binding => {
652
+ const exportName = binding.kind === "default" ? "default" : binding.imported
653
+ const key = `${binding.target}:${exportName}`
654
+ if (resolvedOutsideClickHooks.has(key)) return resolvedOutsideClickHooks.get(key)
655
+ const hook = resolveComponentExport(binding.target, exportName, importedSource, sourceFiles)
656
+ const analysis = analyzeOutsideClickHook(hook)
657
+ resolvedOutsideClickHooks.set(key, analysis)
658
+ return analysis
659
+ }
619
660
 
620
661
  const collect = node => {
621
662
  if (ts.isVariableDeclaration(node) && node.initializer && ts.isCallExpression(node.initializer)) {
@@ -629,7 +670,8 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
629
670
  const delay = node.initializer.arguments[1] && unwrapExpression(node.initializer.arguments[1])
630
671
  const setters = owner ? settersByFunction.get(owner) ?? new Map() : new Map()
631
672
  if (!owner) throw sourceNodeError(node, sourceFile, "Parameterized debounce hooks cannot be used outside a Kudzu component")
632
- if (node.initializer.arguments.length !== 2 || !ts.isIdentifier(value) || !new Set(setters.values()).has(value.text) || !componentHasDirectPrimitiveState(owner, value.text)) throw sourceNodeError(node.initializer, sourceFile, "Parameterized debounce hooks require one direct primitive state argument")
673
+ const stateInitializer = ts.isIdentifier(value) ? directStateInitializer(owner, value.text) : undefined
674
+ if (node.initializer.arguments.length !== 2 || !ts.isIdentifier(value) || !new Set(setters.values()).has(value.text) || !stateInitializer || !isPrimitiveDefaultLiteral(stateInitializer)) throw sourceNodeError(node.initializer, sourceFile, "Parameterized debounce hooks require one direct primitive state argument")
633
675
  if (!ts.isNumericLiteral(delay)) throw sourceNodeError(node.initializer.arguments[1] ?? node.initializer, sourceFile, "Parameterized debounce hook delays must be numeric literals")
634
676
  const syntheticSetter = `__kSetDebounced_${Math.max(0, node.pos)}`
635
677
  setters.set(syntheticSetter, node.name.text)
@@ -657,6 +699,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
657
699
  }
658
700
  }
659
701
  const contextSubstitutions = new Map()
702
+ const contextSharedStates = new Map()
660
703
  for (const [setter, state] of hook.states) {
661
704
  if (hook.context) {
662
705
  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`)
@@ -676,6 +719,9 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
676
719
  const localSetter = localName(setter)
677
720
  setters.set(localSetter, localState)
678
721
  registerState(owner, localState, localSetter, "context", node, { owner: hook.stateOwner, state: hook.stateSymbols.get(state) })
722
+ const sharedState = registerSharedState(moduleIR, { identity: hook.stateOwner.symbol.id, field: state })
723
+ contextSharedStates.set(state, sharedState)
724
+ stateOwnersByFunction.get(owner).set(localState, { kind: "shared-state", sharedState: sharedState.slot })
679
725
  if (requiredContextStates.has(state)) {
680
726
  for (const [field, local] of [[state, localState], [setter, localSetter]]) {
681
727
  if (names.has(field) || privateFields.some(entry => (typeof entry === "string" ? entry : entry.property) === field)) continue
@@ -710,7 +756,16 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
710
756
  if (hook.context) {
711
757
  const reducers = reducersByFunction.get(owner) ?? new Map()
712
758
  const states = new Map([...hook.states].map(([setter, state]) => [contextSubstitutions.get(setter)?.text ?? setter, contextSubstitutions.get(state)?.text ?? state]))
713
- reducers.set(name, { contextAction: callback, states })
759
+ const referenced = referencedStateNames(hook.callbacks.get(name).body, hook.states, hook.callbacks.get(name))
760
+ const anchor = [...hook.states.values()].find(state => referenced.has(state))
761
+ const sharedState = contextSharedStates.get(anchor)
762
+ if (!sharedState) {
763
+ reducers.set(name, { sourceKind: "Context", directImplementation: callback, states })
764
+ reducersByFunction.set(owner, reducers)
765
+ continue
766
+ }
767
+ const action = registerSharedAction(moduleIR, { state: sharedState.slot, name })
768
+ reducers.set(name, { state: contextSubstitutions.get(anchor)?.text ?? anchor, sourceKind: "Context", sharedAction: { ...action, directImplementation: callback, states } })
714
769
  reducersByFunction.set(owner, reducers)
715
770
  }
716
771
  }
@@ -788,6 +843,17 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
788
843
  }
789
844
  }
790
845
  }
846
+ if (ts.isCallExpression(node) && ts.isExpressionStatement(node.parent) && ts.isIdentifier(node.expression) && /^use[A-Z]/.test(node.expression.text) && importBindings.has(node.expression.text) && importBindings.get(node.expression.text).kind !== "namespace") {
847
+ const hook = resolveOutsideClickHook(importBindings.get(node.expression.text))
848
+ if (hook) {
849
+ const owner = nearestFunction(node)
850
+ const ref = node.arguments[0] && unwrapExpression(node.arguments[0])
851
+ const callback = directSetterLiteralCallback(node.arguments[1], owner ? settersByFunction.get(owner) ?? new Map() : new Map())
852
+ if (!owner || node.arguments.length !== 2 || !ts.isIdentifier(ref) || !componentHasDirectObjectRef(owner, ref.text)) throw sourceNodeError(node, sourceFile, "Outside-click hooks require one direct component DOM ref as their first argument")
853
+ if (!callback) throw sourceNodeError(node.arguments[1] ?? node, sourceFile, "Outside-click hooks require one inline direct literal setter callback")
854
+ outsideClickCalls.set(node, callback)
855
+ }
856
+ }
791
857
  if (ts.isFunctionDeclaration(node) && node.name) {
792
858
  functions.set(node.name.text, node)
793
859
  if (node.parent === sourceFile) {
@@ -936,6 +1002,78 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
936
1002
  if (!returns.length || returns.some(returned => !returned || !ts.isObjectLiteralExpression(returned))) fail(call.expression, "Reactive imported calculations must return a plain object")
937
1003
  const fieldExists = returns.every(returned => returned.properties.some(property => ts.isSpreadAssignment(property) || !ts.isComputedPropertyName(property.name) && property.name.text === field))
938
1004
  if (!fieldExists) fail(call.parent, `Reactive imported calculation does not return field ${JSON.stringify(field)}`)
1005
+ return { calculation, returns }
1006
+ }
1007
+ const validateSelectedEffectCalculation = (call, field) => {
1008
+ const { calculation, returns } = validateImportedCalculation(call, field)
1009
+ const calculationSource = calculation.getSourceFile()
1010
+ const reject = (target, message) => { throw sourceNodeError(target, calculationSource, message) }
1011
+ if (calculation.asteriskToken || calculation.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) reject(calculation, "Imported calculations used by useEffect() must be synchronous; move async work into the owned effect")
1012
+ if (calculation.parameters.some(parameter => !ts.isIdentifier(parameter.name) || parameter.dotDotDotToken || parameter.initializer || parameter.questionToken)) reject(calculation, "Imported calculations used by useEffect() require ordinary identifier parameters without defaults or rest")
1013
+ if (call.arguments.some(argument => !ts.isIdentifier(unwrapExpression(argument)))) fail(call, "useEffect() selected calculation arguments must be direct primitive state identifiers; aliases, property paths, and composed arguments are not supported")
1014
+ const shapes = returns.map(returned => returned.properties.map(property => {
1015
+ if (!ts.isPropertyAssignment(property) && !ts.isShorthandPropertyAssignment(property) || ts.isComputedPropertyName(property.name) || ["__proto__", "constructor", "prototype"].includes(property.name.text)) reject(property, "Imported calculations used by useEffect() must return direct safe plain-object fields without spreads, methods, or computed names")
1016
+ return property.name.text
1017
+ }).sort())
1018
+ const expected = JSON.stringify(shapes[0])
1019
+ if (shapes.some(shape => JSON.stringify(shape) !== expected)) reject(calculation, `Imported calculations used by useEffect() must return the same direct plain-object fields on every path; expected ${expected}`)
1020
+ if (validatedEffectCalculations.has(calculation)) return
1021
+ const staticImports = importedSerializableCollections(calculationSource, calculationSource.fileName, sourceFiles, sourceIndex)
1022
+ const imported = new Map()
1023
+ for (const statement of calculationSource.statements) if (ts.isImportDeclaration(statement) && statement.importClause && !statement.importClause.isTypeOnly && ts.isStringLiteral(statement.moduleSpecifier)) {
1024
+ const names = []
1025
+ if (statement.importClause.name) names.push(statement.importClause.name.text)
1026
+ const bindings = statement.importClause.namedBindings
1027
+ if (bindings && ts.isNamespaceImport(bindings)) names.push(bindings.name.text)
1028
+ if (bindings && ts.isNamedImports(bindings)) names.push(...bindings.elements.filter(entry => !entry.isTypeOnly).map(entry => entry.name.text))
1029
+ for (const name of names) imported.set(name, statement.moduleSpecifier.text)
1030
+ }
1031
+ for (const [name, target] of imported) if (!target.startsWith(".") && referencesIdentifier(calculation.body, name)) reject(calculation.body, `Imported calculations used by useEffect() cannot reference package import ${JSON.stringify(name)} from ${JSON.stringify(target)}`)
1032
+ const localNames = new Set(calculation.parameters.flatMap(parameter => bindingNames(parameter.name)))
1033
+ if (ts.isBlock(calculation.body)) for (const statement of calculation.body.statements) {
1034
+ if (ts.isVariableStatement(statement)) for (const declaration of statement.declarationList.declarations) for (const name of bindingNames(declaration.name)) localNames.add(name)
1035
+ if (ts.isFunctionDeclaration(statement) && statement.name) localNames.add(statement.name.text)
1036
+ }
1037
+ const localDeclarations = new Map()
1038
+ if (ts.isBlock(calculation.body)) for (const statement of calculation.body.statements) if (ts.isVariableStatement(statement)) for (const declaration of statement.declarationList.declarations) if (ts.isIdentifier(declaration.name) && declaration.initializer) localDeclarations.set(declaration.name.text, declaration.initializer)
1039
+ const visiting = []
1040
+ const visited = new Set()
1041
+ const visitLocal = name => {
1042
+ if (visited.has(name)) return
1043
+ const cycle = visiting.indexOf(name)
1044
+ if (cycle >= 0) reject(localDeclarations.get(name), `Imported calculation derived-local cycle: ${[...visiting.slice(cycle), name].join(" -> ")}`)
1045
+ visiting.push(name)
1046
+ const initializer = localDeclarations.get(name)
1047
+ if (initializer) for (const candidate of localDeclarations.keys()) if (referencesIdentifier(initializer, candidate)) visitLocal(candidate)
1048
+ visiting.pop()
1049
+ visited.add(name)
1050
+ }
1051
+ for (const name of localDeclarations.keys()) visitLocal(name)
1052
+ const visit = node => {
1053
+ if (ts.isTypeNode(node)) return
1054
+ if (ts.isBinaryExpression(node) && assignmentOperators.has(node.operatorToken.kind) || ts.isPostfixUnaryExpression(node) || ts.isPrefixUnaryExpression(node) && [ts.SyntaxKind.PlusPlusToken, ts.SyntaxKind.MinusMinusToken].includes(node.operator) || ts.isDeleteExpression(node)) reject(node, "Imported calculations used by useEffect() must be pure; assignments, updates, delete, and mutation are not supported")
1055
+ if (ts.isAwaitExpression(node) || ts.isYieldExpression(node) || ts.isNewExpression(node)) reject(node, "Imported calculations used by useEffect() must be deterministic and synchronous; await, yield, and construction are not supported")
1056
+ if (ts.isCallExpression(node)) {
1057
+ if (ts.isIdentifier(node.expression) && ["Boolean", "Number", "String"].includes(node.expression.text)) {
1058
+ // Primitive conversions are deterministic.
1059
+ } else if (ts.isPropertyAccessExpression(node.expression)) {
1060
+ const method = node.expression.name.text
1061
+ const receiver = unwrapExpression(node.expression.expression)
1062
+ if (mutatingListMethods.has(method)) reject(node, `Imported calculations used by useEffect() must be pure; mutating method ${JSON.stringify(method)} is not supported`)
1063
+ const math = ts.isIdentifier(receiver) && receiver.text === "Math"
1064
+ const find = method === "find" && node.arguments.length === 1 && ts.isArrowFunction(node.arguments[0]) && !ts.isBlock(node.arguments[0].body) && !node.arguments[0].modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)
1065
+ if (!find && !(math && pureMathMethods.has(method)) && !pureListMethods.has(method)) reject(node, `Imported calculations used by useEffect() must be deterministic; call ${JSON.stringify(node.expression.getText(calculationSource))} is not supported`)
1066
+ } else reject(node, "Imported calculations used by useEffect() cannot call arbitrary functions")
1067
+ }
1068
+ if (ts.isIdentifier(node) && isReferenceIdentifier(node)) {
1069
+ const target = imported.get(node.text)
1070
+ if (target && !target.startsWith(".")) reject(node, `Imported calculations used by useEffect() cannot reference package import ${JSON.stringify(node.text)} from ${JSON.stringify(target)}`)
1071
+ if (target?.startsWith(".") && !staticImports.has(node.text)) reject(node, `Imported calculations used by useEffect() may capture only relative exported JSON-safe const arrays; capture ${JSON.stringify(node.text)} is opaque or nonserializable`)
1072
+ }
1073
+ ts.forEachChild(node, visit)
1074
+ }
1075
+ visit(calculation.body)
1076
+ validatedEffectCalculations.add(calculation)
939
1077
  }
940
1078
  const validateReactiveJsxExpression = (expression, allowedNames) => {
941
1079
  const value = unwrapExpression(expression)
@@ -1009,6 +1147,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1009
1147
  }
1010
1148
  const componentSpecializations = new WeakMap()
1011
1149
  const specializedEffectStateOwners = new WeakMap()
1150
+ const calculationDerivedByOwner = new WeakMap()
1012
1151
  const setterHookHelpers = new WeakMap()
1013
1152
  const expandedRowSpecializations = new WeakMap()
1014
1153
  const nestedRowSpecializations = new Map()
@@ -1039,7 +1178,9 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1039
1178
  props: result.props.map(prop => {
1040
1179
  const expression = result.propExpressions.get(prop.name)
1041
1180
  const signals = expression ? propSignals(expression) : []
1042
- return { ...prop, ...(signals.length ? { signals } : {}) }
1181
+ const uses = result.propertyUses.get(prop.local) ?? []
1182
+ const properties = signals.length === 1 ? uses.map(use => ({ signal: signals[0], path: use.path, consumers: use.consumers, equality: "object-is" })) : []
1183
+ return { ...prop, ...(signals.length ? { signals } : {}), ...(properties.length ? { properties } : {}) }
1043
1184
  }),
1044
1185
  states: [
1045
1186
  ...result.rowStates.map(({ state, setter, source }) => ({ name: state, setter, kind: "row", ...(analysisSite(source, "hook") ? { site: analysisSite(source, "hook") } : {}), ...(analysisSource(source) ? { source: analysisSource(source) } : {}) })),
@@ -1117,6 +1258,44 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1117
1258
  merged.parent = root.parent
1118
1259
  return merged
1119
1260
  }
1261
+ const materializeComponentHelper = (call, specialization, prefix, prepend) => {
1262
+ const owner = nearestFunction(call)
1263
+ const name = `${prefix}${Math.max(0, call.pos)}`
1264
+ const effectStatements = specialization.effects.map(entry => {
1265
+ const effectCall = factory.updateCallExpression(entry.call, factory.createIdentifier("__kComponentUseEffect"), entry.call.typeArguments, entry.call.arguments)
1266
+ synthesizeTree(effectCall)
1267
+ ts.setOriginalNode(effectCall, entry.source)
1268
+ specializedEffectStateOwners.set(effectCall, { owner: { kind: "specialization", slot: specialization.analysis.slot }, references: specialization.propStateOwners })
1269
+ return factory.createExpressionStatement(effectCall)
1270
+ })
1271
+ const rendered = prepend ? factory.createNull() : specialization.root
1272
+ const helper = factory.createFunctionDeclaration(undefined, undefined, name, undefined, [], undefined, factory.createBlock([...specialization.hookDeclarations, ...effectStatements, factory.createReturnStatement(rendered)], true))
1273
+ ts.setParentRecursive(helper, false)
1274
+ helper.parent = owner.body
1275
+ const helpers = setterHookHelpers.get(owner.body) ?? []
1276
+ helpers.push(helper)
1277
+ setterHookHelpers.set(owner.body, helpers)
1278
+ const setters = new Map(settersForNode(call, settersByFunction))
1279
+ for (const state of specialization.ordinaryStates) setters.set(state.setter, state.state)
1280
+ settersByFunction.set(helper, setters)
1281
+ const stateOwners = new Map([...stateOwnersForNode(call), ...specialization.propStateOwners])
1282
+ for (const state of specialization.ordinaryStates) stateOwners.set(state.state, state.analysisReference)
1283
+ stateOwnersByFunction.set(helper, stateOwners)
1284
+ usesComponentState ||= specialization.ordinaryStates.length > 0
1285
+ usesComponentId ||= specialization.usesComponentId
1286
+ usesComponentRef ||= specialization.ordinaryRefs.length > 0
1287
+ usesComponentEffects ||= specialization.effects.length > 0
1288
+ const invocation = factory.createJsxSelfClosingElement(factory.createIdentifier(name), undefined, factory.createJsxAttributes([]))
1289
+ specialization.root = prepend ? prependJsxChild(specialization.root, invocation, factory) : invocation
1290
+ ts.setParentRecursive(specialization.root, false)
1291
+ specialization.root.parent = call.parent
1292
+ }
1293
+ const attachStateBackedEffects = (call, specialization, componentSource, imported) => {
1294
+ if (imported) synthesizeTree(specialization.root = mergeSpecializedImports(specialization.root, componentSource, call, specialization.effects))
1295
+ if (!specialization.effects.length) return
1296
+ if (specialization.hookDeclarations.length) fail(call, "State-backed object property components cannot declare state, refs, or IDs")
1297
+ materializeComponentHelper(call, specialization, "KPropertyEffect", true)
1298
+ }
1120
1299
  const expandReducerCallbacks = (root, componentSource, call, ownership) => {
1121
1300
  const componentImports = clientImportBindings(componentSource, componentSource.fileName, sourceFiles)
1122
1301
  const replacements = new WeakMap()
@@ -1300,8 +1479,10 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1300
1479
  if (identifierReferenceCount(sourceFile, name) !== calls.length) fail(component.declaration, `State-backed list component ${name} may only be referenced as JSX`)
1301
1480
  if (stateBackedCalls.length !== calls.length) fail(component.declaration, `State-backed list component ${name} must receive its mapped prop from local state at every call`)
1302
1481
  for (const call of stateBackedCalls) {
1303
- const specialization = specialize(call, component.function)
1304
- if (specialization.effects.length) fail(call, "State-backed list components cannot declare effects")
1482
+ const objectProperty = isStateBackedListComponentCall(call, component.function, settersByFunction.get(nearestFunction(call)) ?? new Map(), true)
1483
+ const specialization = specialize(call, component.function, objectProperty ? "Object-property component" : "Keyed list")
1484
+ if (specialization.effects.length && !objectProperty) fail(call, "State-backed list components cannot declare effects")
1485
+ if (objectProperty) attachStateBackedEffects(call, specialization, component.function.getSourceFile(), false)
1305
1486
  componentSpecializations.set(call, specialization)
1306
1487
  stateBackedComponentRoots.push(specialization.root)
1307
1488
  }
@@ -1322,8 +1503,10 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1322
1503
  }
1323
1504
  const stateBackedCalls = calls.filter(call => isStateBackedListComponentCall(call, component, settersByFunction.get(nearestFunction(call)) ?? new Map()))
1324
1505
  for (const call of stateBackedCalls) {
1325
- const specialization = specialize(call, component)
1326
- if (specialization.effects.length) fail(call, "State-backed list components cannot declare effects")
1506
+ const objectProperty = isStateBackedListComponentCall(call, component, settersByFunction.get(nearestFunction(call)) ?? new Map(), true)
1507
+ const specialization = specialize(call, component, objectProperty ? "Object-property component" : "Keyed list")
1508
+ if (specialization.effects.length && !objectProperty) fail(call, "State-backed list components cannot declare effects")
1509
+ if (objectProperty) attachStateBackedEffects(call, specialization, component.getSourceFile(), true)
1327
1510
  componentSpecializations.set(call, specialization)
1328
1511
  stateBackedComponentRoots.push(specialization.root)
1329
1512
  }
@@ -1340,7 +1523,8 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1340
1523
  const setterAttribute = attributes.properties.find(entry => ts.isJsxAttribute(entry) && entry.name.text === prop)
1341
1524
  const stateValue = stateAttribute?.initializer && ts.isJsxExpression(stateAttribute.initializer) ? unwrapExpression(stateAttribute.initializer.expression) : undefined
1342
1525
  const setterValue = setterAttribute?.initializer && ts.isJsxExpression(setterAttribute.initializer) ? unwrapExpression(setterAttribute.initializer.expression) : undefined
1343
- if (!ts.isIdentifier(stateValue) || !ts.isIdentifier(setterValue) || setters.get(setterValue.text) !== stateValue.text || !componentHasDirectArrayState(nearestFunction(call), stateValue.text)) fail(setterAttribute ?? call, `Setter-callback effect prop ${JSON.stringify(prop)} must target the same direct array state passed through ${JSON.stringify(effect.stateProp)}`)
1526
+ const stateInitializer = ts.isIdentifier(stateValue) ? directStateInitializer(nearestFunction(call), stateValue.text) : undefined
1527
+ if (!ts.isIdentifier(stateValue) || !ts.isIdentifier(setterValue) || setters.get(setterValue.text) !== stateValue.text || !stateInitializer || !ts.isArrayLiteralExpression(stateInitializer)) fail(setterAttribute ?? call, `Setter-callback effect prop ${JSON.stringify(prop)} must target the same direct array state passed through ${JSON.stringify(effect.stateProp)}`)
1344
1528
  }
1345
1529
  const specialization = specialize(call, component, "Setter-callback", true, true, new Set(settersForNode(call, settersByFunction).values()))
1346
1530
  if (specialization.hookDeclarations.length || specialization.effects.length) {
@@ -1358,44 +1542,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1358
1542
  }
1359
1543
  specialization.root = expandSetterComponents(specialization.root, component.getSourceFile(), [component], specialization, settersForNode(call, settersByFunction), stateOwnersForNode(call))
1360
1544
  if (imported) synthesizeTree(specialization.root = mergeSpecializedImports(specialization.root, component.getSourceFile(), call, specialization.effects))
1361
- if (specialization.hookDeclarations.length || specialization.effects.length) {
1362
- const owner = nearestFunction(call)
1363
- const name = `KSetterComponent${Math.max(0, call.pos)}`
1364
- const effectStatements = specialization.effects.map(entry => {
1365
- const effectCall = factory.updateCallExpression(entry.call, factory.createIdentifier("__kComponentUseEffect"), entry.call.typeArguments, entry.call.arguments)
1366
- synthesizeTree(effectCall)
1367
- ts.setOriginalNode(effectCall, entry.source)
1368
- specializedEffectStateOwners.set(effectCall, { owner: { kind: "specialization", slot: specialization.analysis.slot }, references: specialization.propStateOwners })
1369
- return factory.createExpressionStatement(effectCall)
1370
- })
1371
- const helper = factory.createFunctionDeclaration(
1372
- undefined,
1373
- undefined,
1374
- name,
1375
- undefined,
1376
- [],
1377
- undefined,
1378
- factory.createBlock([...specialization.hookDeclarations, ...effectStatements, factory.createReturnStatement(specialization.root)], true)
1379
- )
1380
- ts.setParentRecursive(helper, false)
1381
- helper.parent = owner.body
1382
- const helpers = setterHookHelpers.get(owner.body) ?? []
1383
- helpers.push(helper)
1384
- setterHookHelpers.set(owner.body, helpers)
1385
- const setters = new Map(settersForNode(call, settersByFunction))
1386
- for (const state of specialization.ordinaryStates) setters.set(state.setter, state.state)
1387
- settersByFunction.set(helper, setters)
1388
- const stateOwners = new Map([...stateOwnersForNode(call), ...specialization.propStateOwners])
1389
- for (const state of specialization.ordinaryStates) stateOwners.set(state.state, state.analysisReference)
1390
- stateOwnersByFunction.set(helper, stateOwners)
1391
- usesComponentState ||= specialization.ordinaryStates.length > 0
1392
- usesComponentId ||= specialization.usesComponentId
1393
- usesComponentRef ||= specialization.ordinaryRefs.length > 0
1394
- usesComponentEffects ||= specialization.effects.length > 0
1395
- specialization.root = factory.createJsxSelfClosingElement(factory.createIdentifier(name), undefined, factory.createJsxAttributes([]))
1396
- ts.setParentRecursive(specialization.root, false)
1397
- specialization.root.parent = call.parent
1398
- }
1545
+ if (specialization.hookDeclarations.length || specialization.effects.length) materializeComponentHelper(call, specialization, "KSetterComponent", false)
1399
1546
  componentSpecializations.set(call, specialization)
1400
1547
  }
1401
1548
  for (const [name, component] of components) {
@@ -1702,7 +1849,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1702
1849
  : collectionSymbol !== undefined ? { kind: "symbol", symbol: collectionSymbol } : { kind: "static" }
1703
1850
  if (listParts.calculation) {
1704
1851
  usesBinding = true
1705
- const compiled = descriptors.compileReactiveBinding(listParts.calculation, { setters: settersForNode(node, settersByFunction), importBindings, keyedBlock: blockSlot })
1852
+ const compiled = descriptors.compileReactiveBinding(listParts.calculation, { setters: settersForNode(node, settersByFunction), importBindings, keyedBlock: blockSlot, derived: calculationBinding(node, nearestFunction(node)) })
1706
1853
  listSource = compiled.node
1707
1854
  collection = { kind: "binding", binding: compiled.binding }
1708
1855
  }
@@ -1747,6 +1894,8 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1747
1894
  return factory.updateJsxExpression(node, factory.createCallExpression(factory.createIdentifier("__kList"), undefined, arguments_))
1748
1895
  }
1749
1896
  const visitor = node => {
1897
+ const outsideClick = ts.isCallExpression(node) && outsideClickCalls.get(node)
1898
+ if (outsideClick) return factory.updateCallExpression(node, node.expression, node.typeArguments, [node.arguments[0], factory.createIdentifier(outsideClick.setter), cloneAst(outsideClick.value, factory, context)])
1750
1899
  if (ts.isJsxAttribute(node) && contextProviderPrivateSetters.has(node)) {
1751
1900
  const expression = unwrapExpression(node.initializer.expression)
1752
1901
  const value = factory.updateObjectLiteralExpression(expression, [...expression.properties, ...contextProviderPrivateSetters.get(node).map(name => factory.createShorthandPropertyAssignment(name))])
@@ -1825,6 +1974,26 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1825
1974
  if (callback.parameters.length) effectFail(callback, "useEffect() callback cannot declare parameters")
1826
1975
  if (!ts.isArrayLiteralExpression(dependencies)) effectFail(dependencies, "useEffect() dependencies must be a literal array")
1827
1976
  const setters = settersForNode(node, settersByFunction)
1977
+ const resolveCalculation = dependency => {
1978
+ if (specializedEffect) return undefined
1979
+ const value = unwrapExpression(dependency)
1980
+ const result = ts.isIdentifier(value) ? value : ts.isPropertyAccessExpression(value) || ts.isElementAccessExpression(value) ? unwrapExpression(value.expression) : undefined
1981
+ if (!result || !ts.isIdentifier(result)) return undefined
1982
+ const entries = jsxLocalDeclarations.get(effectOwner)?.get(result.text)
1983
+ const initializer = entries?.length === 1 && entries[0].node.parent?.parent?.parent === effectOwner?.body ? unwrapExpression(entries[0].initializer) : undefined
1984
+ if (!initializer || !ts.isCallExpression(initializer) || !ts.isIdentifier(initializer.expression) || !importBindings.has(initializer.expression.text)) return undefined
1985
+ if (ts.isIdentifier(value)) effectFail(value, `useEffect() cannot depend on the whole imported calculation result ${JSON.stringify(value.text)}; select one JSON-safe primitive field such as ${value.text}.id`)
1986
+ if (ts.isElementAccessExpression(value)) effectFail(value, `useEffect() imported calculation dependencies require one direct static result field such as ${result.text}.id; computed result properties are not supported`)
1987
+ if (!ts.isPropertyAccessExpression(value) || ["__proto__", "constructor", "prototype"].includes(value.name.text)) effectFail(value, "useEffect() imported calculation dependency field must not be __proto__, prototype, or constructor")
1988
+ validateSelectedEffectCalculation(initializer, value.name.text)
1989
+ const expanded = unwrapExpression(resolveReactiveJsxExpression(dependency, effectOwner, setters))
1990
+ if (!ts.isPropertyAccessExpression(expanded) || expanded.name.text !== value.name.text) return undefined
1991
+ const call = unwrapExpression(expanded.expression)
1992
+ if (!ts.isCallExpression(call) || !ts.isIdentifier(call.expression) || !importBindings.has(call.expression.text)) return undefined
1993
+ const states = referencedStateNames(call, setters, call, bindingIndex)
1994
+ if (states.size < 2 || call.arguments.some(argument => !ts.isIdentifier(unwrapExpression(argument)) || !states.has(unwrapExpression(argument).text))) effectFail(call, "useEffect() selected calculations require at least two direct primitive state arguments")
1995
+ return { name: value.expression.text, field: value.name.text, call, states, source: dependency }
1996
+ }
1828
1997
  const dependencyAnalysis = analyzeEffectDependencies({
1829
1998
  dependencies,
1830
1999
  node,
@@ -1834,9 +2003,12 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1834
2003
  localDeclarations: jsxLocalDeclarations.get(nearestFunction(node)),
1835
2004
  factory,
1836
2005
  fail: effectFail,
1837
- bindingIndex
2006
+ bindingIndex,
2007
+ resolveCalculation
1838
2008
  })
1839
2009
  const { dependencyItem, itemDependencies, ordinaryDependencies, entries: dependencyEntries, dependencyStates, substitutions: dependencySubstitutions, subscriptions: subscriptionDependencies, hasDerived: hasDerivedDependency } = dependencyAnalysis
2010
+ const calculationEntries = dependencyEntries.filter(entry => entry.kind === "calculation")
2011
+ if (calculationEntries.length && (calculationEntries.length !== 1 || dependencyEntries.length !== 1)) effectFail(dependencies, "useEffect() selected calculation fields must be the only dependency")
1840
2012
  if (!effectOwner) fail(node, "useEffect() cannot be used outside a Kudzu component")
1841
2013
  if (!ts.isBlock(callback.body)) effectFail(callback, "useEffect() callback must use a block body")
1842
2014
  const cleanupSubstitutions = new Map()
@@ -1860,6 +2032,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1860
2032
  if (invalidCleanup) effectFail(invalidCleanup, "useEffect() cleanup functions cannot declare parameters or be generators")
1861
2033
  if (returns.cleanup && callback.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) effectFail(callback, "useEffect() async callbacks cannot return cleanup functions")
1862
2034
  validateEffectOwnedBrowserResources(callback, returns, effectFail, bindingIndex)
2035
+ const calculationEvaluators = dependencyEntries.map(entry => entry.kind === "calculation" ? descriptors.compileDerivedEvaluator(entry.call, { setters, importBindings }) : undefined)
1863
2036
  const callbackSource = specializedEffect?.sourceFile ?? sourceFile
1864
2037
  const callbackFile = callbackSource.fileName
1865
2038
  let compiledCallback = dependencySubstitutions.size ? substituteClone(callback, dependencySubstitutions, factory, context) : callback
@@ -1889,7 +2062,16 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1889
2062
  })
1890
2063
  usesListItem ||= Boolean(itemDependencies.length && !listEffect)
1891
2064
  usesBehavior = true
1892
- const derivedDependencies = hasDerivedDependency ? dependencyEntries.map(entry => entry.kind === "derived" ? descriptors.registerDerived("expression", entry.expression, entry.states, entry.source) : undefined) : []
2065
+ const derivedDependencies = hasDerivedDependency ? dependencyEntries.map((entry, index) => entry.kind === "derived"
2066
+ ? descriptors.registerDerived("expression", entry.expression, entry.states, entry.source)
2067
+ : entry.kind === "calculation" ? descriptors.registerDerived("calculation", { binding: calculationEvaluators[index].binding, fields: [entry.field] }, entry.states, entry.source) : undefined) : []
2068
+ if (calculationEntries.length) {
2069
+ const calculations = calculationDerivedByOwner.get(effectOwner) ?? new Map()
2070
+ dependencyEntries.forEach((entry, index) => {
2071
+ if (entry.kind === "calculation") calculations.set(entry.name, derivedDependencies[index])
2072
+ })
2073
+ calculationDerivedByOwner.set(effectOwner, calculations)
2074
+ }
1893
2075
  const effectSource = specializedEffect?.source ?? node
1894
2076
  const lexicalOwner = nearestFunction(effectSource)
1895
2077
  const effectStateOwners = new Map([...stateOwnersForNode(callbackArgument), ...stateOwnersForNode(node), ...(specializedEffectStateOwners.get(node)?.references ?? []), ...(activeStateOwners ?? [])])
@@ -1898,7 +2080,9 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1898
2080
  const dependencyStateNames = [...dependencyStates.keys()]
1899
2081
  const effect = descriptors.registerEffect(descriptor, {
1900
2082
  cleanup: returns.cleanup,
1901
- dependencies: hasDerivedDependency ? dependencyEntries.map((entry, index) => entry.kind === "derived" ? { kind: "derived", derived: derivedDependencies[index].slot, sources: [...entry.states].map(signalFor) } : { kind: "signal", signal: signalFor(entry.name) }) : ordinaryDependencies.map(dependency => ({ kind: "signal", signal: signalFor(dependency.text) })),
2083
+ dependencies: hasDerivedDependency ? dependencyEntries.map((entry, index) => entry.kind === "derived"
2084
+ ? { kind: "derived", derived: derivedDependencies[index].slot, sources: [...entry.states].map(signalFor) }
2085
+ : entry.kind === "calculation" ? { kind: "derived", derived: derivedDependencies[index].slot, sources: [...entry.states].map(signalFor), field: entry.field, evaluator: calculationEvaluators[index].binding } : { kind: "signal", signal: signalFor(entry.name) }) : ordinaryDependencies.map(dependency => ({ kind: "signal", signal: signalFor(dependency.text) })),
1902
2086
  subscriptions: subscriptionNames.map(signalFor),
1903
2087
  dependencySignals: dependencyStateNames.map(signalFor),
1904
2088
  itemDependencies,
@@ -1912,7 +2096,12 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1912
2096
  ...(analysisSite(effectSource, "hook") ? { site: analysisSite(effectSource, "hook") } : {}),
1913
2097
  ...(analysisSource(effectSource) ? { source: analysisSource(effectSource) } : {})
1914
2098
  })
1915
- const dependencyExpressions = effect.dependencies.map((dependency, index) => dependency.kind === "derived" ? moduleIR.derived[dependency.derived].expression : ["state", dependencyEntries[index]?.name ?? ordinaryDependencies[index].text])
2099
+ const hasExpressionDependency = dependencyEntries.some(entry => entry.kind === "derived")
2100
+ const dependencyExpressions = hasExpressionDependency ? effect.dependencies.map((dependency, index) => dependency.kind === "derived" ? moduleIR.derived[dependency.derived].expression : ["state", dependencyEntries[index]?.name ?? ordinaryDependencies[index].text]) : []
2101
+ const dependencyEvaluators = calculationEntries.length ? calculationEvaluators.map((evaluator, index) => evaluator ? factory.createObjectLiteralExpression([
2102
+ factory.createSpreadAssignment(evaluator.descriptor),
2103
+ factory.createPropertyAssignment("field", factory.createStringLiteral(dependencyEntries[index].field))
2104
+ ]) : factory.createNull()) : []
1916
2105
  const buildCallback = [...packageBindings].some(([name]) => referenceIdentifiers(callback, name).length)
1917
2106
  ? factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), factory.createBlock([], false))
1918
2107
  : callback
@@ -1926,8 +2115,9 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1926
2115
  factory.createStringLiteral(specializedEffect ? sourceLocation(specializedEffect.source, specializedEffect.sourceFile) : sourceLocation(node, sourceFile)),
1927
2116
  effect.cleanup ? factory.createTrue() : factory.createFalse(),
1928
2117
  factory.createArrayLiteralExpression(effect.itemDependencies.map(field => factory.createStringLiteral(field))),
1929
- hasDerivedDependency ? jsonExpression(dependencyExpressions, factory) : factory.createArrayLiteralExpression(),
1930
- factory.createArrayLiteralExpression(dependencyStateNames.map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), factory.createIdentifier(name)])))
2118
+ dependencyExpressions.length ? jsonExpression(dependencyExpressions, factory) : factory.createArrayLiteralExpression(),
2119
+ factory.createArrayLiteralExpression(dependencyStateNames.map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), factory.createIdentifier(name)]))),
2120
+ factory.createArrayLiteralExpression(dependencyEvaluators)
1931
2121
  ])
1932
2122
  }
1933
2123
 
@@ -2004,7 +2194,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
2004
2194
  if ((usedStates.size || captures.size) && !ts.isIdentifier(expression) && !containsJsx(expression)) {
2005
2195
  usesBehavior = true
2006
2196
  usesBinding = true
2007
- return factory.updateJsxExpression(node, descriptors.compileReactiveBinding(expression, { setters, importBindings }).node)
2197
+ return factory.updateJsxExpression(node, descriptors.compileReactiveBinding(expression, { setters, importBindings, derived: calculationBinding(node.expression, nearestFunction(node)) }).node)
2008
2198
  }
2009
2199
  }
2010
2200
 
@@ -2017,7 +2207,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
2017
2207
  if ((usedStates.size || captures.size) && !ts.isIdentifier(expression)) {
2018
2208
  usesBehavior = true
2019
2209
  usesBinding = true
2020
- const compiled = descriptors.compileReactiveBinding(expression, { setters, importBindings })
2210
+ const compiled = descriptors.compileReactiveBinding(expression, { setters, importBindings, derived: calculationBinding(sourceExpression, nearestFunction(node)) })
2021
2211
  return factory.updateJsxAttribute(node, node.name, factory.createJsxExpression(undefined, compiled.node))
2022
2212
  }
2023
2213
  }
@@ -2046,6 +2236,27 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
2046
2236
  return ts.visitEachChild(node, visitor, context)
2047
2237
  }
2048
2238
 
2239
+ function calculationBinding(expression, owner) {
2240
+ const calculations = calculationDerivedByOwner.get(owner)
2241
+ if (!calculations?.size) return undefined
2242
+ const fields = new Map()
2243
+ const visit = node => {
2244
+ const value = unwrapExpression(node)
2245
+ if (ts.isPropertyAccessExpression(value) && ts.isIdentifier(value.expression) && calculations.has(value.expression.text)) {
2246
+ const derived = calculations.get(value.expression.text)
2247
+ const entry = fields.get(derived.slot) ?? { derived, fields: new Set() }
2248
+ entry.fields.add(value.name.text)
2249
+ fields.set(derived.slot, entry)
2250
+ }
2251
+ ts.forEachChild(node, visit)
2252
+ }
2253
+ visit(expression)
2254
+ if (fields.size !== 1) return undefined
2255
+ const [{ derived, fields: selected }] = fields.values()
2256
+ for (const field of selected) if (!derived.calculation.fields.includes(field)) derived.calculation.fields.push(field)
2257
+ return { derived: derived.slot, fields: [...selected] }
2258
+ }
2259
+
2049
2260
  const transformed = ts.visitNode(sourceFile, visitor)
2050
2261
  descriptors.finalize()
2051
2262
  if (!usesBehavior) return transformed
@@ -2181,7 +2392,7 @@ function jsonExpression(value, factory) {
2181
2392
  return factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("JSON"), "parse"), undefined, [factory.createStringLiteral(JSON.stringify(value))])
2182
2393
  }
2183
2394
 
2184
- function isStateBackedListComponentCall(call, component, setters) {
2395
+ function isStateBackedListComponentCall(call, component, setters, propertyOnly = false) {
2185
2396
  if (component.parameters.length !== 1 || !ts.isObjectBindingPattern(component.parameters[0].name)) return false
2186
2397
  const attributes = ts.isJsxElement(call) ? call.openingElement.attributes : call.attributes
2187
2398
  const stateNames = new Set(setters.values())
@@ -2201,7 +2412,9 @@ function isStateBackedListComponentCall(call, component, setters) {
2201
2412
  let found = false
2202
2413
  const visit = node => {
2203
2414
  if (found || node !== returned && isFunctionLike(node)) return
2204
- if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "map" && ts.isIdentifier(node.expression.expression) && mappedProps.has(node.expression.expression.text)) {
2415
+ const collection = ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "map" ? unwrapExpression(node.expression.expression) : undefined
2416
+ const root = collection && ts.isPropertyAccessExpression(collection) ? unwrapExpression(collection.expression) : collection
2417
+ if (root && ts.isIdentifier(root) && mappedProps.has(root.text) && (!propertyOnly || ts.isPropertyAccessExpression(collection))) {
2205
2418
  found = true
2206
2419
  return
2207
2420
  }
@@ -2211,6 +2424,75 @@ function isStateBackedListComponentCall(call, component, setters) {
2211
2424
  return found
2212
2425
  }
2213
2426
 
2427
+ function referencesComponentProperty(expression, locals) {
2428
+ let found = false
2429
+ const visit = node => {
2430
+ if (found) return
2431
+ if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(unwrapExpression(node.expression)) && locals.has(unwrapExpression(node.expression).text)) {
2432
+ found = true
2433
+ return
2434
+ }
2435
+ ts.forEachChild(node, visit)
2436
+ }
2437
+ visit(expression)
2438
+ return found
2439
+ }
2440
+
2441
+ function componentPropertyMutation(component, locals) {
2442
+ let invalid
2443
+ const root = expression => {
2444
+ let value = unwrapExpression(expression)
2445
+ while (ts.isPropertyAccessExpression(value) || ts.isElementAccessExpression(value)) value = unwrapExpression(value.expression)
2446
+ return ts.isIdentifier(value) ? value.text : undefined
2447
+ }
2448
+ const visit = node => {
2449
+ if (invalid) return
2450
+ if (ts.isBinaryExpression(node) && node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && node.operatorToken.kind <= ts.SyntaxKind.LastAssignment && locals.has(root(node.left))) invalid = node
2451
+ else if ((ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node)) && locals.has(root(node.operand))) invalid = node
2452
+ else if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && mutatingListMethods.has(node.expression.name.text) && locals.has(root(node.expression.expression))) invalid = node
2453
+ if (!invalid) ts.forEachChild(node, visit)
2454
+ }
2455
+ visit(component)
2456
+ return invalid
2457
+ }
2458
+
2459
+ function prependJsxChild(root, child, factory) {
2460
+ if (ts.isJsxElement(root)) return factory.updateJsxElement(root, root.openingElement, [child, ...root.children], root.closingElement)
2461
+ const opening = factory.createJsxOpeningElement(root.tagName, root.typeArguments, root.attributes)
2462
+ const closing = factory.createJsxClosingElement(root.tagName)
2463
+ return factory.createJsxElement(opening, [child], closing)
2464
+ }
2465
+
2466
+ function componentPropertyUses(component, returned, effectCalls) {
2467
+ const locals = new Set(component.parameters[0].name.elements.filter(element => !element.dotDotDotToken && ts.isIdentifier(element.name)).map(element => element.name.text))
2468
+ const uses = new Map()
2469
+ const add = (local, field, consumer) => {
2470
+ if (["__proto__", "constructor", "prototype"].includes(field)) return
2471
+ const fields = uses.get(local) ?? new Map()
2472
+ const consumers = fields.get(field) ?? new Set()
2473
+ consumers.add(consumer)
2474
+ fields.set(field, consumers)
2475
+ uses.set(local, fields)
2476
+ }
2477
+ for (const call of effectCalls) {
2478
+ const dependencies = call.arguments[1]
2479
+ if (!ts.isArrayLiteralExpression(dependencies)) continue
2480
+ for (const dependency of dependencies.elements) {
2481
+ const value = unwrapExpression(dependency)
2482
+ if (ts.isPropertyAccessExpression(value) && ts.isIdentifier(unwrapExpression(value.expression)) && locals.has(unwrapExpression(value.expression).text)) add(unwrapExpression(value.expression).text, value.name.text, "effect")
2483
+ }
2484
+ }
2485
+ const visit = node => {
2486
+ if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(unwrapExpression(node.expression)) && locals.has(unwrapExpression(node.expression).text)) {
2487
+ const map = ts.isPropertyAccessExpression(node.parent) && node.parent.expression === node && node.parent.name.text === "map" && ts.isCallExpression(node.parent.parent)
2488
+ add(unwrapExpression(node.expression).text, node.name.text, map ? "list" : "binding")
2489
+ }
2490
+ ts.forEachChild(node, visit)
2491
+ }
2492
+ visit(returned)
2493
+ return new Map([...uses].map(([local, fields]) => [local, [...fields].map(([field, consumers]) => ({ path: [field], consumers: ["binding", "effect", "list"].filter(consumer => consumers.has(consumer)) }))]))
2494
+ }
2495
+
2214
2496
  function jsxCallHasDirectStateProp(call, setters) {
2215
2497
  const attributes = ts.isJsxElement(call) ? call.openingElement.attributes : call.attributes
2216
2498
  const stateNames = new Set(setters.values())
@@ -2238,12 +2520,30 @@ function componentHasDirectPropStateInitializer(component) {
2238
2520
  return component.body.statements.some(statement => ts.isVariableStatement(statement) && statement.declarationList.declarations.some(declaration => declaration.initializer && ts.isCallExpression(declaration.initializer) && ts.isIdentifier(declaration.initializer.expression) && declaration.initializer.expression.text === "useState" && declaration.initializer.arguments.length === 1 && ts.isIdentifier(declaration.initializer.arguments[0]) && props.has(declaration.initializer.arguments[0].text)))
2239
2521
  }
2240
2522
 
2241
- function componentHasDirectPrimitiveState(component, state) {
2242
- return Boolean(component && ts.isBlock(component.body) && component.body.statements.some(statement => ts.isVariableStatement(statement) && statement.declarationList.declarations.some(declaration => ts.isArrayBindingPattern(declaration.name) && ts.isIdentifier(declaration.name.elements[0]?.name) && declaration.name.elements[0].name.text === state && declaration.initializer && ts.isCallExpression(declaration.initializer) && ts.isIdentifier(declaration.initializer.expression) && declaration.initializer.expression.text === "useState" && declaration.initializer.arguments.length === 1 && isPrimitiveDefaultLiteral(unwrapExpression(declaration.initializer.arguments[0])))))
2523
+ function componentHasDirectObjectRef(component, ref) {
2524
+ if (!component || !ts.isBlock(component.body)) return false
2525
+ const declaration = component.body.statements.some(statement => ts.isVariableStatement(statement) && statement.declarationList.declarations.some(entry => ts.isIdentifier(entry.name) && entry.name.text === ref && entry.initializer && ts.isCallExpression(entry.initializer) && ts.isIdentifier(entry.initializer.expression) && entry.initializer.expression.text === "useRef" && entry.initializer.arguments.length === 1 && entry.initializer.arguments[0].kind === ts.SyntaxKind.NullKeyword))
2526
+ let attachments = 0
2527
+ let intrinsic = 0
2528
+ const visit = node => {
2529
+ if (ts.isJsxAttribute(node) && node.name.text === "ref" && node.initializer && ts.isJsxExpression(node.initializer) && ts.isIdentifier(node.initializer.expression) && node.initializer.expression.text === ref) {
2530
+ attachments++
2531
+ const element = node.parent?.parent
2532
+ const tag = ts.isJsxOpeningElement(element) || ts.isJsxSelfClosingElement(element) ? element.tagName : undefined
2533
+ if (ts.isIdentifier(tag) && tag.text[0] === tag.text[0].toLowerCase()) intrinsic++
2534
+ }
2535
+ ts.forEachChild(node, visit)
2536
+ }
2537
+ visit(component.body)
2538
+ return declaration && attachments === 1 && intrinsic === 1
2243
2539
  }
2244
2540
 
2245
- function componentHasDirectArrayState(component, state) {
2246
- return Boolean(component && ts.isBlock(component.body) && component.body.statements.some(statement => ts.isVariableStatement(statement) && statement.declarationList.declarations.some(declaration => ts.isArrayBindingPattern(declaration.name) && ts.isIdentifier(declaration.name.elements[0]?.name) && declaration.name.elements[0].name.text === state && declaration.initializer && ts.isCallExpression(declaration.initializer) && ts.isIdentifier(declaration.initializer.expression) && declaration.initializer.expression.text === "useState" && declaration.initializer.arguments.length === 1 && ts.isArrayLiteralExpression(unwrapExpression(declaration.initializer.arguments[0])))))
2541
+ function directSetterLiteralCallback(node, setters) {
2542
+ node = node && unwrapExpression(node)
2543
+ if ((!ts.isArrowFunction(node) && !ts.isFunctionExpression(node)) || node.parameters.length || node.asteriskToken || node.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) return undefined
2544
+ const expression = ts.isBlock(node.body) ? node.body.statements.length === 1 && ts.isExpressionStatement(node.body.statements[0]) ? node.body.statements[0].expression : undefined : node.body
2545
+ if (!expression || !ts.isCallExpression(expression) || !ts.isIdentifier(expression.expression) || !setters.has(expression.expression.text) || expression.arguments.length !== 1 || !isPrimitiveDefaultLiteral(unwrapExpression(expression.arguments[0]))) return undefined
2546
+ return { setter: expression.expression.text, value: unwrapExpression(expression.arguments[0]) }
2247
2547
  }
2248
2548
 
2249
2549
  function directSetterPropEffect(component, setterProp) {
@@ -2597,6 +2897,7 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
2597
2897
  }
2598
2898
  const substitutions = new Map()
2599
2899
  const acceptedProps = new Set()
2900
+ const propLocals = new Set()
2600
2901
  let rest
2601
2902
  const elements = component.parameters[0].name.elements
2602
2903
  for (const [index, element] of elements.entries()) {
@@ -2609,10 +2910,15 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
2609
2910
  if (element.initializer && !isSerializableStateLiteral(element.initializer)) fail(element.initializer, `${label} component prop defaults must be directly serializable primitive, plain-object, or array literals`)
2610
2911
  const prop = (element.propertyName ?? element.name).text
2611
2912
  acceptedProps.add(prop)
2913
+ propLocals.add(element.name.text)
2612
2914
  substitutions.set(element.name.text, props.has(prop) ? props.get(prop) : element.initializer ?? factory.createIdentifier("undefined"))
2613
2915
  }
2614
2916
  const restEntries = [...props].filter(([prop]) => !acceptedProps.has(prop))
2615
2917
  if (!rest) for (const [prop] of restEntries) fail(call, `Unknown ${label.toLowerCase()} component prop "${prop}"`)
2918
+ if (label === "Object-property component") {
2919
+ const mutation = componentPropertyMutation(component, propLocals)
2920
+ if (mutation) fail(mutation, "Object-property component props must remain immutable")
2921
+ }
2616
2922
  const propAnalysis = elements.map(element => ({
2617
2923
  name: (element.propertyName ?? element.name).getText(),
2618
2924
  local: element.name.getText(),
@@ -2650,7 +2956,7 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
2650
2956
  const substitutedProp = propReceiver ? substitutions.get(propReceiver.text) : undefined
2651
2957
  const substitutedState = substitutedProp && ts.isIdentifier(unwrapExpression(substitutedProp)) ? unwrapExpression(substitutedProp).text : undefined
2652
2958
  const directProp = ts.isIdentifier(initialArgument)
2653
- const parentInitializer = substitutedState ? directStateInitializer(call, substitutedState) : undefined
2959
+ const parentInitializer = substitutedState ? directStateInitializer(nearestFunction(call), substitutedState) : undefined
2654
2960
  const propInitializer = ordinaryHooks && substitutedState && ordinaryStateNames.has(substitutedState) && parentInitializer && (isPrimitiveDefaultLiteral(parentInitializer) || directProp && (ts.isObjectLiteralExpression(parentInitializer) || ts.isArrayLiteralExpression(parentInitializer)))
2655
2961
  const rowItemProp = propReceiver && elements.find(element => !element.dotDotDotToken && ts.isIdentifier(element.name) && element.name.text === propReceiver.text)
2656
2962
  const rowItemInitializer = !ordinaryHooks && directProp && substitutedState && rowItemProp && directProps.has((rowItemProp.propertyName ?? rowItemProp.name).text)
@@ -2702,6 +3008,7 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
2702
3008
  continue
2703
3009
  }
2704
3010
  if (!ts.isIdentifier(declaration.name) || !declaration.initializer) fail(declaration, `${label} component locals must be initialized identifiers`)
3011
+ if (label === "Object-property component" && referencesComponentProperty(declaration.initializer, propLocals)) fail(declaration.initializer, "Object-property component property aliases are not supported; read the static prop.field path directly at each consumer")
2705
3012
  const calculation = substituteClone(declaration.initializer, substitutions, factory, context)
2706
3013
  calculations.push({ name: declaration.name.text, expression: calculation })
2707
3014
  substitutions.set(declaration.name.text, calculation)
@@ -2740,6 +3047,7 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
2740
3047
  ordinaryStates,
2741
3048
  ordinaryRefs,
2742
3049
  ordinaryIds,
3050
+ propertyUses: componentPropertyUses(component, returned, effectCalls),
2743
3051
  propExpressions: props,
2744
3052
  props: propAnalysis,
2745
3053
  usesComponentId: ordinaryIds.length > 0
@@ -2754,8 +3062,7 @@ function isSerializableStateLiteral(node) {
2754
3062
  return value.properties.every(property => ts.isPropertyAssignment(property) && !ts.isComputedPropertyName(property.name) && property.name.text !== "__proto__" && isSerializableStateLiteral(property.initializer))
2755
3063
  }
2756
3064
 
2757
- function directStateInitializer(call, name) {
2758
- const owner = nearestFunction(call)
3065
+ function directStateInitializer(owner, name) {
2759
3066
  if (!owner || !ts.isBlock(owner.body)) return
2760
3067
  for (const statement of owner.body.statements) {
2761
3068
  if (!ts.isVariableStatement(statement)) continue
@@ -3086,10 +3393,34 @@ function importedSerializableCollectionNames(sourceFile, file, sourceFiles, sour
3086
3393
  return new Set(importedSerializableCollections(sourceFile, file, sourceFiles, sourceIndex).keys())
3087
3394
  }
3088
3395
 
3089
- function importedSerializableCollections(sourceFile, file, sourceFiles, sourceIndex) {
3396
+ function importedSerializableCollections(sourceFile, file, sourceFiles, sourceIndex, onlyMapped = false) {
3090
3397
  const collections = new Map()
3398
+ const mapped = new Set()
3399
+ const memoNames = new Set(["useMemo"])
3400
+ for (const statement of sourceFile.statements) if (ts.isImportDeclaration(statement) && ["react", "@kudzujs/core"].includes(statement.moduleSpecifier.text) && statement.importClause?.namedBindings && ts.isNamedImports(statement.importClause.namedBindings)) {
3401
+ for (const entry of statement.importClause.namedBindings.elements) if ((entry.propertyName ?? entry.name).text === "useMemo") memoNames.add(entry.name.text)
3402
+ }
3403
+ const collectRoot = node => {
3404
+ if (ts.isPropertyAccessExpression(node) && ["filter", "flatMap", "slice", "toSorted", "map"].includes(node.name.text)) {
3405
+ let expression = node.expression
3406
+ while (ts.isCallExpression(expression) || ts.isPropertyAccessExpression(expression)) expression = expression.expression
3407
+ if (ts.isIdentifier(expression)) mapped.add(expression.text)
3408
+ }
3409
+ ts.forEachChild(node, collectRoot)
3410
+ }
3411
+ const visit = node => {
3412
+ if (ts.isPropertyAccessExpression(node) && node.name.text === "map") {
3413
+ collectRoot(node)
3414
+ }
3415
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && memoNames.has(node.expression.text) && node.arguments[0]) collectRoot(node.arguments[0])
3416
+ ts.forEachChild(node, visit)
3417
+ }
3418
+ if (onlyMapped) {
3419
+ visit(sourceFile)
3420
+ if (!mapped.size) return collections
3421
+ }
3091
3422
  for (const [name, binding] of clientImportBindings(sourceFile, file, sourceFiles)) {
3092
- if (binding.kind !== "named") continue
3423
+ if (binding.kind !== "named" || onlyMapped && !mapped.has(name)) continue
3093
3424
  const imported = parseSourceFile(binding.target, sourceIndex.get(binding.target))
3094
3425
  for (const statement of imported.statements) {
3095
3426
  if (!ts.isVariableStatement(statement) || !(statement.declarationList.flags & ts.NodeFlags.Const) || !statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword)) continue
@@ -3227,7 +3558,9 @@ function orderSourceStyles(entryFiles, sourceFiles, sourceIndex, staticFiles) {
3227
3558
  const visit = file => {
3228
3559
  if (seenSources.has(file)) return
3229
3560
  seenSources.add(file)
3230
- const sourceFile = parseSourceFile(file, sourceIndex.get(file))
3561
+ const source = sourceIndex.get(file)
3562
+ if (importFreeTypeScriptModule(file, source)) return
3563
+ const sourceFile = parseSourceFile(file, source)
3231
3564
  for (const statement of sourceFile.statements) {
3232
3565
  if ((!ts.isImportDeclaration(statement) && !ts.isExportDeclaration(statement)) || !runtimeModuleReference(statement) || !statement.moduleSpecifier || !ts.isStringLiteral(statement.moduleSpecifier) || !statement.moduleSpecifier.text.startsWith(".")) continue
3233
3566
  const specifier = statement.moduleSpecifier.text
@@ -3338,7 +3671,7 @@ const printHandlerModule = createHandlerCodegen({
3338
3671
  const { normalizeReactMigrationSyntax, validateUseIdSyntax } = createReactMigrationPass({ cloneAst, jsxTagName })
3339
3672
  const normalizeReactRouterSyntax = createRouterPass({ withBase })
3340
3673
 
3341
- return { collectClientModules, compileClientModule, compiledPath, compileSource, layoutExportError, orderSourceStyles, reachableSourceFiles, safeStaticFiles }
3674
+ return { collectClientModules, compileClientModule, compiledPath, compileSource, compileSourceAsync, layoutExportError, orderSourceStyles, reachableSourceFiles, safeStaticFiles }
3342
3675
  }
3343
3676
 
3344
3677
  const currentCompiler = sourceIndex => createSourceCompiler(createProjectSession(process.cwd(), { sourceIndex }))