@kudzujs/core 0.8.17 → 0.8.19

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.
@@ -4,6 +4,7 @@ import { dirname, extname, isAbsolute, join, relative, resolve, sep } from "node
4
4
  import { pathToFileURL } from "node:url"
5
5
  import { build as bundle, transform } from "esbuild"
6
6
  import ts from "typescript"
7
+ import { createComponentAnalysisSession } from "./compiler/analysis/component-analysis.mjs"
7
8
  import { normalizeEffectAnimationFrameRefs } from "./compiler/animation-frame-pass.mjs"
8
9
  import { bindingNames, containsJsx, effectReturns, functionVarDeclaresName, importDeclarationNames, isFunctionLike, isLocalConst, isReferenceIdentifier, isShadowedByParameter, isShadowedIdentifier, isUnshadowedGlobal, nearestFunction, referenceIdentifiers, referencesIdentifier, sourceLocation, sourceNodeError, statementDeclaresName, unwrapExpression } from "./compiler/ast-helpers.mjs"
9
10
  import { normalizeMediaQueryExternalStores, normalizeNavigatorCapabilityConditions } from "./compiler/browser-signal-passes.mjs"
@@ -12,6 +13,7 @@ import { normalizeCustomHookTimerRefs } from "./compiler/custom-hook-timer-pass.
12
13
  import { captureNames, createDescriptorSession, createSemanticArtifact, nativeCaptureNames, referencedReducerDispatches, referencedStateNames } from "./compiler/descriptor-session.mjs"
13
14
  import { createEffectCodegen } from "./compiler/effect-codegen.mjs"
14
15
  import { createHandlerCodegen } from "./compiler/handler-codegen.mjs"
16
+ import { createHandlerLowering } from "./compiler/handler-lowering.mjs"
15
17
  import { createCommandSpecializer } from "./compiler/optimize/command-specialization.mjs"
16
18
  import { applyNormalizationPasses } from "./compiler/normalization-pipeline.mjs"
17
19
  import { createReactMigrationPass, reactMemoExpression } from "./compiler/react-migration-pass.mjs"
@@ -72,13 +74,13 @@ export async function build({ quiet = false, minify = true } = {}) {
72
74
  const importedAssets = new Set()
73
75
  const { cssModules, cssOutputs } = await prepareSourceStyles(cssFiles, staticFiles, importedAssets, base)
74
76
 
75
- const handlerModules = []
77
+ const sourceResults = []
76
78
  const workerReferences = []
77
79
  for (const file of sourceFiles) {
78
80
  if (file.endsWith(".worker.ts")) continue
79
- const handlerModule = await compile(file, sourceFileSet, sourceIndex, staticFiles, importedAssets, cssModules, base, workerReferences)
80
- if (handlerModule) handlerModules.push(handlerModule)
81
+ sourceResults.push(await compile(file, sourceFileSet, sourceIndex, staticFiles, importedAssets, cssModules, base, workerReferences))
81
82
  }
83
+ const handlerModules = sourceResults.flatMap(result => result.handlerModule ? [result.handlerModule] : [])
82
84
 
83
85
  const plans = []
84
86
  const routeCapabilities = new Map()
@@ -466,6 +468,7 @@ export async function build({ quiet = false, minify = true } = {}) {
466
468
  }
467
469
 
468
470
  if (!quiet) console.log(`Built ${plans.length} page(s), ${behaviorCount} interactive page(s) into dist/`)
471
+ return { sourceResults }
469
472
  }
470
473
 
471
474
  function preloadModules(html) {
@@ -702,17 +705,19 @@ async function compile(file, sourceFiles, sourceIndex, staticFiles, importedAsse
702
705
  await mkdir(resolve(output, ".."), { recursive: true })
703
706
  await writeFile(output, result.outputText)
704
707
 
705
- const { nativeHandlers, effectHandlers, reactiveBindings, listExpressions, clientImports } = semantic
706
- if (!nativeHandlers.length && !effectHandlers.length && !reactiveBindings.length && !listExpressions.length) return undefined
707
- const callbacks = [...nativeHandlers, ...effectHandlers]
708
- const moduleSource = printHandlerModule({ callbacks, reactiveBindings, listExpressions, handlerPath })
708
+ const { componentAnalysis, moduleIR } = semantic
709
+ const sourceResult = { file: relative(root, file).replaceAll(sep, "/"), componentAnalysis, moduleIR }
710
+ const moduleHandlers = moduleIR.handlers.filter(handler => handler.kind === "module-export")
711
+ if (!moduleHandlers.length && !moduleIR.bindings.length) return sourceResult
712
+ const moduleSource = printHandlerModule({ moduleIR, handlerPath })
709
713
  const moduleResult = ts.transpileModule(moduleSource, {
710
714
  compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext },
711
715
  reportDiagnostics: true
712
716
  })
713
717
  const moduleErrors = moduleResult.diagnostics?.filter(diagnostic => diagnostic.category === ts.DiagnosticCategory.Error) ?? []
714
718
  if (moduleErrors.length) throw new Error(moduleErrors.map(error => ts.flattenDiagnosticMessageText(error.messageText, "\n")).join("\n"))
715
- return { path: handlerPath, code: moduleResult.outputText, hasNativeHandlers: nativeHandlers.length > 0, hasEffects: effectHandlers.length > 0, clientImports: [...clientImports], hasPackageImports: [...callbacks, ...reactiveBindings].some(entry => entry.imports?.some(import_ => import_.package)) }
719
+ 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) }
720
+ return sourceResult
716
721
  }
717
722
 
