@kudzujs/core 0.8.18 → 0.8.20

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.
@@ -13,6 +13,7 @@ import { normalizeCustomHookTimerRefs } from "./compiler/custom-hook-timer-pass.
13
13
  import { captureNames, createDescriptorSession, createSemanticArtifact, nativeCaptureNames, referencedReducerDispatches, referencedStateNames } from "./compiler/descriptor-session.mjs"
14
14
  import { createEffectCodegen } from "./compiler/effect-codegen.mjs"
15
15
  import { createHandlerCodegen } from "./compiler/handler-codegen.mjs"
16
+ import { createHandlerLowering } from "./compiler/handler-lowering.mjs"
16
17
  import { createCommandSpecializer } from "./compiler/optimize/command-specialization.mjs"
17
18
  import { applyNormalizationPasses } from "./compiler/normalization-pipeline.mjs"
18
19
  import { createReactMigrationPass, reactMemoExpression } from "./compiler/react-migration-pass.mjs"
@@ -704,18 +705,18 @@ async function compile(file, sourceFiles, sourceIndex, staticFiles, importedAsse
704
705
  await mkdir(resolve(output, ".."), { recursive: true })
705
706
  await writeFile(output, result.outputText)
706
707
 
707
- const { componentAnalysis, nativeHandlers, effectHandlers, reactiveBindings, listExpressions, clientImports } = semantic
708
- const sourceResult = { file: relative(root, file).replaceAll(sep, "/"), componentAnalysis }
709
- if (!nativeHandlers.length && !effectHandlers.length && !reactiveBindings.length && !listExpressions.length) return sourceResult
710
- const callbacks = [...nativeHandlers, ...effectHandlers]
711
- 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 })
712
713
  const moduleResult = ts.transpileModule(moduleSource, {
713
714
  compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext },
714
715
  reportDiagnostics: true
715
716
  })
716
717
  const moduleErrors = moduleResult.diagnostics?.filter(diagnostic => diagnostic.category === ts.DiagnosticCategory.Error) ?? []
717
718
  if (moduleErrors.length) throw new Error(moduleErrors.map(error => ts.flattenDiagnosticMessageText(error.messageText, "\n")).join("\n"))
718
- sourceResult.handlerModule = { 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) }
719
720
  return sourceResult
720
721
  }
721
722
 
@@ -882,7 +883,7 @@ function normalizeCompilerSource(sourceFile, { base, context, file, importedColl
882
883
  }
883
884
 
884
885
  function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourceIndex, staticFiles, importedAssets, cssModules, base, workerReferences }) {
885
- const { nativeHandlers, effectHandlers } = semantic
886
+ const { moduleIR } = semantic
886
887
  return context => sourceFile => {
887
888
  const hasLinkElements = /<link/i.test(sourceFile.text)
888
889
  const importedStaticCollections = importedSerializableCollections(sourceFile, file, sourceFiles, sourceIndex)
@@ -899,6 +900,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
899
900
  factory,
900
901
  context,
901
902
  compileEventCommand,
903
+ handlerLowering,
902
904
  isPrimitiveLiteral: isPrimitiveDefaultLiteral,
903
905
  sourceName,
904
906
  rejectWorkerConstructions: expression => workerCompiler.rejectConstructions(expression, expression.getSourceFile(), "Relative TypeScript Worker construction is only supported directly inside an inline useEffect() callback")
@@ -950,13 +952,8 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
950
952
  const customHooks = new Map()
951
953
  const jsxLocalDeclarations = new Map()
952
954
  const jsxLocalsByFunction = new Map()
953
- const listLocalDeclarations = new WeakSet()
954
- const listLocalUses = new WeakMap()
955
- const listValues = new WeakMap()
956
- const listEventItems = new WeakMap()
957
- const listConditions = new WeakMap()
958
- const nestedLists = new WeakMap()
959
- const listEffectEntries = new WeakMap()
955
+ const listLocalDeclarations = []
956
+ const listLocalUses = []
960
957
  const componentEffectEntries = new WeakMap()
961
958
  const analysisSource = node => {
962
959
  const original = ts.getOriginalNode(node)
@@ -1338,8 +1335,8 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1338
1335
  const position = sourceFile.getLineAndCharacterOfPosition(declaration.node.getStart(sourceFile))
1339
1336
  if (uses.length > 1) throw new Error(`${sourceFile.fileName}:${position.line + 1}:${position.character + 1} Keyed list local "${name}" must be rendered exactly once`)
1340
1337
  if (references !== uses.length) throw new Error(`${sourceFile.fileName}:${position.line + 1}:${position.character + 1} Keyed list local "${name}" may only be used as a JSX child`)
1341
- listLocalDeclarations.add(declaration.node)
1342
- if (uses.length) listLocalUses.set(uses[0], parts)
1338
+ listLocalDeclarations.push(declaration.node)
1339
+ if (uses.length) listLocalUses.push({ node: uses[0], parts })
1343
1340
  }
1344
1341
  }
1345
1342
  }
@@ -1482,6 +1479,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1482
1479
  ids: result.ordinaryIds.map(({ name, source }) => ({ name, ...(analysisSource(source) ? { source: analysisSource(source) } : {}) }))
1483
1480
  })
1484
1481
  for (const state of [...result.rowStates, ...result.ordinaryStates]) state.analysisOwner = `specialization:${result.analysis.slot}`
1482
+ for (const ref of [...result.rowRefs, ...result.ordinaryRefs]) ref.analysisOwner = `specialization:${result.analysis.slot}`
1485
1483
  return result
1486
1484
  }
1487
1485
  const registerRowHooks = (call, specialization) => {
@@ -1845,20 +1843,20 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1845
1843
  if (expanded === value || !referencedStateNames(expanded, setters).size) fail(value, "Calculated collection fields must directly depend on local state")
1846
1844
  return expanded
1847
1845
  }
1848
- const parts = listLocalUses.get(node) ?? keyedListParts(node.expression, setters, jsxLocalDeclarations.get(owner), fail, new Set(), importedCollections, factory, context, importedCollectionTransforms, calculatedCollection, staticCollection)
1846
+ const parts = listLocalUses.find(entry => entry.node === node)?.parts ?? keyedListParts(node.expression, setters, jsxLocalDeclarations.get(owner), fail, new Set(), importedCollections, factory, context, importedCollectionTransforms, calculatedCollection, staticCollection)
1849
1847
  if (parts) {
1850
- for (const declaration of parts.aliasDeclarations ?? []) listLocalDeclarations.add(declaration)
1848
+ for (const declaration of parts.aliasDeclarations ?? []) if (!listLocalDeclarations.includes(declaration)) listLocalDeclarations.push(declaration)
1851
1849
  rawRenderedLists.push({ node, parts })
1852
1850
  }
1853
1851
  }
1854
1852
  ts.forEachChild(node, collectRenderedLists)
1855
1853
  }
1856
1854
  collectRenderedLists(sourceFile)
1857
- const collectionAliasUses = new WeakSet(rawRenderedLists.flatMap(({ parts }) => parts.aliasUses ?? []))
1855
+ const collectionAliasUses = rawRenderedLists.flatMap(({ parts }) => parts.aliasUses ?? [])
1858
1856
  const collectionAliasDeclarations = new Set(rawRenderedLists.flatMap(({ parts }) => parts.aliasDeclarations ?? []))
1859
1857
  for (const declaration of collectionAliasDeclarations) {
1860
1858
  const owner = nearestFunction(declaration)
1861
- const unsupported = identifierReferences(owner.body, declaration.name.text).find(reference => !collectionAliasUses.has(reference))
1859
+ const unsupported = identifierReferences(owner.body, declaration.name.text).find(reference => !collectionAliasUses.includes(reference))
1862
1860
  if (unsupported) fail(unsupported, `Rendered collection alias "${declaration.name.text}" may only be used as a rendered collection source`)
1863
1861
  }