718
723
  function emittedPackageReference(source, file, packages) {
@@ -878,7 +883,7 @@ function normalizeCompilerSource(sourceFile, { base, context, file, importedColl
878
883
  }
879
884
 
880
885
  function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourceIndex, staticFiles, importedAssets, cssModules, base, workerReferences }) {
881
- const { nativeHandlers, effectHandlers } = semantic
886
+ const { moduleIR } = semantic
882
887
  return context => sourceFile => {
883
888
  const hasLinkElements = /<link/i.test(sourceFile.text)
884
889
  const importedStaticCollections = importedSerializableCollections(sourceFile, file, sourceFiles, sourceIndex)
@@ -887,14 +892,17 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
887
892
  sourceFile = normalized.sourceFile
888
893
  const { customHookTimerStates } = normalized
889
894
  const factory = context.factory
895
+ const sourceName = source => relative(root, source.fileName).replaceAll(sep, "/")
896
+ const componentAnalysis = createComponentAnalysisSession(semantic.componentAnalysis)
890
897
  const descriptors = createDescriptorSession({
891
898
  semantic,
892
899
  handlerUrl,
893
900
  factory,
894
901
  context,
895
902
  compileEventCommand,
903
+ handlerLowering,
896
904
  isPrimitiveLiteral: isPrimitiveDefaultLiteral,
897
- sourceName: source => relative(root, source.fileName).replaceAll(sep, "/"),
905
+ sourceName,
898
906
  rejectWorkerConstructions: expression => workerCompiler.rejectConstructions(expression, expression.getSourceFile(), "Relative TypeScript Worker construction is only supported directly inside an inline useEffect() callback")
899
907
  })
900
908
  const importBindings = clientImportBindings(sourceFile, file, sourceFiles)
@@ -923,6 +931,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
923
931
  } catch {}
924
932
  }
925
933
  const settersByFunction = new Map()
934
+ const stateOwnersByFunction = new Map()
926
935
  const localStateSettersByFunction = new Map()
927
936
  const reducersByFunction = new Map()
928
937
  const zustandStores = new Map()
@@ -951,6 +960,42 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
951
960
  const nestedLists = new WeakMap()
952
961
  const listEffectEntries = new WeakMap()
953
962
  const componentEffectEntries = new WeakMap()
963
+ const analysisSource = node => {
964
+ const original = ts.getOriginalNode(node)
965
+ return original.pos >= 0 && original.end >= 0 ? { file: sourceName(original.getSourceFile()), start: original.getStart(), end: original.end } : undefined
966
+ }
967
+ const analyzedProps = owner => {
968
+ if (owner.parameters.length !== 1 || !ts.isObjectBindingPattern(owner.parameters[0].name)) return []
969
+ return owner.parameters[0].name.elements.map(element => ({
970
+ name: (element.propertyName ?? element.name).getText(),
971
+ local: element.name.getText(),
972
+ ...(element.dotDotDotToken ? { rest: true } : {}),
973
+ ...(element.initializer ? { hasDefault: true } : {})
974
+ }))
975
+ }
976
+ const ownerName = owner => owner.name?.text ?? (ts.isVariableDeclaration(owner.parent) && ts.isIdentifier(owner.parent.name) ? owner.parent.name.text : "anonymous")
977
+ const ensureOwner = (owner, kind = "component") => componentAnalysis.registerOwner(owner, { kind, name: ownerName(owner), props: analyzedProps(owner), source: analysisSource(owner) })
978
+ const registerState = (owner, state, setter, kind, node, externalOwner) => {
979
+ const ownerRecord = ensureOwner(owner)
980
+ const stateOwner = externalOwner ?? `owner:${ownerRecord.slot}`
981
+ const stateOwners = stateOwnersByFunction.get(owner) ?? new Map()
982
+ stateOwners.set(state, stateOwner)
983
+ stateOwnersByFunction.set(owner, stateOwners)
984
+ return componentAnalysis.registerState(owner, { name: state, setter, kind, ...(externalOwner ? { owner: externalOwner } : {}), source: analysisSource(node) })
985
+ }
986
+ const stateOwnersForNode = node => {
987
+ for (let current = node.parent; current; current = current.parent) {
988
+ if (isFunctionLike(current) && stateOwnersByFunction.has(current)) return stateOwnersByFunction.get(current)
989
+ }
990
+ return new Map()
991
+ }
992
+ const fallbackOwner = node => {
993
+ for (let current = node.parent; current; current = current.parent) {
994
+ const owner = isFunctionLike(current) ? componentAnalysis.owner(current) : undefined
995
+ if (owner) return `owner:${owner.slot}`
996
+ }
997
+ return "module"
998
+ }
954
999
  let usesBehavior = false
955
1000
  let usesBinding = false
956
1001
  let usesConditional = false
@@ -996,6 +1041,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
996
1041
  if (!value || !ts.isObjectLiteralExpression(value)) throw sourceNodeError(provider, providerSource, "Context Provider value must be one direct object literal")
997
1042
  const owner = nearestFunction(provider)
998
1043
  if (!owner) throw sourceNodeError(provider, providerSource, "Context Provider value must be returned by a component")
1044
+ const stateOwner = `external:${sourceName(providerSource)}:${owner.getStart(providerSource)}`
999
1045
 
1000
1046
  const states = new Map()
1001
1047
  const callbacks = new Map()
@@ -1033,7 +1079,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1033
1079
  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)}`)
1034
1080
  }
1035
1081
  }
1036
- return { callbacks: new Map([...callbacks].filter(([name]) => fields.has(name))), context: true, fields, privateStates: new Set(), states }
1082
+ return { callbacks: new Map([...callbacks].filter(([name]) => fields.has(name))), context: true, fields, privateStates: new Set(), stateOwner, states }
1037
1083
  }
1038
1084
 
1039
1085
  const resolveCustomHook = (binding, call) => {
@@ -1106,7 +1152,9 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1106
1152
  if (hook.context) {
1107
1153
  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`)
1108
1154
  if (!names.has(state) && !requiredContextStates.has(state)) continue