1864
1862
  const rejectUnsupportedRenderControl = node => {
@@ -1910,7 +1908,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1910
1908
  let count = 0
1911
1909
  const visit = (node, currentAggregate = aggregate) => {
1912
1910
  if (node !== root && ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "map" && containsJsx(node)) {
1913
- const nestedAggregate = { calculations: [], effects: [], hookDeclarations: [], rowStates: [], rowRefs: [] }
1911
+ const nestedAggregate = { calculations: [], effects: [], hookDeclarations: [], rowStates: [], rowRefs: [], specializations: [] }
1914
1912
  for (const argument of node.arguments) visit(argument, nestedAggregate)
1915
1913
  if (nestedAggregate.hookDeclarations.length || nestedAggregate.effects.length) nestedRowSpecializations.set(`${node.pos}:${node.end}`, nestedAggregate)
1916
1914
  return
@@ -1937,6 +1935,8 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1937
1935
  if (imported) synthesizeTree(specialization.root = mergeSpecializedImports(specialization.root, component.getSourceFile(), node, specialization.effects))
1938
1936
  expandedRowSpecializations.set(specialization.root, specialization)
1939
1937
  if (currentAggregate) {
1938
+ currentAggregate.specializations ??= []
1939
+ currentAggregate.specializations.push(specialization.analysis.slot, ...(specialization.specializations ?? []))
1940
1940
  currentAggregate.effects.push(...specialization.effects)
1941
1941
  currentAggregate.hookDeclarations.push(...specialization.hookDeclarations)
1942
1942
  currentAggregate.rowStates.push(...specialization.rowStates)
@@ -1955,8 +1955,8 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1955
1955
  expanded.parent = root.parent
1956
1956
  return expanded
1957
1957
  }
1958
- const renderedLists = new WeakMap()
1959
- const prepareListCallback = (callback, root, specialization, item) => {
1958
+ const preparedRenderedLists = []
1959
+ const prepareListCallback = (callback, root, specialization, item, effectEntries) => {
1960
1960
  const statements = [...specialization.hookDeclarations]
1961
1961
  if (specialization.effects.length) {
1962
1962
  usesListEffects = true
@@ -1964,7 +1964,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1964
1964
  const call = factory.updateCallExpression(entry.call, factory.createIdentifier("__kListUseEffect"), entry.call.typeArguments, entry.call.arguments)
1965
1965
  synthesizeTree(call)
1966
1966
  const effectSource = entry.source.getSourceFile()
1967
- listEffectEntries.set(call, { item, source: entry.source, sourceFile: effectSource, imports: clientImportBindings(effectSource, effectSource.fileName, sourceFiles) })
1967
+ effectEntries.push({ node: call, item, source: entry.source, sourceFile: effectSource, imports: clientImportBindings(effectSource, effectSource.fileName, sourceFiles) })
1968
1968
  return factory.createExpressionStatement(call)
1969
1969
  }))
1970
1970
  }
@@ -1995,15 +1995,25 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1995
1995
  ts.setParentRecursive(callback, false)
1996
1996
  callback.parent = originalParts.callback.parent
1997
1997
  }
1998
- callback = prepareListCallback(callback, root, specialization, originalParts.item)
1999
- const parts = { ...originalParts, root, callback, analysisStateOwners: new Map([...stateOwnersForNode(originalParts.root), ...[...specialization.rowStates, ...specialization.ordinaryStates].map(state => [state.state, state.analysisOwner])]) }
1998
+ const effectEntries = []
1999
+ callback = prepareListCallback(callback, root, specialization, originalParts.item, effectEntries)
2000
+ const parts = {
2001
+ ...originalParts,
2002
+ root,
2003
+ callback,
2004
+ effectEntries,
2005
+ specializations: [specialization.analysis?.slot, ...(specialization.specializations ?? [])].filter(slot => slot !== undefined),
2006
+ rowStates: [...specialization.rowStates, ...specialization.ordinaryStates],
2007
+ rowRefs: specialization.rowRefs,
2008
+ analysisStateOwners: new Map([...stateOwnersForNode(originalParts.root), ...[...specialization.rowStates, ...specialization.ordinaryStates].map(state => [state.state, state.analysisOwner])])
2009
+ }
2000
2010
  for (const calculation of specialization.calculations) {
2001
2011
  ts.setParentRecursive(calculation, false)
2002
2012
  calculation.parent = callback
2003
2013
  validateListExpression(calculation, parts.item, originalParts.root, fail)
2004
2014
  }
2005
- validateKeyedList(parts, sourceFile, listValues, listEventItems, listConditions, settersForNode(originalParts.root, settersByFunction), specialization.rowStates, nestedLists, componentSpecializations, expandedRowSpecializations, nestedRowSpecializations, factory, prepareListCallback)
2006
- renderedLists.set(node, parts)
2015
+ const analysis = validateKeyedList(parts, sourceFile, settersForNode(originalParts.root, settersByFunction), specialization.rowStates, componentSpecializations, expandedRowSpecializations, nestedRowSpecializations, factory, prepareListCallback)
2016
+ preparedRenderedLists.push({ node, parts, analysis })
2007
2017
  }
2008
2018
 
2009
2019
  const compileRenderExpression = (expression, anchor) => {
@@ -2025,6 +2035,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
2025
2035
  }
2026
2036
 
2027
2037
  let activeStateOwners
2038
+ let activeKeyedBlock
2028
2039
  const visitWithStateOwners = (node, stateOwners) => {
2029
2040
  const previous = activeStateOwners
2030
2041
  activeStateOwners = new Map([...(previous ?? []), ...stateOwners])
@@ -2032,6 +2043,58 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
2032
2043
  activeStateOwners = previous
2033
2044
  return result
2034
2045
  }
2046
+ const keyedEntry = (entries, node) => entries.find(entry => entry.node === node)
2047
+ const compileKeyedBlock = (node, { parts: listParts, analysis }) => {
2048
+ usesBehavior = true
2049
+ usesList = true
2050
+ const blockSlot = moduleIR.keyedBlocks.length
2051
+ let listSource = listParts.state
2052
+ let collection = { kind: "signal", name: listParts.state?.text }
2053
+ if (listParts.calculation) {
2054
+ usesBinding = true
2055
+ listSource = descriptors.compileReactiveBinding(listParts.calculation, { setters: settersForNode(node, settersByFunction), importBindings, keyedBlock: blockSlot })
2056
+ const exportName = ts.isCallExpression(listSource) && ts.isStringLiteral(listSource.arguments[2]) ? listSource.arguments[2].text : undefined
2057
+ collection = { kind: "binding", ...(exportName ? { exportName } : {}) }
2058
+ }
2059
+ const derived = listParts.selector?.length ? descriptors.registerDerived("selector", listParts.selector, listParts.selectorStates, node) : undefined
2060
+ const parent = activeKeyedBlock?.block
2061
+ const rowStates = (listParts.rowStates ?? []).map(state => ({ name: state.state, setter: state.setter, owner: state.analysisOwner, ...(analysisSource(state.source) ? { source: analysisSource(state.source) } : {}) }))
2062
+ const rowRefs = (listParts.rowRefs ?? []).map(ref => ({ name: ref.name, owner: ref.analysisOwner, ...(analysisSource(ref.source) ? { source: analysisSource(ref.source) } : {}) }))
2063
+ const specializations = [...new Set([...(listParts.specializations ?? []), ...rowStates.map(state => state.owner), ...rowRefs.map(ref => ref.owner)].filter(value => value !== undefined).map(value => typeof value === "string" ? Number(value.slice(value.lastIndexOf(":") + 1)) : value))]
2064
+ const block = descriptors.registerKeyedBlock({
2065
+ ...(analysisSource(node) ? { source: analysisSource(node) } : {}),
2066
+ ...(parent ? { parent: parent.slot } : {}),
2067
+ children: [],
2068
+ collection,
2069
+ key: listParts.keyField,
2070
+ ...(listParts.ownerField ? { ownerField: listParts.ownerField } : {}),
2071
+ item: listParts.item,
2072
+ ...(listParts.index ? { index: listParts.index } : {}),
2073
+ indexed: listParts.indexed,
2074
+ static: Boolean(listParts.static),
2075
+ ...(derived ? { selector: derived.slot } : {}),
2076
+ selectorStates: [...(listParts.selectorStates ?? [])],
2077
+ specializations,
2078
+ rowStates,
2079
+ rowRefs
2080
+ })
2081
+ if (parent) parent.children.push(block.slot)
2082
+ const previous = activeKeyedBlock
2083
+ activeKeyedBlock = { analysis, block, parts: listParts }
2084
+ const callback = visitWithStateOwners(listParts.callback, listParts.analysisStateOwners ?? new Map())
2085
+ activeKeyedBlock = previous
2086
+ const arguments_ = [
2087
+ listSource,
2088
+ block.key === null ? factory.createNull() : factory.createStringLiteral(block.key),
2089
+ callback,
2090
+ factory.createStringLiteral(block.ownerField ?? ""),
2091
+ jsonExpression(derived?.selector ?? listParts.selector ?? [], factory),
2092
+ block.indexed ? factory.createTrue() : factory.createFalse()
2093
+ ]
2094
+ if (block.selectorStates.length || block.static) arguments_.push(factory.createArrayLiteralExpression(block.selectorStates.map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), factory.createIdentifier(name)]))))
2095
+ if (block.static) arguments_.push(factory.createTrue())
2096
+ return factory.updateJsxExpression(node, factory.createCallExpression(factory.createIdentifier("__kList"), undefined, arguments_))
2097
+ }
2035
2098
  const visitor = node => {
2036
2099
  if (ts.isVariableDeclaration(node) && ts.isObjectBindingPattern(node.name) && customHookPrivateFields.has(node)) {
2037
2100
  const privateFields = customHookPrivateFields.get(node)
@@ -2075,7 +2138,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
2075
2138
  return factory.updateExportDeclaration(node, node.modifiers, node.isTypeOnly, node.exportClause, factory.createStringLiteral(relativeModulePath(compiledPath(file), compiledPath(target))), node.attributes)
2076
2139
  }
2077
2140
 
2078
- const listEffect = ts.isCallExpression(node) ? listEffectEntries.get(node) : undefined
2141
+ const listEffect = ts.isCallExpression(node) ? keyedEntry(activeKeyedBlock?.analysis.effects ?? [], node) : undefined
2079
2142
  const componentEffect = ts.isCallExpression(node) ? componentEffectEntries.get(node) : undefined
2080
2143
  const specializedEffect = listEffect ?? componentEffect
2081
2144
  if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && (hasUseEffectImport && node.expression.text === "useEffect" || specializedEffect)) {
@@ -2118,7 +2181,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
2118
2181
  }