1109
- setters.set(names.has(setter) || requiredContextStates.has(state) ? setter : `__kContextState_${state}`, state)
1155
+ const localSetter = names.has(setter) || requiredContextStates.has(state) ? setter : `__kContextState_${state}`
1156
+ setters.set(localSetter, state)
1157
+ registerState(owner, state, localSetter, "context", node, hook.stateOwner)
1110
1158
  if (requiredContextStates.has(state)) {
1111
1159
  const fields = customHookPrivateFields.get(node) ?? []
1112
1160
  for (const field of [state, setter]) {
@@ -1121,13 +1169,17 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1121
1169
  }
1122
1170
  if (hook.privateStates.has(state)) {
1123
1171
  setters.set(setter, state)
1172
+ registerState(owner, state, setter, "custom-hook", node)
1124
1173
  const fields = customHookPrivateFields.get(node) ?? []
1125
1174
  fields.push(state, setter)
1126
1175
  customHookPrivateFields.set(node, fields)
1127
1176
  continue
1128
1177
  }
1129
1178
  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`)
1130
- if (names.has(setter)) setters.set(setter, state)
1179
+ if (names.has(setter)) {
1180
+ setters.set(setter, state)
1181
+ registerState(owner, state, setter, "custom-hook", node)
1182
+ }
1131
1183
  }
1132
1184
  settersByFunction.set(owner, setters)
1133
1185
  for (const name of names) {
@@ -1154,9 +1206,14 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1154
1206
  const owner = nearestFunction(node)
1155
1207
  if (!owner) throw sourceNodeError(node, sourceFile, "Zustand stores cannot be used outside a Kudzu component")
1156
1208
  const setters = settersByFunction.get(owner) ?? new Map()
1157
- if (selected === store.field) setters.set(`__kStoreState_${node.name.text}`, node.name.text)
1209
+ if (selected === store.field) {
1210
+ const setter = `__kStoreState_${node.name.text}`
1211
+ setters.set(setter, node.name.text)
1212
+ registerState(owner, node.name.text, setter, "store", node)
1213
+ }
1158
1214
  else if (store.actions.has(selected)) {
1159
1215
  setters.set(node.name.text, node.name.text)
1216
+ registerState(owner, node.name.text, node.name.text, "store-action", node)
1160
1217
  const reducers = reducersByFunction.get(owner) ?? new Map()
1161
1218
  reducers.set(node.name.text, { state: node.name.text, store, action: selected })
1162
1219
  reducersByFunction.set(owner, reducers)
@@ -1183,6 +1240,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1183
1240
  if (!owner) throw sourceNodeError(node, sourceFile, "useReducer() cannot be used outside a Kudzu component")
1184
1241
  const setters = settersByFunction.get(owner) ?? new Map()
1185
1242
  setters.set(dispatchElement.name.text, stateElement.name.text)
1243
+ registerState(owner, stateElement.name.text, dispatchElement.name.text, "reducer", node)
1186
1244
  settersByFunction.set(owner, setters)
1187
1245
  const reducers = reducersByFunction.get(owner) ?? new Map()
1188
1246
  reducers.set(dispatchElement.name.text, { state: stateElement.name.text, reducer: reducer.text, import: reducerImport })
@@ -1195,6 +1253,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1195
1253
  if (owner) {
1196
1254
  const setters = settersByFunction.get(owner) ?? new Map()
1197
1255
  setters.set(setterElement.name.text, stateElement.name.text)
1256
+ registerState(owner, stateElement.name.text, setterElement.name.text, "state", node)
1198
1257
  settersByFunction.set(owner, setters)
1199
1258
  const localSetters = localStateSettersByFunction.get(owner) ?? new Set()
1200
1259
  localSetters.add(setterElement.name.text)
@@ -1205,11 +1264,28 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1205
1264
  }
1206
1265
  if (ts.isFunctionDeclaration(node) && node.name) {
1207
1266
  functions.set(node.name.text, node)
1208
- if (node.parent === sourceFile) components.set(node.name.text, { function: node, declaration: node })
1267
+ if (node.parent === sourceFile) {
1268
+ components.set(node.name.text, { function: node, declaration: node })
1269
+ ensureOwner(node)
1270
+ }
1209
1271
  }
1210
1272
  if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && (ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer))) {
1211
1273
  functions.set(node.name.text, node.initializer)
1212
- if (node.parent?.parent?.parent === sourceFile) components.set(node.name.text, { function: node.initializer, declaration: node })
1274
+ if (node.parent?.parent?.parent === sourceFile) {
1275
+ components.set(node.name.text, { function: node.initializer, declaration: node })
1276
+ ensureOwner(node.initializer)
1277
+ }
1278
+ }
1279
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isCallExpression(node.initializer) && ts.isIdentifier(node.initializer.expression)) {
1280
+ const owner = nearestFunction(node)
1281
+ if (owner && node.initializer.expression.text === "useRef" && node.initializer.arguments.length === 1 && node.initializer.arguments[0].kind === ts.SyntaxKind.NullKeyword) {
1282
+ ensureOwner(owner)
1283
+ componentAnalysis.registerRef(owner, { name: node.name.text, source: analysisSource(node) })
1284
+ }
1285
+ if (owner && node.initializer.expression.text === "useId" && node.initializer.arguments.length === 0) {
1286
+ ensureOwner(owner)
1287
+ componentAnalysis.registerId(owner, { name: node.name.text, source: analysisSource(node) })
1288
+ }
1213
1289
  }
1214
1290
  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)
1215
1291
  if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && isLocalConst(node)) {
@@ -1378,6 +1454,38 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1378
1454
  const stateBackedComponentFunctions = new WeakSet()
1379
1455
  const stateBackedComponentRoots = []
1380
1456
  let specializedImportIndex = 0
1457
+ const specialize = (call, component, label = "Keyed list", allowComponentRoot = false, ordinaryHooks = false, ordinaryStateNames = new Set(), ownership) => {
1458
+ const result = specializeComponentCall(call, component, sourceFile, factory, context, fail, label, allowComponentRoot, ordinaryHooks, ordinaryStateNames)
1459
+ const owner = nearestFunction(call)
1460
+ const setters = ownership?.setters ?? settersForNode(call, settersByFunction)
1461
+ const stateOwners = ownership?.stateOwners ?? stateOwnersForNode(call)
1462
+ const callbacks = functionsForNode(call)
1463
+ const propSignals = expression => {
1464
+ const signals = new Set()
1465
+ if (ts.isIdentifier(expression) && setters.has(expression.text)) signals.add(setters.get(expression.text))
1466
+ const callback = ts.isIdentifier(expression) ? callbacks.get(expression.text) : undefined
1467
+ for (const state of referencedStateNames((callback ?? expression).body ?? callback ?? expression, setters, callback ?? expression)) signals.add(state)
1468
+ return [...signals].map(name => ({ name, owner: stateOwners.get(name) ?? (owner ? `owner:${ensureOwner(owner).slot}` : "module") }))
1469
+ }
1470
+ result.analysis = componentAnalysis.registerSpecialization({
1471
+ kind: label,
1472
+ ...(owner ? { owner: ensureOwner(owner).slot } : {}),
1473
+ ...(analysisSource(call) ? { source: analysisSource(call) } : {}),
1474
+ props: result.props.map(prop => {
1475
+ const expression = result.propExpressions.get(prop.name)
1476
+ const signals = expression ? propSignals(expression) : []
1477
+ return { ...prop, ...(signals.length ? { signals } : {}) }
1478
+ }),
1479
+ states: [
1480
+ ...result.rowStates.map(({ state, setter, source }) => ({ name: state, setter, kind: "row", ...(analysisSource(source) ? { source: analysisSource(source) } : {}) })),
1481
+ ...result.ordinaryStates.map(({ state, setter, source }) => ({ name: state, setter, kind: "component", ...(analysisSource(source) ? { source: analysisSource(source) } : {}) }))
1482
+ ],
1483
+ 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) } : {}) }))],
1484
+ ids: result.ordinaryIds.map(({ name, source }) => ({ name, ...(analysisSource(source) ? { source: analysisSource(source) } : {}) }))
1485
+ })
1486
+ for (const state of [...result.rowStates, ...result.ordinaryStates]) state.analysisOwner = `specialization:${result.analysis.slot}`
1487
+ return result
1488
+ }
1381
1489
  const registerRowHooks = (call, specialization) => {
1382
1490
  if (!specialization.rowStates.length && !specialization.rowRefs.length) return
1383
1491
  let owner
@@ -1389,8 +1497,13 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1389
1497
  }
1390
1498
  if (!owner) owner = nearestFunction(call)
1391
1499
  const setters = new Map(settersByFunction.get(owner))
1392
- for (const state of specialization.rowStates) setters.set(state.setter, state.state)
1500
+ const stateOwners = new Map(stateOwnersByFunction.get(owner))
1501
+ for (const state of specialization.rowStates) {
1502
+ setters.set(state.setter, state.state)
1503
+ stateOwners.set(state.state, state.analysisOwner)
1504
+ }
1393
1505
  settersByFunction.set(owner, setters)
1506
+ stateOwnersByFunction.set(owner, stateOwners)
1394
1507
  rowHookCalls.push(call)
1395
1508
  usesRowState ||= specialization.rowStates.length > 0
1396
1509
  usesRowRef ||= specialization.rowRefs.length > 0
@@ -1444,7 +1557,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1444
1557
  fail(nestedCalls[0], "Reducer callback props require a component imported from a relative TypeScript module")
1445
1558
  }
1446
1559
  for (const nestedCall of nestedCalls) {
1447
- const nested = specializeComponentCall(nestedCall, nestedComponent, sourceFile, factory, context, fail, "Reducer-callback")
1560
+ const nested = specialize(nestedCall, nestedComponent, "Reducer-callback")
1448
1561
  if (nested.effects.length) fail(nestedCall, "Reducer-callback components cannot declare effects")
1449
1562
  nested.root = mergeSpecializedImports(nested.root, nestedComponent.getSourceFile(), nestedCall, nested.effects)
1450
1563
  synthesizeTree(nested.root)
@@ -1483,7 +1596,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1483
1596
  folded.parent = root.parent
1484
1597
  return folded
1485
1598
  }
1486
- const expandSetterComponents = (root, componentSource, trail, aggregate, parentSetters) => {
1599
+ const expandSetterComponents = (root, componentSource, trail, aggregate, parentSetters, parentStateOwners) => {
1487
1600
  root = foldSetterStaticConditions(root)
1488
1601
  const replacements = new WeakMap()
1489
1602
  let count = 0
@@ -1517,10 +1630,12 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1517
1630
  }
1518
1631
  const setters = new Map(parentSetters)
1519
1632
  for (const state of aggregate.ordinaryStates) setters.set(state.setter, state.state)
1633
+ const stateOwners = new Map(parentStateOwners)
1634
+ for (const state of aggregate.ordinaryStates) stateOwners.set(state.state, state.analysisOwner)
1520
1635
  if (jsxSetterCallbackProps(node, setters, functionsForNode(node), reducersForNode(node, reducersByFunction)).length) fail(node, "Setter callbacks cannot cross a second component boundary")
1521
- const nested = specializeComponentCall(node, component, sourceFile, factory, context, fail, "Nested setter-callback", true, true, new Set(setters.values()))
1636
+ const nested = specialize(node, component, "Nested setter-callback", true, true, new Set(setters.values()), { setters, stateOwners })
1522
1637
  if (dynamic && (nested.hookDeclarations.length || nested.effects.length)) fail(node, "Hookful nested setter-callback components require an unconditional or statically truthy render path")
1523
- nested.root = expandSetterComponents(nested.root, component.getSourceFile(), [...trail, component], nested, setters)
1638
+ nested.root = expandSetterComponents(nested.root, component.getSourceFile(), [...trail, component], nested, setters, stateOwners)
1524
1639
  if (imported) synthesizeTree(nested.root = mergeSpecializedImports(nested.root, component.getSourceFile(), node, nested.effects))
1525
1640
  aggregate.calculations.push(...nested.calculations)
1526
1641
  aggregate.effects.push(...nested.effects)
@@ -1549,7 +1664,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1549
1664
  if (identifierReferenceCount(sourceFile, name) !== calls.length) fail(component.declaration, `State-backed list component ${name} may only be referenced as JSX`)
1550
1665
  if (stateBackedCalls.length !== calls.length) fail(component.declaration, `State-backed list component ${name} must receive its mapped prop from local state at every call`)
1551
1666
  for (const call of stateBackedCalls) {
1552
- const specialization = specializeComponentCall(call, component.function, sourceFile, factory, context, fail)
1667
+ const specialization = specialize(call, component.function)
1553
1668
  if (specialization.effects.length) fail(call, "State-backed list components cannot declare effects")
1554
1669
  componentSpecializations.set(call, specialization)
1555
1670
  stateBackedComponentRoots.push(specialization.root)
@@ -1571,7 +1686,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1571
1686
  }
1572
1687
  const stateBackedCalls = calls.filter(call => isStateBackedListComponentCall(call, component, settersByFunction.get(nearestFunction(call)) ?? new Map()))
1573
1688
  for (const call of stateBackedCalls) {
1574
- const specialization = specializeComponentCall(call, component, sourceFile, factory, context, fail)
1689
+ const specialization = specialize(call, component)
1575
1690
  if (specialization.effects.length) fail(call, "State-backed list components cannot declare effects")
1576
1691
  componentSpecializations.set(call, specialization)
1577
1692
  stateBackedComponentRoots.push(specialization.root)
@@ -1591,7 +1706,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1591
1706
  collectReferences(component.body)
1592
1707
  if (references.length !== 1) fail(element, `Setter-callback prop ${JSON.stringify(prop)} must be used exactly once in the component`)
1593
1708
  }
1594
- const specialization = specializeComponentCall(call, component, sourceFile, factory, context, fail, "Setter-callback", false, true, new Set(settersForNode(call, settersByFunction).values()))
1709
+ const specialization = specialize(call, component, "Setter-callback", false, true, new Set(settersForNode(call, settersByFunction).values()))
1595
1710
  if (specialization.hookDeclarations.length || specialization.effects.length) {
1596
1711
  const substitutions = new Map()
1597
1712
  const attributes = ts.isJsxElement(call) ? call.openingElement.attributes : call.attributes
@@ -1605,7 +1720,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1605
1720
  for (const effect of specialization.effects) effect.call = substituteClone(effect.call, substitutions, factory, context)
1606
1721
  }
1607
1722
  }
1608
- specialization.root = expandSetterComponents(specialization.root, component.getSourceFile(), [component], specialization, settersForNode(call, settersByFunction))
1723
+ specialization.root = expandSetterComponents(specialization.root, component.getSourceFile(), [component], specialization, settersForNode(call, settersByFunction), stateOwnersForNode(call))
1609
1724
  if (imported) synthesizeTree(specialization.root = mergeSpecializedImports(specialization.root, component.getSourceFile(), call, specialization.effects))
1610
1725
  if (specialization.hookDeclarations.length || specialization.effects.length) {
1611
1726
  const owner = nearestFunction(call)
@@ -1634,6 +1749,9 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1634
1749
  const setters = new Map(settersForNode(call, settersByFunction))
1635
1750
  for (const state of specialization.ordinaryStates) setters.set(state.setter, state.state)
1636
1751
  settersByFunction.set(helper, setters)
1752
+ const stateOwners = new Map(stateOwnersForNode(call))
1753
+ for (const state of specialization.ordinaryStates) stateOwners.set(state.state, state.analysisOwner)
1754
+ stateOwnersByFunction.set(helper, stateOwners)
1637
1755
  usesComponentState ||= specialization.ordinaryStates.length > 0
1638
1756
  usesComponentId ||= specialization.usesComponentId
1639
1757
  usesComponentRef ||= specialization.ordinaryRefs.length > 0
@@ -1673,7 +1791,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1673
1791
  if (dispatchCalls.length !== calls.length) fail(component.declaration, `Reducer-dispatch component ${name} must receive a direct local reducer dispatch at every call`)
1674
1792
  for (const call of dispatchCalls) {
1675
1793
  if (componentSpecializations.has(call)) fail(call, "Reducer dispatch props cannot be combined with another component specialization")
1676
- const specialization = specializeComponentCall(call, component.function, sourceFile, factory, context, fail, "Reducer-dispatch")
1794
+ const specialization = specialize(call, component.function, "Reducer-dispatch")
1677
1795
  registerRowHooks(call, specialization)
1678
1796
  specialization.root = expandReducerCallbacks(specialization.root, component.function.getSourceFile(), call)
1679
1797
  componentSpecializations.set(call, specialization)
@@ -1696,7 +1814,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1696
1814
  const componentSource = component.getSourceFile()
1697
1815
  for (const call of dispatchCalls) {
1698
1816
  if (componentSpecializations.has(call)) fail(call, "Reducer dispatch props cannot be combined with another component specialization")
1699
- const specialization = specializeComponentCall(call, component, sourceFile, factory, context, fail, "Reducer-dispatch")
1817
+ const specialization = specialize(call, component, "Reducer-dispatch")
1700
1818
  registerRowHooks(call, specialization)
1701
1819
  specialization.root = expandReducerCallbacks(specialization.root, componentSource, call)
1702
1820
  specialization.root = mergeSpecializedImports(specialization.root, componentSource, call, specialization.effects)
@@ -1779,7 +1897,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1779
1897
  for (const call of calls) {
1780
1898
  const specialization = reducerComponentCalls.has(call)
1781
1899
  ? componentSpecializations.get(call)
1782
- : specializeComponentCall(call, component.function, sourceFile, factory, context, fail, "Keyed list", true)
1900
+ : specialize(call, component.function, "Keyed list", true)
1783
1901
  registerRowHooks(call, specialization)
1784
1902
  if (specialization.effects.length && !keyedComponentCalls.has(call)) fail(call, "Effectful keyed row components may only be used directly as keyed map rows")
1785
1903
  specialization.component = component.function
@@ -1815,7 +1933,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1815
1933
  const chain = [...trail, component].map(entry => entry.name?.text || "anonymous").join(" -> ")
1816
1934
  fail(node, `Keyed list component cycle: ${chain}`)
1817
1935
  }
1818
- const specialization = specializeComponentCall(node, component, sourceFile, factory, context, fail, "Keyed list", true)
1936
+ const specialization = specialize(node, component, "Keyed list", true)
1819
1937
  registerRowHooks(node, specialization)
1820
1938
  specialization.root = expandKeyedComponents(specialization.root, component.getSourceFile(), [...trail, component], specialization)
1821
1939
  if (imported) synthesizeTree(specialization.root = mergeSpecializedImports(specialization.root, component.getSourceFile(), node, specialization.effects))
@@ -1860,7 +1978,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1860
1978
  }
1861
1979
  for (const { node, parts: originalParts } of rawRenderedLists) {
1862
1980
  if (keyedListParentTag(node) === "table") throw new Error("Keyed table rows must be wrapped in <tbody>, <thead>, or <tfoot>")
1863
- const specialization = componentSpecializations.get(originalParts.root) ?? { root: originalParts.root, calculations: [], effects: [], hookDeclarations: [], rowStates: [], rowRefs: [] }
1981
+ const specialization = componentSpecializations.get(originalParts.root) ?? { root: originalParts.root, calculations: [], effects: [], hookDeclarations: [], rowStates: [], rowRefs: [], ordinaryStates: [] }
1864
1982
  const componentSource = specialization.componentSource ?? sourceFile
1865
1983
  specialization.root = expandKeyedComponents(specialization.root, componentSource, specialization.component ? [specialization.component] : [], specialization)
1866
1984
  if (specialization.imported) synthesizeTree(specialization.root = mergeSpecializedImports(specialization.root, componentSource, originalParts.root, specialization.effects))
@@ -1880,7 +1998,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1880
1998
  callback.parent = originalParts.callback.parent
1881
1999
  }
1882
2000
  callback = prepareListCallback(callback, root, specialization, originalParts.item)
1883
- const parts = { ...originalParts, root, callback }
2001
+ const parts = { ...originalParts, root, callback, analysisStateOwners: new Map([...stateOwnersForNode(originalParts.root), ...[...specialization.rowStates, ...specialization.ordinaryStates].map(state => [state.state, state.analysisOwner])]) }
1884
2002
  for (const calculation of specialization.calculations) {
1885
2003
  ts.setParentRecursive(calculation, false)
1886
2004
  calculation.parent = callback
@@ -1908,6 +2026,14 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1908
2026
  )
1909
2027
  }
1910
2028
 
2029
+ let activeStateOwners
2030
+ const visitWithStateOwners = (node, stateOwners) => {
2031
+ const previous = activeStateOwners
2032
+ activeStateOwners = new Map([...(previous ?? []), ...stateOwners])
2033
+ const result = ts.visitNode(node, visitor)
2034
+ activeStateOwners = previous
2035
+ return result
2036
+ }
1911
2037
  const visitor = node => {
1912
2038
  if (ts.isVariableDeclaration(node) && ts.isObjectBindingPattern(node.name) && customHookPrivateFields.has(node)) {
1913
2039
  const privateFields = customHookPrivateFields.get(node)
@@ -1920,7 +2046,11 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1920
2046
  return ts.visitEachChild(factory.updateBlock(node, [...setterHookHelpers.get(node), ...node.statements]), visitor, context)
1921
2047
  }
1922
2048
  if (specializedDeclarations.has(node)) return node
1923
- if (componentSpecializations.has(node)) return ts.visitNode(componentSpecializations.get(node).root, visitor)
2049
+ if (componentSpecializations.has(node)) {
2050
+ const specialization = componentSpecializations.get(node)
2051
+ const stateOwners = new Map([...stateOwnersForNode(node), ...[...specialization.rowStates, ...specialization.ordinaryStates].map(state => [state.state, state.analysisOwner])])
2052
+ return visitWithStateOwners(specialization.root, stateOwners)
2053
+ }
1924
2054
 
1925
2055
  if (hasLinkElements && (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) && isStylesheetLink(node)) {
1926
2056
  fail(node, "Stylesheets must be placed under src/ or declared in kudzu.config styles so Kudzu can emit them in <head>")
@@ -1990,7 +2120,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1990
2120
  }
1991
2121
  const invalidDependency = ordinaryDependencies.find(dependency => !ts.isIdentifier(dependency))
1992
2122
  if (invalidDependency) effectFail(invalidDependency, "useEffect() dependencies must be direct state or runtime parameter identifiers")
1993
- const dependencyExpressions = []
2123
+ const dependencyDerived = []
1994
2124
  const dependencyStates = new Map()
1995
2125
  const dependencySubstitutions = new Map()
1996
2126
  const subscriptionDependencies = []
@@ -2006,7 +2136,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
2006
2136
  const usedStates = new Set()
2007
2137
  const expression = collectionExpression(initializer, { fail: effectFail, stateNames, selectorStates: usedStates })
2008
2138
  if (!usedStates.size) effectFail(dependency, `useEffect() derived dependency "${dependency.text}" must read direct primitive state`)
2009
- dependencyExpressions.push(expression)
2139
+ dependencyDerived.push({ expression, states: usedStates, source: initializer })
2010
2140
  for (const name of usedStates) {
2011
2141
  subscriptionDependencies.push(factory.createIdentifier(name))
2012
2142
  dependencyStates.set(name, factory.createIdentifier(name))
@@ -2015,12 +2145,12 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
2015
2145
  hasDerivedDependency = true
2016
2146
  } else {
2017
2147
  subscriptionDependencies.push(dependency)
2018
- dependencyExpressions.push(["state", dependency.text])
2148
+ dependencyDerived.push({ expression: ["state", dependency.text], states: [dependency.text], source: dependency })
2019
2149
  dependencyStates.set(dependency.text, dependency)
2020
2150
  }
2021
2151
  }
2022
2152
  if (!hasDerivedDependency) {
2023
- dependencyExpressions.length = 0
2153
+ dependencyDerived.length = 0
2024
2154
  dependencyStates.clear()
2025
2155
  }
2026
2156
  if (!effectOwner) fail(node, "useEffect() cannot be used outside a Kudzu component")
@@ -2072,6 +2202,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
2072
2202
  for (const reference of workerReferences.slice(workerStart)) Object.assign(reference, { module: handlerUrl, handler: descriptor.exportName })
2073
2203
  usesListItem ||= Boolean(itemDependencies.length && !listEffect)
2074
2204
  usesBehavior = true
2205
+ const derivedDependencies = hasDerivedDependency ? dependencyDerived.map(entry => descriptors.registerDerived("expression", entry.expression, entry.states, entry.source)) : []
2075
2206
  return factory.updateCallExpression(node, node.expression, node.typeArguments, [
2076
2207
  callback,
2077
2208
  factory.createArrayLiteralExpression(hasDerivedDependency ? subscriptionDependencies : ordinaryDependencies),
@@ -2082,7 +2213,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
2082
2213
  factory.createStringLiteral(specializedEffect ? sourceLocation(specializedEffect.source, specializedEffect.sourceFile) : sourceLocation(node, sourceFile)),
2083
2214
  returns.cleanup ? factory.createTrue() : factory.createFalse(),
2084
2215
  factory.createArrayLiteralExpression(itemDependencies.map(field => factory.createStringLiteral(field))),
2085
- hasDerivedDependency ? jsonExpression(dependencyExpressions, factory) : factory.createArrayLiteralExpression(),
2216
+ hasDerivedDependency ? jsonExpression(derivedDependencies.map(entry => entry.expression), factory) : factory.createArrayLiteralExpression(),
2086
2217
  factory.createArrayLiteralExpression([...dependencyStates].map(([name, state]) => factory.createArrayLiteralExpression([factory.createStringLiteral(name), state])))
2087
2218
  ])
2088
2219
  }
@@ -2150,12 +2281,13 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
2150
2281
  usesBinding = true
2151
2282
  listSource = descriptors.compileReactiveBinding(listParts.calculation, { setters: settersForNode(node, settersByFunction), importBindings })
2152
2283
  }
2284
+ const selector = listParts.selector?.length ? descriptors.registerDerived("selector", listParts.selector, listParts.selectorStates, node).selector : listParts.selector ?? []
2153
2285
  const arguments_ = [
2154
2286
  listSource,
2155
2287
  listParts.keyField === null ? factory.createNull() : factory.createStringLiteral(listParts.keyField),
2156
- ts.visitNode(listParts.callback, visitor),
2288
+ visitWithStateOwners(listParts.callback, listParts.analysisStateOwners ?? new Map()),
2157
2289
  factory.createStringLiteral(listParts.ownerField ?? ""),
2158
- jsonExpression(listParts.selector ?? [], factory),
2290
+ jsonExpression(selector, factory),
2159
2291
  listParts.indexed ? factory.createTrue() : factory.createFalse()
2160
2292
  ]
2161
2293
  if (listParts.selectorStates?.size || listParts.static) arguments_.push(factory.createArrayLiteralExpression([...(listParts.selectorStates ?? [])].map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), factory.createIdentifier(name)]))))
@@ -2195,6 +2327,8 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
2195
2327
  if (ts.isJsxAttribute(node) && node.initializer && ts.isJsxExpression(node.initializer) && node.initializer.expression && /^on[A-Z]/.test(node.name.text)) {
2196
2328
  const setters = settersForNode(node, settersByFunction)
2197
2329
  const event = descriptors.compileEvent(node.initializer.expression, {
2330
+ owner: fallbackOwner(node),
2331
+ stateOwners: activeStateOwners ?? stateOwnersForNode(node),
2198
2332
  setters,
2199
2333
  reducers: reducersForNode(node, reducersByFunction),
2200
2334
  functions: functionsForNode(node),
@@ -2214,10 +2348,11 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
2214
2348
  }
2215
2349
 
2216
2350
  const transformed = ts.visitNode(sourceFile, visitor)
2351
+ descriptors.finalize()
2217
2352
  if (!usesBehavior) return transformed
2218
2353
 
2219
2354
  const behaviorImports = [factory.createImportSpecifier(false, factory.createIdentifier("behavior"), factory.createIdentifier("__kBehavior"))]
2220
- if (nativeHandlers.length) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("nativeBehavior"), factory.createIdentifier("__kNativeBehavior")))
2355
+ if (moduleIR.handlers.some(handler => handler.kind === "module-export" && handler.role === "native")) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("nativeBehavior"), factory.createIdentifier("__kNativeBehavior")))
2221
2356
  if (usesBinding) {
2222
2357
  behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("binding"), factory.createIdentifier("__kBinding")))
2223
2358
  behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("select"), factory.createIdentifier("__kSelect")))
@@ -2476,7 +2611,8 @@ function validateKeyedList(parts, sourceFile, listValues, listEventItems, listCo
2476
2611
  callback.parent = nested.callback.parent
2477
2612
  }
2478
2613
  callback = prepareListCallback(callback, root, specialization ?? { hookDeclarations: [], effects: [] }, nested.item)
2479
- const nestedParts = { ...nested, root, callback, state: parts.state, nested: true }
2614
+ const specializedStates = [...(specialization?.rowStates ?? []), ...(specialization?.ordinaryStates ?? [])]
2615
+ const nestedParts = { ...nested, root, callback, state: parts.state, nested: true, analysisStateOwners: new Map([...(parts.analysisStateOwners ?? []), ...specializedStates.map(state => [state.state, state.analysisOwner])]) }
2480
2616
  for (const calculation of specialization?.calculations ?? []) {
2481
2617
  ts.setParentRecursive(calculation, false)
2482
2618
  calculation.parent = callback
@@ -2704,6 +2840,13 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
2704
2840
  }
2705
2841
  const restEntries = [...props].filter(([prop]) => !acceptedProps.has(prop))
2706
2842
  if (!rest) for (const [prop] of restEntries) fail(call, `Unknown ${label.toLowerCase()} component prop "${prop}"`)
2843
+ const propAnalysis = elements.map(element => ({
2844
+ name: (element.propertyName ?? element.name).getText(),
2845
+ local: element.name.getText(),
2846
+ provided: element.dotDotDotToken ? restEntries.length > 0 : props.has((element.propertyName ?? element.name).text),
2847
+ ...(element.dotDotDotToken ? { rest: true } : {}),
2848
+ ...(element.initializer ? { hasDefault: true, defaultApplied: !props.has((element.propertyName ?? element.name).text) } : {})
2849
+ }))
2707
2850
 
2708
2851
  let returned
2709
2852
  const calculations = []
@@ -2713,7 +2856,7 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
2713
2856
  const rowRefs = []
2714
2857
  const ordinaryStates = []
2715
2858
  const ordinaryRefs = []
2716
- let usesComponentId = false
2859
+ const ordinaryIds = []
2717
2860
  if (!ts.isBlock(component.body)) {
2718
2861
  returned = component.body
2719
2862
  } else {
@@ -2748,8 +2891,8 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
2748
2891
  synthesizeTree(initialValue)
2749
2892
  const initializer = factory.createCallExpression(factory.createIdentifier(ordinaryHooks ? "__kComponentUseState" : "__kRowUseState"), undefined, [initialValue])
2750
2893
  hookDeclarations.push(factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(binding, undefined, undefined, initializer)], ts.NodeFlags.Const)))
2751
- if (ordinaryHooks) ordinaryStates.push({ state, setter })
2752
- else rowStates.push({ state, setter })
2894
+ if (ordinaryHooks) ordinaryStates.push({ state, setter, source: declaration })
2895
+ else rowStates.push({ state, setter, source: declaration })
2753
2896
  continue
2754
2897
  }
2755
2898
  if (declaration.initializer && ts.isCallExpression(declaration.initializer) && ts.isIdentifier(declaration.initializer.expression) && declaration.initializer.expression.text === "useRef") {
@@ -2761,7 +2904,7 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
2761
2904
  substitutions.set(declaration.name.text, factory.createIdentifier(name))
2762
2905
  const initializer = factory.createCallExpression(factory.createIdentifier(ordinaryHooks ? "__kComponentUseRef" : "__kRowUseRef"), declaration.initializer.typeArguments?.map(type => cloneAst(type, factory, context)), [factory.createNull()])
2763
2906
  hookDeclarations.push(factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(factory.createIdentifier(name), undefined, undefined, initializer)], ts.NodeFlags.Const)))
2764
- refs.push({ name })
2907
+ refs.push({ name, source: declaration })
2765
2908
  continue
2766
2909
  }
2767
2910
  if (declaration.initializer && ts.isCallExpression(declaration.initializer) && ts.isIdentifier(declaration.initializer.expression) && declaration.initializer.expression.text === "useId") {
@@ -2771,7 +2914,7 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
2771
2914
  substitutions.set(declaration.name.text, factory.createIdentifier(name))
2772
2915
  const initializer = factory.createCallExpression(factory.createIdentifier("__kComponentUseId"), undefined, [])
2773
2916
  hookDeclarations.push(factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(factory.createIdentifier(name), undefined, undefined, initializer)], ts.NodeFlags.Const)))
2774
- usesComponentId = true
2917
+ ordinaryIds.push({ name, source: declaration })
2775
2918
  continue
2776
2919
  }
2777
2920
  if (!ts.isIdentifier(declaration.name) || !declaration.initializer) fail(declaration, `${label} component locals must be initialized identifiers`)
@@ -2812,7 +2955,10 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
2812
2955
  rowRefs,
2813
2956
  ordinaryStates,
2814
2957
  ordinaryRefs,
2815
- usesComponentId
2958
+ ordinaryIds,
2959
+ propExpressions: props,
2960
+ props: propAnalysis,
2961
+ usesComponentId: ordinaryIds.length > 0
2816
2962
  }
2817
2963
  }
2818
2964
 
@@ -3744,9 +3890,8 @@ const workerCompiler = createWorkerCompiler({
3744
3890
  })
3745
3891
 
3746
3892
  const printEffectEntry = createEffectCodegen({ assetPath, inlineJson, relativeModulePath })
3893
+ const handlerLowering = createHandlerLowering({ cloneAst, synthesizeTree })
3747
3894
  const printHandlerModule = createHandlerCodegen({
3748
- cloneAst,
3749
- synthesizeTree,
3750
3895
  resolveClientImport: (entry, handlerPath) => entry.package ? entry.target : relativeModulePath(handlerPath, clientModulePath(entry.target))
3751
3896
  })
3752
3897
  const { normalizeReactMigrationSyntax, validateUseIdSyntax } = createReactMigrationPass({ cloneAst, jsxTagName })