2119
2182
  const invalidDependency = ordinaryDependencies.find(dependency => !ts.isIdentifier(dependency))
2120
2183
  if (invalidDependency) effectFail(invalidDependency, "useEffect() dependencies must be direct state or runtime parameter identifiers")
2121
- const dependencyExpressions = []
2184
+ const dependencyDerived = []
2122
2185
  const dependencyStates = new Map()
2123
2186
  const dependencySubstitutions = new Map()
2124
2187
  const subscriptionDependencies = []
@@ -2134,7 +2197,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
2134
2197
  const usedStates = new Set()
2135
2198
  const expression = collectionExpression(initializer, { fail: effectFail, stateNames, selectorStates: usedStates })
2136
2199
  if (!usedStates.size) effectFail(dependency, `useEffect() derived dependency "${dependency.text}" must read direct primitive state`)
2137
- dependencyExpressions.push(expression)
2200
+ dependencyDerived.push({ expression, states: usedStates, source: initializer })
2138
2201
  for (const name of usedStates) {
2139
2202
  subscriptionDependencies.push(factory.createIdentifier(name))
2140
2203
  dependencyStates.set(name, factory.createIdentifier(name))
@@ -2143,12 +2206,12 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
2143
2206
  hasDerivedDependency = true
2144
2207
  } else {
2145
2208
  subscriptionDependencies.push(dependency)
2146
- dependencyExpressions.push(["state", dependency.text])
2209
+ dependencyDerived.push({ expression: ["state", dependency.text], states: [dependency.text], source: dependency })
2147
2210
  dependencyStates.set(dependency.text, dependency)
2148
2211
  }
2149
2212
  }
2150
2213
  if (!hasDerivedDependency) {
2151
- dependencyExpressions.length = 0
2214
+ dependencyDerived.length = 0
2152
2215
  dependencyStates.clear()
2153
2216
  }
2154
2217
  if (!effectOwner) fail(node, "useEffect() cannot be used outside a Kudzu component")
@@ -2193,6 +2256,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
2193
2256
  reducers: reducersForNode(node, reducersByFunction),
2194
2257
  importBindings: specializedEffect?.imports ?? importBindings,
2195
2258
  listItem: dependencyItem,
2259
+ keyedBlock: activeKeyedBlock?.block.slot,
2196
2260
  deferValues: true,
2197
2261
  snapshotNested: returns.cleanup,
2198
2262
  liveStates: customHookTimerStates
@@ -2200,6 +2264,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
2200
2264
  for (const reference of workerReferences.slice(workerStart)) Object.assign(reference, { module: handlerUrl, handler: descriptor.exportName })
2201
2265
  usesListItem ||= Boolean(itemDependencies.length && !listEffect)
2202
2266
  usesBehavior = true
2267
+ const derivedDependencies = hasDerivedDependency ? dependencyDerived.map(entry => descriptors.registerDerived("expression", entry.expression, entry.states, entry.source)) : []
2203
2268
  return factory.updateCallExpression(node, node.expression, node.typeArguments, [
2204
2269
  callback,
2205
2270
  factory.createArrayLiteralExpression(hasDerivedDependency ? subscriptionDependencies : ordinaryDependencies),
@@ -2210,7 +2275,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
2210
2275
  factory.createStringLiteral(specializedEffect ? sourceLocation(specializedEffect.source, specializedEffect.sourceFile) : sourceLocation(node, sourceFile)),
2211
2276
  returns.cleanup ? factory.createTrue() : factory.createFalse(),
2212
2277
  factory.createArrayLiteralExpression(itemDependencies.map(field => factory.createStringLiteral(field))),
2213
- hasDerivedDependency ? jsonExpression(dependencyExpressions, factory) : factory.createArrayLiteralExpression(),
2278
+ hasDerivedDependency ? jsonExpression(derivedDependencies.map(entry => entry.expression), factory) : factory.createArrayLiteralExpression(),
2214
2279
  factory.createArrayLiteralExpression([...dependencyStates].map(([name, state]) => factory.createArrayLiteralExpression([factory.createStringLiteral(name), state])))
2215
2280
  ])
2216
2281
  }
@@ -2225,7 +2290,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
2225
2290
  return factory.updateVariableDeclaration(node, node.name, node.exclamationToken, node.type, initializer)
2226
2291
  }
2227
2292
 
2228
- if (ts.isVariableDeclaration(node) && listLocalDeclarations.has(node)) {
2293
+ if (ts.isVariableDeclaration(node) && listLocalDeclarations.includes(node)) {
2229
2294
  return factory.updateVariableDeclaration(node, node.name, node.exclamationToken, node.type, factory.createIdentifier("undefined"))
2230
2295
  }
2231
2296
 
@@ -2250,46 +2315,31 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
2250
2315
  if (compiled !== node.expression) return factory.updateReturnStatement(node, compiled)
2251
2316
  }
2252
2317
 
2253
- if (ts.isJsxExpression(node) && node.expression && listConditions.has(node.expression)) {
2254
- const entry = listConditions.get(node.expression)
2318
+ const listCondition = ts.isJsxExpression(node) && node.expression ? keyedEntry(activeKeyedBlock?.analysis.conditions ?? [], node.expression) : undefined
2319
+ if (listCondition) {
2320
+ const entry = listCondition.value
2255
2321
  return factory.updateJsxExpression(node, descriptors.compileListConditional({
2256
2322
  ...entry,
2323
+ keyedBlock: activeKeyedBlock.block.slot,
2257
2324
  truthy: ts.visitNode(entry.truthy, visitor),
2258
2325
  falsy: ts.visitNode(entry.falsy, visitor)
2259
2326
  }))
2260
2327
  }
2261
2328
 
2262
- if (ts.isJsxExpression(node) && node.expression && listValues.has(node.expression)) {
2263
- return factory.updateJsxExpression(node, descriptors.compileListValue(node.expression, listValues.get(node.expression)))
2329
+ const listValue = ts.isJsxExpression(node) && node.expression ? keyedEntry(activeKeyedBlock?.analysis.values ?? [], node.expression) : undefined
2330
+ if (listValue) {
2331
+ return factory.updateJsxExpression(node, descriptors.compileListValue(node.expression, { ...listValue.value, keyedBlock: activeKeyedBlock.block.slot }))
2264
2332
  }
2265
2333
 
2266
- if (ts.isJsxAttribute(node) && node.initializer && ts.isJsxExpression(node.initializer) && node.initializer.expression && listValues.has(node.initializer.expression)) {
2267
- return factory.updateJsxAttribute(node, node.name, factory.createJsxExpression(undefined, descriptors.compileListValue(node.initializer.expression, listValues.get(node.initializer.expression))))
2334
+ const attributeListValue = ts.isJsxAttribute(node) && node.initializer && ts.isJsxExpression(node.initializer) && node.initializer.expression ? keyedEntry(activeKeyedBlock?.analysis.values ?? [], node.initializer.expression) : undefined
2335
+ if (attributeListValue) {
2336
+ return factory.updateJsxAttribute(node, node.name, factory.createJsxExpression(undefined, descriptors.compileListValue(node.initializer.expression, { ...attributeListValue.value, keyedBlock: activeKeyedBlock.block.slot })))
2268
2337
  }
2269
2338
 
2270
2339
  if (ts.isJsxExpression(node) && node.initializer === undefined && node.expression && (ts.isJsxElement(node.parent) || ts.isJsxFragment(node.parent))) {
2271
- const nestedParts = nestedLists.get(unwrapExpression(node.expression))
2272
- const listParts = renderedLists.get(node) ?? nestedParts
2273
- if (listParts) {
2274
- usesBehavior = true
2275
- usesList = true
2276
- let listSource = listParts.state
2277
- if (listParts.calculation) {
2278
- usesBinding = true
2279
- listSource = descriptors.compileReactiveBinding(listParts.calculation, { setters: settersForNode(node, settersByFunction), importBindings })
2280
- }
2281
- const arguments_ = [
2282
- listSource,
2283
- listParts.keyField === null ? factory.createNull() : factory.createStringLiteral(listParts.keyField),
2284
- visitWithStateOwners(listParts.callback, listParts.analysisStateOwners ?? new Map()),
2285
- factory.createStringLiteral(listParts.ownerField ?? ""),
2286
- jsonExpression(listParts.selector ?? [], factory),
2287
- listParts.indexed ? factory.createTrue() : factory.createFalse()
2288
- ]
2289
- if (listParts.selectorStates?.size || listParts.static) arguments_.push(factory.createArrayLiteralExpression([...(listParts.selectorStates ?? [])].map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), factory.createIdentifier(name)]))))
2290
- if (listParts.static) arguments_.push(factory.createTrue())
2291
- return factory.updateJsxExpression(node, factory.createCallExpression(factory.createIdentifier("__kList"), undefined, arguments_))
2292
- }
2340
+ const renderedList = keyedEntry(preparedRenderedLists, node)
2341
+ const nestedList = keyedEntry(activeKeyedBlock?.analysis.nested ?? [], unwrapExpression(node.expression))
2342
+ if (renderedList || nestedList) return compileKeyedBlock(node, renderedList ?? nestedList)
2293
2343
  const conditional = conditionalParts(node.expression)
2294
2344
  if (conditional) {
2295
2345
  const compiled = compileRenderExpression(node.expression, node)
@@ -2328,7 +2378,8 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
2328
2378
  setters,
2329
2379
  reducers: reducersForNode(node, reducersByFunction),
2330
2380
  functions: functionsForNode(node),
2331
- listItem: listEventItems.get(node),
2381
+ listItem: activeKeyedBlock ? { item: activeKeyedBlock.parts.item, index: activeKeyedBlock.parts.index } : undefined,
2382
+ keyedBlock: activeKeyedBlock?.block.slot,
2332
2383
  importBindings: new Map([...importBindings, ...packageBindings])
2333
2384
  })
2334
2385
  if (event) {
@@ -2344,10 +2395,11 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
2344
2395
  }
2345
2396
 
2346
2397
  const transformed = ts.visitNode(sourceFile, visitor)
2398
+ descriptors.finalize()
2347
2399
  if (!usesBehavior) return transformed
2348
2400
 
2349
2401
  const behaviorImports = [factory.createImportSpecifier(false, factory.createIdentifier("behavior"), factory.createIdentifier("__kBehavior"))]
2350
- if (nativeHandlers.length) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("nativeBehavior"), factory.createIdentifier("__kNativeBehavior")))
2402
+ if (moduleIR.handlers.some(handler => handler.kind === "module-export" && handler.role === "native")) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("nativeBehavior"), factory.createIdentifier("__kNativeBehavior")))
2351
2403
  if (usesBinding) {
2352
2404
  behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("binding"), factory.createIdentifier("__kBinding")))
2353
2405
  behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("select"), factory.createIdentifier("__kSelect")))
@@ -2562,10 +2614,11 @@ function insideJsxEventHandler(node, root) {
2562
2614
  return false
2563
2615
  }
2564
2616
 
2565
- function validateKeyedList(parts, sourceFile, listValues, listEventItems, listConditions, setters, rowStates, nestedLists, componentSpecializations, expandedRowSpecializations, nestedRowSpecializations, factory, prepareListCallback) {
2617
+ function validateKeyedList(parts, sourceFile, setters, rowStates, componentSpecializations, expandedRowSpecializations, nestedRowSpecializations, factory, prepareListCallback) {
2566
2618
  const fail = (node, message) => {
2567
2619
  throw sourceNodeError(node, sourceFile, message)
2568
2620
  }
2621
+ const analysis = { values: [], conditions: [], nested: [], effects: parts.effectEntries ?? [] }
2569
2622
  const root = parts.root
2570
2623
  const item = parts.item
2571
2624
  const nestedDiagnostic = "Nested keyed list collections must be a direct property of the parent item"
@@ -2580,7 +2633,6 @@ function validateKeyedList(parts, sourceFile, listValues, listEventItems, listCo
2580
2633
  if (node !== root && ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "map" && containsJsx(node)) fail(node, nestedDiagnostic)
2581
2634
  if (ts.isJsxSpreadAttribute(node) && referencesIdentifier(node.expression, item)) fail(node, "Keyed list item spreads are not supported")
2582
2635
  if (ts.isJsxAttribute(node) && /^on[A-Z]/.test(node.name.text)) {
2583
- listEventItems.set(node, { item, index: parts.index })
2584
2636
  return
2585
2637
  }
2586
2638
  if (ts.isJsxExpression(node) && node.expression) {
@@ -2605,16 +2657,28 @@ function validateKeyedList(parts, sourceFile, listValues, listEventItems, listCo
2605
2657
  ts.setParentRecursive(callback, false)
2606
2658
  callback.parent = nested.callback.parent
2607
2659
  }
2608
- callback = prepareListCallback(callback, root, specialization ?? { hookDeclarations: [], effects: [] }, nested.item)
2660
+ const effectEntries = []
2661
+ callback = prepareListCallback(callback, root, specialization ?? { hookDeclarations: [], effects: [] }, nested.item, effectEntries)
2609
2662
  const specializedStates = [...(specialization?.rowStates ?? []), ...(specialization?.ordinaryStates ?? [])]
2610
- const nestedParts = { ...nested, root, callback, state: parts.state, nested: true, analysisStateOwners: new Map([...(parts.analysisStateOwners ?? []), ...specializedStates.map(state => [state.state, state.analysisOwner])]) }
2663
+ const nestedParts = {
2664
+ ...nested,
2665
+ root,
2666
+ callback,
2667
+ state: parts.state,
2668
+ nested: true,
2669
+ effectEntries,
2670
+ specializations: [specialization?.analysis?.slot, ...(specialization?.specializations ?? [])].filter(slot => slot !== undefined),
2671
+ rowStates: specializedStates,
2672
+ rowRefs: specialization?.rowRefs ?? [],
2673
+ analysisStateOwners: new Map([...(parts.analysisStateOwners ?? []), ...specializedStates.map(state => [state.state, state.analysisOwner])])
2674
+ }
2611
2675
  for (const calculation of specialization?.calculations ?? []) {
2612
2676
  ts.setParentRecursive(calculation, false)
2613
2677
  calculation.parent = callback
2614
2678
  validateListExpression(calculation, nested.item, nested.root, fail)
2615
2679
  }
2616
- nestedLists.set(expression, nestedParts)
2617
- validateKeyedList(nestedParts, sourceFile, listValues, listEventItems, listConditions, setters, specialization?.rowStates ?? [], nestedLists, componentSpecializations, expandedRowSpecializations, nestedRowSpecializations, factory, prepareListCallback)
2680
+ const nestedAnalysis = validateKeyedList(nestedParts, sourceFile, setters, specialization?.rowStates ?? [], componentSpecializations, expandedRowSpecializations, nestedRowSpecializations, factory, prepareListCallback)
2681
+ analysis.nested.push({ node: expression, parts: nestedParts, analysis: nestedAnalysis })
2618
2682
  return
2619
2683
  }
2620
2684
  const condition = conditionalParts(expression)
@@ -2626,7 +2690,7 @@ function validateKeyedList(parts, sourceFile, listValues, listEventItems, listCo
2626
2690
  }
2627
2691
  if (!referencesIdentifier(condition.condition, item) && !(parts.index && referencesIdentifier(condition.condition, parts.index))) fail(node, "Keyed list item conditions must read the item or index")
2628
2692
  validateListExpression(condition.condition, item, node, fail, parts.index)
2629
- listConditions.set(node.expression, { ...condition, item, index: parts.index })
2693
+ analysis.conditions.push({ node: node.expression, value: { ...condition, item, index: parts.index } })
2630
2694
  visit(condition.truthy)
2631
2695
  visit(condition.falsy)
2632
2696
  return
@@ -2637,7 +2701,7 @@ function validateKeyedList(parts, sourceFile, listValues, listEventItems, listCo
2637
2701
  if (field && ts.isJsxAttribute(node.parent) && ["ref", "dangerouslysetinnerhtml"].includes(node.parent.name.text.toLowerCase())) fail(node, `Keyed list item ${node.parent.name.text} is not supported`)
2638
2702
  if (isRootKey) return
2639
2703
  if (field) {
2640
- listValues.set(node.expression, { field })
2704
+ analysis.values.push({ node: node.expression, value: { field } })
2641
2705
  return
2642
2706
  }
2643
2707
  if (referencesIdentifier(expression, item) || parts.index && referencesIdentifier(expression, parts.index)) {
@@ -2646,13 +2710,14 @@ function validateKeyedList(parts, sourceFile, listValues, listEventItems, listCo
2646
2710
  if (parts.nested && states.size) fail(node, "Nested keyed list item expressions cannot read parent state")
2647
2711
  validateListExpression(expression, item, node, fail, parts.index, states)
2648
2712
  if (ts.isJsxAttribute(node.parent) && ["ref", "dangerouslysetinnerhtml"].includes(node.parent.name.text.toLowerCase())) fail(node, `Keyed list item ${node.parent.name.text} is not supported`)
2649
- listValues.set(node.expression, { item, index: parts.index, states })
2713
+ analysis.values.push({ node: node.expression, value: { item, index: parts.index, states } })
2650
2714
  return
2651
2715
  }
2652
2716
  }
2653
2717
  ts.forEachChild(node, visit)
2654
2718
  }
2655
2719
  visit(root)
2720
+ return analysis
2656
2721
  }
2657
2722
 
2658
2723
  function directConstObjectLiteral(expression, call) {
@@ -3885,9 +3950,8 @@ const workerCompiler = createWorkerCompiler({
3885
3950
  })
3886
3951
 
3887
3952
  const printEffectEntry = createEffectCodegen({ assetPath, inlineJson, relativeModulePath })
3953
+ const handlerLowering = createHandlerLowering({ cloneAst, synthesizeTree })
3888
3954
  const printHandlerModule = createHandlerCodegen({
3889
- cloneAst,
3890
- synthesizeTree,
3891
3955
  resolveClientImport: (entry, handlerPath) => entry.package ? entry.target : relativeModulePath(handlerPath, clientModulePath(entry.target))
3892
3956
  })
3893
3957
  const { normalizeReactMigrationSyntax, validateUseIdSyntax } = createReactMigrationPass({ cloneAst, jsxTagName })