@kudzujs/core 0.8.19 → 0.8.21

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.
@@ -11,6 +11,7 @@ import { normalizeMediaQueryExternalStores, normalizeNavigatorCapabilityConditio
11
11
  import { analyzeCollectionPipeline, collectionExpression, collectionParameters, isArrayFromCall, mutatingCollectionMethods as mutatingListMethods, pureCollectionMathMethods as pureMathMethods, pureCollectionMethods as pureListMethods } from "./compiler/collection-analysis.mjs"
12
12
  import { normalizeCustomHookTimerRefs } from "./compiler/custom-hook-timer-pass.mjs"
13
13
  import { captureNames, createDescriptorSession, createSemanticArtifact, nativeCaptureNames, referencedReducerDispatches, referencedStateNames } from "./compiler/descriptor-session.mjs"
14
+ import { analyzeEffectDependencies, validateEffectOwnedBrowserResources } from "./compiler/effect-analysis.mjs"
14
15
  import { createEffectCodegen } from "./compiler/effect-codegen.mjs"
15
16
  import { createHandlerCodegen } from "./compiler/handler-codegen.mjs"
16
17
  import { createHandlerLowering } from "./compiler/handler-lowering.mjs"
@@ -75,12 +76,16 @@ export async function build({ quiet = false, minify = true } = {}) {
75
76
  const { cssModules, cssOutputs } = await prepareSourceStyles(cssFiles, staticFiles, importedAssets, base)
76
77
 
77
78
  const sourceResults = []
78
- const workerReferences = []
79
79
  for (const file of sourceFiles) {
80
80
  if (file.endsWith(".worker.ts")) continue
81
- sourceResults.push(await compile(file, sourceFileSet, sourceIndex, staticFiles, importedAssets, cssModules, base, workerReferences))
81
+ sourceResults.push(await compile(file, sourceFileSet, sourceIndex, staticFiles, importedAssets, cssModules, base))
82
82
  }
83
83
  const handlerModules = sourceResults.flatMap(result => result.handlerModule ? [result.handlerModule] : [])
84
+ const workerReferences = sourceResults.flatMap(result => result.moduleIR.effects.flatMap(effect => {
85
+ const handler = result.moduleIR.handlers[effect.setup.handler]
86
+ if (!handler || handler.kind !== "module-export" || handler.role !== "effect") throw new Error(`EffectIR ${effect.slot} has no effect HandlerIR`)
87
+ return effect.workers.map(worker => ({ ...worker, module: assetPath(base, `assets/${result.handlerModule.path}`), handler: handler.exportName }))
88
+ }))
84
89
 
85
90
  const plans = []
86
91
  const routeCapabilities = new Map()
@@ -678,7 +683,7 @@ function escapeAttribute(value) {
678
683
  return escapeHtml(value).replaceAll('"', """).replaceAll("'", "'")
679
684
  }
680
685
 
681
- async function compile(file, sourceFiles, sourceIndex, staticFiles, importedAssets, cssModules, base, workerReferences) {
686
+ async function compile(file, sourceFiles, sourceIndex, staticFiles, importedAssets, cssModules, base) {
682
687
  const source = sourceIndex.get(file)
683
688
  const semantic = createSemanticArtifact(relative(root, file).replaceAll(sep, "/"))
684
689
  const handlerPath = `handlers/${relative(sourceDirectory, file).replaceAll(sep, "/").replace(/\.(?:ts|tsx)$/, ".js")}`
@@ -690,7 +695,7 @@ async function compile(file, sourceFiles, sourceIndex, staticFiles, importedAsse
690
695
  jsx: ts.JsxEmit.ReactJSX,
691
696
  jsxImportSource: "@kudzujs/core"
692
697
  },
693
- transformers: { before: [createKudzuTransformer({ semantic, handlerUrl: assetPath(base, `assets/${handlerPath}`), file, sourceFiles, sourceIndex, staticFiles, importedAssets, cssModules, base, workerReferences })] },
698
+ transformers: { before: [createKudzuTransformer({ semantic, handlerUrl: assetPath(base, `assets/${handlerPath}`), file, sourceFiles, sourceIndex, staticFiles, importedAssets, cssModules, base })] },
694
699
  reportDiagnostics: true
695
700
  })
696
701
 
@@ -882,7 +887,7 @@ function normalizeCompilerSource(sourceFile, { base, context, file, importedColl
882
887
  return { sourceFile, customHookTimerStates }
883
888
  }
884
889
 
885
- function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourceIndex, staticFiles, importedAssets, cssModules, base, workerReferences }) {
890
+ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourceIndex, staticFiles, importedAssets, cssModules, base }) {
886
891
  const { moduleIR } = semantic
887
892
  return context => sourceFile => {
888
893
  const hasLinkElements = /<link/i.test(sourceFile.text)
@@ -952,14 +957,8 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
952
957
  const customHooks = new Map()
953
958
  const jsxLocalDeclarations = new Map()
954
959
  const jsxLocalsByFunction = new Map()
955
- const listLocalDeclarations = new WeakSet()
956
- const listLocalUses = new WeakMap()
957
- const listValues = new WeakMap()
958
- const listEventItems = new WeakMap()
959
- const listConditions = new WeakMap()
960
- const nestedLists = new WeakMap()
961
- const listEffectEntries = new WeakMap()
962
- const componentEffectEntries = new WeakMap()
960
+ const listLocalDeclarations = []
961
+ const listLocalUses = []
963
962
  const analysisSource = node => {
964
963
  const original = ts.getOriginalNode(node)
965
964
  return original.pos >= 0 && original.end >= 0 ? { file: sourceName(original.getSourceFile()), start: original.getStart(), end: original.end } : undefined
@@ -1340,8 +1339,8 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1340
1339
  const position = sourceFile.getLineAndCharacterOfPosition(declaration.node.getStart(sourceFile))
1341
1340
  if (uses.length > 1) throw new Error(`${sourceFile.fileName}:${position.line + 1}:${position.character + 1} Keyed list local "${name}" must be rendered exactly once`)
1342
1341
  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`)
1343
- listLocalDeclarations.add(declaration.node)
1344
- if (uses.length) listLocalUses.set(uses[0], parts)
1342
+ listLocalDeclarations.push(declaration.node)
1343
+ if (uses.length) listLocalUses.push({ node: uses[0], parts })
1345
1344
  }
1346
1345
  }
1347
1346
  }
@@ -1484,6 +1483,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1484
1483
  ids: result.ordinaryIds.map(({ name, source }) => ({ name, ...(analysisSource(source) ? { source: analysisSource(source) } : {}) }))
1485
1484
  })
1486
1485
  for (const state of [...result.rowStates, ...result.ordinaryStates]) state.analysisOwner = `specialization:${result.analysis.slot}`
1486
+ for (const ref of [...result.rowRefs, ...result.ordinaryRefs]) ref.analysisOwner = `specialization:${result.analysis.slot}`
1487
1487
  return result
1488
1488
  }
1489
1489
  const registerRowHooks = (call, specialization) => {
@@ -1728,8 +1728,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1728
1728
  const effectStatements = specialization.effects.map(entry => {
1729
1729
  const effectCall = factory.updateCallExpression(entry.call, factory.createIdentifier("__kComponentUseEffect"), entry.call.typeArguments, entry.call.arguments)
1730
1730
  synthesizeTree(effectCall)
1731
- const effectSource = entry.source.getSourceFile()
1732
- componentEffectEntries.set(effectCall, { source: entry.source, sourceFile: effectSource, imports: clientImportBindings(effectSource, effectSource.fileName, sourceFiles) })
1731
+ ts.setOriginalNode(effectCall, entry.source)
1733
1732
  return factory.createExpressionStatement(effectCall)
1734
1733
  })
1735
1734
  const helper = factory.createFunctionDeclaration(
@@ -1847,20 +1846,20 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1847
1846
  if (expanded === value || !referencedStateNames(expanded, setters).size) fail(value, "Calculated collection fields must directly depend on local state")
1848
1847
  return expanded
1849
1848
  }
1850
- const parts = listLocalUses.get(node) ?? keyedListParts(node.expression, setters, jsxLocalDeclarations.get(owner), fail, new Set(), importedCollections, factory, context, importedCollectionTransforms, calculatedCollection, staticCollection)
1849
+ 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)
1851
1850
  if (parts) {
1852
- for (const declaration of parts.aliasDeclarations ?? []) listLocalDeclarations.add(declaration)
1851
+ for (const declaration of parts.aliasDeclarations ?? []) if (!listLocalDeclarations.includes(declaration)) listLocalDeclarations.push(declaration)
1853
1852
  rawRenderedLists.push({ node, parts })
1854
1853
  }
1855
1854
  }
1856
1855
  ts.forEachChild(node, collectRenderedLists)
1857
1856
  }
1858
1857
  collectRenderedLists(sourceFile)
1859
- const collectionAliasUses = new WeakSet(rawRenderedLists.flatMap(({ parts }) => parts.aliasUses ?? []))
1858
+ const collectionAliasUses = rawRenderedLists.flatMap(({ parts }) => parts.aliasUses ?? [])
1860
1859
  const collectionAliasDeclarations = new Set(rawRenderedLists.flatMap(({ parts }) => parts.aliasDeclarations ?? []))
1861
1860
  for (const declaration of collectionAliasDeclarations) {
1862
1861
  const owner = nearestFunction(declaration)
1863
- const unsupported = identifierReferences(owner.body, declaration.name.text).find(reference => !collectionAliasUses.has(reference))
1862
+ const unsupported = identifierReferences(owner.body, declaration.name.text).find(reference => !collectionAliasUses.includes(reference))
1864
1863
  if (unsupported) fail(unsupported, `Rendered collection alias "${declaration.name.text}" may only be used as a rendered collection source`)
1865
1864
  }
1866
1865
  const rejectUnsupportedRenderControl = node => {
@@ -1912,7 +1911,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1912
1911
  let count = 0
1913
1912
  const visit = (node, currentAggregate = aggregate) => {
1914
1913
  if (node !== root && ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "map" && containsJsx(node)) {
1915
- const nestedAggregate = { calculations: [], effects: [], hookDeclarations: [], rowStates: [], rowRefs: [] }
1914
+ const nestedAggregate = { calculations: [], effects: [], hookDeclarations: [], rowStates: [], rowRefs: [], specializations: [] }
1916
1915
  for (const argument of node.arguments) visit(argument, nestedAggregate)
1917
1916
  if (nestedAggregate.hookDeclarations.length || nestedAggregate.effects.length) nestedRowSpecializations.set(`${node.pos}:${node.end}`, nestedAggregate)
1918
1917
  return
@@ -1939,6 +1938,8 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1939
1938
  if (imported) synthesizeTree(specialization.root = mergeSpecializedImports(specialization.root, component.getSourceFile(), node, specialization.effects))
1940
1939
  expandedRowSpecializations.set(specialization.root, specialization)
1941
1940
  if (currentAggregate) {
1941
+ currentAggregate.specializations ??= []
1942
+ currentAggregate.specializations.push(specialization.analysis.slot, ...(specialization.specializations ?? []))
1942
1943
  currentAggregate.effects.push(...specialization.effects)
1943
1944
  currentAggregate.hookDeclarations.push(...specialization.hookDeclarations)
1944
1945
  currentAggregate.rowStates.push(...specialization.rowStates)
@@ -1957,16 +1958,15 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1957
1958
  expanded.parent = root.parent
1958
1959
  return expanded
1959
1960
  }
1960
- const renderedLists = new WeakMap()
1961
- const prepareListCallback = (callback, root, specialization, item) => {
1961
+ const preparedRenderedLists = []
1962
+ const prepareListCallback = (callback, root, specialization) => {
1962
1963
  const statements = [...specialization.hookDeclarations]
1963
1964
  if (specialization.effects.length) {
1964
1965
  usesListEffects = true
1965
1966
  statements.push(...specialization.effects.map(entry => {
1966
1967
  const call = factory.updateCallExpression(entry.call, factory.createIdentifier("__kListUseEffect"), entry.call.typeArguments, entry.call.arguments)
1967
1968
  synthesizeTree(call)
1968
- const effectSource = entry.source.getSourceFile()
1969
- listEffectEntries.set(call, { item, source: entry.source, sourceFile: effectSource, imports: clientImportBindings(effectSource, effectSource.fileName, sourceFiles) })
1969
+ ts.setOriginalNode(call, entry.source)
1970
1970
  return factory.createExpressionStatement(call)
1971
1971
  }))
1972
1972
  }
@@ -1997,15 +1997,23 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1997
1997
  ts.setParentRecursive(callback, false)
1998
1998
  callback.parent = originalParts.callback.parent
1999
1999
  }
2000
- callback = prepareListCallback(callback, root, specialization, originalParts.item)
2001
- const parts = { ...originalParts, root, callback, analysisStateOwners: new Map([...stateOwnersForNode(originalParts.root), ...[...specialization.rowStates, ...specialization.ordinaryStates].map(state => [state.state, state.analysisOwner])]) }
2000
+ callback = prepareListCallback(callback, root, specialization)
2001
+ const parts = {
2002
+ ...originalParts,
2003
+ root,
2004
+ callback,
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
+ }
2002
2010
  for (const calculation of specialization.calculations) {
2003
2011
  ts.setParentRecursive(calculation, false)
2004
2012
  calculation.parent = callback
2005
2013
  validateListExpression(calculation, parts.item, originalParts.root, fail)
2006
2014
  }
2007
- validateKeyedList(parts, sourceFile, listValues, listEventItems, listConditions, settersForNode(originalParts.root, settersByFunction), specialization.rowStates, nestedLists, componentSpecializations, expandedRowSpecializations, nestedRowSpecializations, factory, prepareListCallback)
2008
- 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 })
2009
2017
  }
2010
2018
 
2011
2019
  const compileRenderExpression = (expression, anchor) => {
@@ -2027,6 +2035,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
2027
2035
  }
2028
2036
 
2029
2037
  let activeStateOwners
2038
+ let activeKeyedBlock
2030
2039
  const visitWithStateOwners = (node, stateOwners) => {
2031
2040
  const previous = activeStateOwners
2032
2041
  activeStateOwners = new Map([...(previous ?? []), ...stateOwners])
@@ -2034,6 +2043,58 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
2034
2043
  activeStateOwners = previous
2035
2044
  return result
2036
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
+ }
2037
2098
  const visitor = node => {
2038
2099
  if (ts.isVariableDeclaration(node) && ts.isObjectBindingPattern(node.name) && customHookPrivateFields.has(node)) {
2039
2100
  const privateFields = customHookPrivateFields.get(node)
@@ -2077,10 +2138,14 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
2077
2138
  return factory.updateExportDeclaration(node, node.modifiers, node.isTypeOnly, node.exportClause, factory.createStringLiteral(relativeModulePath(compiledPath(file), compiledPath(target))), node.attributes)
2078
2139
  }
2079
2140
 
2080
- const listEffect = ts.isCallExpression(node) ? listEffectEntries.get(node) : undefined
2081
- const componentEffect = ts.isCallExpression(node) ? componentEffectEntries.get(node) : undefined
2082
- const specializedEffect = listEffect ?? componentEffect
2083
- if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && (hasUseEffectImport && node.expression.text === "useEffect" || specializedEffect)) {
2141
+ const effectAlias = ts.isCallExpression(node) && ts.isIdentifier(node.expression) ? node.expression.text : undefined
2142
+ const listEffect = effectAlias === "__kListUseEffect"
2143
+ const specializedEffect = listEffect || effectAlias === "__kComponentUseEffect" ? (() => {
2144
+ const source = ts.getOriginalNode(node)
2145
+ const sourceFile = source.getSourceFile()
2146
+ return { source, sourceFile, imports: clientImportBindings(sourceFile, sourceFile.fileName, sourceFiles) }
2147
+ })() : undefined
2148
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && (hasUseEffectImport && effectAlias === "useEffect" || specializedEffect)) {
2084
2149
  const effectFail = (target, message) => {
2085
2150
  if (specializedEffect) throw sourceNodeError(specializedEffect.source, specializedEffect.sourceFile, message)
2086
2151
  fail(target, message)
@@ -2101,58 +2166,18 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
2101
2166
  if (callback.asteriskToken) effectFail(callback, "useEffect() callback cannot be a generator")
2102
2167
  if (callback.parameters.length) effectFail(callback, "useEffect() callback cannot declare parameters")
2103
2168
  if (!ts.isArrayLiteralExpression(dependencies)) effectFail(dependencies, "useEffect() dependencies must be a literal array")
2104
- const itemDependencies = []
2105
- const ordinaryDependencies = []
2106
2169
  const setters = settersForNode(node, settersByFunction)
2107
- let dependencyItem = listEffect?.item
2108
- for (const dependency of dependencies.elements) {
2109
- const value = unwrapExpression(dependency)
2110
- if (!dependencyItem && ts.isPropertyAccessExpression(value) && ts.isIdentifier(value.expression) && isDestructuredParameter(value.expression, nearestFunction(node))) dependencyItem = value.expression.text
2111
- const field = dependencyItem && directProperty(dependency, dependencyItem)
2112
- if (field) {
2113
- if (["__proto__", "constructor", "prototype"].includes(field)) effectFail(dependency, `useEffect() keyed item property "${field}" is not supported`)
2114
- itemDependencies.push(field)
2115
- } else if (dependencyItem && referencesIdentifier(dependency, dependencyItem)) {
2116
- effectFail(dependency, "useEffect() keyed item dependencies must be direct item.<field> properties")
2117
- } else {
2118
- ordinaryDependencies.push(dependency)
2119
- }
2120
- }
2121
- const invalidDependency = ordinaryDependencies.find(dependency => !ts.isIdentifier(dependency))
2122
- if (invalidDependency) effectFail(invalidDependency, "useEffect() dependencies must be direct state or runtime parameter identifiers")
2123
- const dependencyDerived = []
2124
- const dependencyStates = new Map()
2125
- const dependencySubstitutions = new Map()
2126
- const subscriptionDependencies = []
2127
- let hasDerivedDependency = false
2128
- const stateNames = new Set(setters.values())
2129
- const localDeclarations = jsxLocalDeclarations.get(nearestFunction(node))
2130
- for (const dependency of ordinaryDependencies) {
2131
- const entries = localDeclarations?.get(dependency.text)
2132
- const initializer = entries?.length === 1 ? entries[0].initializer : undefined
2133
- const directAlias = initializer && ts.isIdentifier(unwrapExpression(initializer)) && stateNames.has(unwrapExpression(initializer).text)
2134
- const derivedStates = initializer && !directAlias ? referencedStateNames(initializer, setters) : new Set()
2135
- if (derivedStates.size) {
2136
- const usedStates = new Set()
2137
- const expression = collectionExpression(initializer, { fail: effectFail, stateNames, selectorStates: usedStates })
2138
- if (!usedStates.size) effectFail(dependency, `useEffect() derived dependency "${dependency.text}" must read direct primitive state`)
2139
- dependencyDerived.push({ expression, states: usedStates, source: initializer })
2140
- for (const name of usedStates) {
2141
- subscriptionDependencies.push(factory.createIdentifier(name))
2142
- dependencyStates.set(name, factory.createIdentifier(name))
2143
- }
2144
- dependencySubstitutions.set(dependency.text, initializer)
2145
- hasDerivedDependency = true
2146
- } else {
2147
- subscriptionDependencies.push(dependency)
2148
- dependencyDerived.push({ expression: ["state", dependency.text], states: [dependency.text], source: dependency })
2149
- dependencyStates.set(dependency.text, dependency)
2150
- }
2151
- }
2152
- if (!hasDerivedDependency) {
2153
- dependencyDerived.length = 0
2154
- dependencyStates.clear()
2155
- }
2170
+ const dependencyAnalysis = analyzeEffectDependencies({
2171
+ dependencies,
2172
+ node,
2173
+ listEffect,
2174
+ keyedItem: activeKeyedBlock?.parts.item,
2175
+ setters,
2176
+ localDeclarations: jsxLocalDeclarations.get(nearestFunction(node)),
2177
+ factory,
2178
+ fail: effectFail
2179
+ })
2180
+ const { dependencyItem, itemDependencies, ordinaryDependencies, entries: dependencyEntries, dependencyStates, substitutions: dependencySubstitutions, subscriptions: subscriptionDependencies, hasDerived: hasDerivedDependency } = dependencyAnalysis
2156
2181
  if (!effectOwner) fail(node, "useEffect() cannot be used outside a Kudzu component")
2157
2182
  if (!ts.isBlock(callback.body)) effectFail(callback, "useEffect() callback must use a block body")
2158
2183
  const cleanupSubstitutions = new Map()
@@ -2178,43 +2203,62 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
2178
2203
  validateEffectOwnedBrowserResources(callback, returns, effectFail)
2179
2204
  const callbackSource = specializedEffect?.sourceFile ?? sourceFile
2180
2205
  const callbackFile = callbackSource.fileName
2181
- const workerStart = workerReferences.length
2182
2206
  let compiledCallback = dependencySubstitutions.size ? substituteClone(callback, dependencySubstitutions, factory, context) : callback
2183
2207
  if (compiledCallback !== callback) {
2184
2208
  ts.setParentRecursive(compiledCallback, false)
2185
2209
  compiledCallback.parent = callback.parent
2186
2210
  }
2211
+ let workers = []
2187
2212
  if (listEffect && callbackFile !== file) {
2188
- const originalCallback = listEffect.source.arguments[0]
2213
+ const originalCallback = specializedEffect.source.arguments[0]
2189
2214
  workerCompiler.rejectConstructions(originalCallback, callbackSource, "Relative TypeScript Worker construction in imported keyed-row effects is not supported; construct the Worker in a directly compiled page or local component effect")
2190
2215
  } else {
2191
- compiledCallback = workerCompiler.rewriteEffect(compiledCallback, callbackFile, callbackSource, sourceFiles, workerReferences, factory, context)
2216
+ const rewritten = workerCompiler.rewriteEffect(compiledCallback, callbackFile, callbackSource, sourceFiles, factory, context)
2217
+ compiledCallback = rewritten.callback
2218
+ workers = rewritten.workers
2192
2219
  }
2193
2220
  const descriptor = descriptors.compileEffectCallback(compiledCallback, {
2194
2221
  setters,
2195
2222
  reducers: reducersForNode(node, reducersByFunction),
2196
2223
  importBindings: specializedEffect?.imports ?? importBindings,
2197
2224
  listItem: dependencyItem,
2225
+ keyedBlock: activeKeyedBlock?.block.slot,
2198
2226
  deferValues: true,
2199
2227
  snapshotNested: returns.cleanup,
2200
2228
  liveStates: customHookTimerStates
2201
2229
  })
2202
- for (const reference of workerReferences.slice(workerStart)) Object.assign(reference, { module: handlerUrl, handler: descriptor.exportName })
2203
2230
  usesListItem ||= Boolean(itemDependencies.length && !listEffect)
2204
2231
  usesBehavior = true
2205
- const derivedDependencies = hasDerivedDependency ? dependencyDerived.map(entry => descriptors.registerDerived("expression", entry.expression, entry.states, entry.source)) : []
2232
+ const derivedDependencies = hasDerivedDependency ? dependencyEntries.map(entry => entry.kind === "derived" ? descriptors.registerDerived("expression", entry.expression, entry.states, entry.source) : undefined) : []
2233
+ const effectSource = specializedEffect?.source ?? node
2234
+ const lexicalOwner = nearestFunction(effectSource)
2235
+ const effect = descriptors.registerEffect(descriptor, {
2236
+ cleanup: returns.cleanup,
2237
+ dependencies: hasDerivedDependency ? dependencyEntries.map((entry, index) => entry.kind === "derived" ? { kind: "derived", derived: derivedDependencies[index].slot, sources: [...entry.states] } : { kind: "signal", name: entry.name }) : ordinaryDependencies.map(dependency => ({ kind: "signal", name: dependency.text })),
2238
+ subscriptions: (hasDerivedDependency ? subscriptionDependencies : ordinaryDependencies).map(dependency => dependency.text),
2239
+ dependencyStates: [...dependencyStates.keys()],
2240
+ itemDependencies,
2241
+ ownership: {
2242
+ kind: activeKeyedBlock ? "keyed" : "component",
2243
+ ...(activeKeyedBlock ? { keyedBlock: activeKeyedBlock.block.slot } : {}),
2244
+ ...(lexicalOwner ? { component: { name: ownerName(lexicalOwner), ...(analysisSource(lexicalOwner) ? { source: analysisSource(lexicalOwner) } : {}) } } : {})
2245
+ },
2246
+ workers,
2247
+ ...(analysisSource(effectSource) ? { source: analysisSource(effectSource) } : {})
2248
+ })
2249
+ const dependencyExpressions = effect.dependencies.map(dependency => dependency.kind === "derived" ? moduleIR.derived[dependency.derived].expression : ["state", dependency.name])
2206
2250
  return factory.updateCallExpression(node, node.expression, node.typeArguments, [
2207
2251
  callback,
2208
- factory.createArrayLiteralExpression(hasDerivedDependency ? subscriptionDependencies : ordinaryDependencies),
2252
+ factory.createArrayLiteralExpression(effect.subscriptions.map(name => factory.createIdentifier(name))),
2209
2253
  factory.createStringLiteral(handlerUrl),
2210
- factory.createStringLiteral(descriptor.exportName),
2254
+ factory.createStringLiteral(effect.setup.exportName),
2211
2255
  descriptor.states,
2212
2256
  descriptor.scope,
2213
2257
  factory.createStringLiteral(specializedEffect ? sourceLocation(specializedEffect.source, specializedEffect.sourceFile) : sourceLocation(node, sourceFile)),
2214
- returns.cleanup ? factory.createTrue() : factory.createFalse(),
2215
- factory.createArrayLiteralExpression(itemDependencies.map(field => factory.createStringLiteral(field))),
2216
- hasDerivedDependency ? jsonExpression(derivedDependencies.map(entry => entry.expression), factory) : factory.createArrayLiteralExpression(),
2217
- factory.createArrayLiteralExpression([...dependencyStates].map(([name, state]) => factory.createArrayLiteralExpression([factory.createStringLiteral(name), state])))
2258
+ effect.cleanup ? factory.createTrue() : factory.createFalse(),
2259
+ factory.createArrayLiteralExpression(effect.itemDependencies.map(field => factory.createStringLiteral(field))),
2260
+ hasDerivedDependency ? jsonExpression(dependencyExpressions, factory) : factory.createArrayLiteralExpression(),
2261
+ factory.createArrayLiteralExpression(effect.dependencyStates.map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), factory.createIdentifier(name)])))
2218
2262
  ])
2219
2263
  }
2220
2264
 
@@ -2228,7 +2272,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
2228
2272
  return factory.updateVariableDeclaration(node, node.name, node.exclamationToken, node.type, initializer)
2229
2273
  }
2230
2274
 
2231
- if (ts.isVariableDeclaration(node) && listLocalDeclarations.has(node)) {
2275
+ if (ts.isVariableDeclaration(node) && listLocalDeclarations.includes(node)) {
2232
2276
  return factory.updateVariableDeclaration(node, node.name, node.exclamationToken, node.type, factory.createIdentifier("undefined"))
2233
2277
  }
2234
2278
 
@@ -2253,47 +2297,31 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
2253
2297
  if (compiled !== node.expression) return factory.updateReturnStatement(node, compiled)
2254
2298
  }
2255
2299
 
2256
- if (ts.isJsxExpression(node) && node.expression && listConditions.has(node.expression)) {
2257
- const entry = listConditions.get(node.expression)
2300
+ const listCondition = ts.isJsxExpression(node) && node.expression ? keyedEntry(activeKeyedBlock?.analysis.conditions ?? [], node.expression) : undefined
2301
+ if (listCondition) {
2302
+ const entry = listCondition.value
2258
2303
  return factory.updateJsxExpression(node, descriptors.compileListConditional({
2259
2304
  ...entry,
2305
+ keyedBlock: activeKeyedBlock.block.slot,
2260
2306
  truthy: ts.visitNode(entry.truthy, visitor),
2261
2307
  falsy: ts.visitNode(entry.falsy, visitor)
2262
2308
  }))
2263
2309
  }
2264
2310
 
2265
- if (ts.isJsxExpression(node) && node.expression && listValues.has(node.expression)) {
2266
- return factory.updateJsxExpression(node, descriptors.compileListValue(node.expression, listValues.get(node.expression)))
2311
+ const listValue = ts.isJsxExpression(node) && node.expression ? keyedEntry(activeKeyedBlock?.analysis.values ?? [], node.expression) : undefined
2312
+ if (listValue) {
2313
+ return factory.updateJsxExpression(node, descriptors.compileListValue(node.expression, { ...listValue.value, keyedBlock: activeKeyedBlock.block.slot }))
2267
2314
  }
2268
2315
 
2269
- if (ts.isJsxAttribute(node) && node.initializer && ts.isJsxExpression(node.initializer) && node.initializer.expression && listValues.has(node.initializer.expression)) {
2270
- return factory.updateJsxAttribute(node, node.name, factory.createJsxExpression(undefined, descriptors.compileListValue(node.initializer.expression, listValues.get(node.initializer.expression))))
2316
+ const attributeListValue = ts.isJsxAttribute(node) && node.initializer && ts.isJsxExpression(node.initializer) && node.initializer.expression ? keyedEntry(activeKeyedBlock?.analysis.values ?? [], node.initializer.expression) : undefined
2317
+ if (attributeListValue) {
2318
+ return factory.updateJsxAttribute(node, node.name, factory.createJsxExpression(undefined, descriptors.compileListValue(node.initializer.expression, { ...attributeListValue.value, keyedBlock: activeKeyedBlock.block.slot })))
2271
2319
  }
2272
2320
 
2273
2321
  if (ts.isJsxExpression(node) && node.initializer === undefined && node.expression && (ts.isJsxElement(node.parent) || ts.isJsxFragment(node.parent))) {
2274
- const nestedParts = nestedLists.get(unwrapExpression(node.expression))
2275
- const listParts = renderedLists.get(node) ?? nestedParts
2276
- if (listParts) {
2277
- usesBehavior = true
2278
- usesList = true
2279
- let listSource = listParts.state
2280
- if (listParts.calculation) {
2281
- usesBinding = true
2282
- listSource = descriptors.compileReactiveBinding(listParts.calculation, { setters: settersForNode(node, settersByFunction), importBindings })
2283
- }
2284
- const selector = listParts.selector?.length ? descriptors.registerDerived("selector", listParts.selector, listParts.selectorStates, node).selector : listParts.selector ?? []
2285
- const arguments_ = [
2286
- listSource,
2287
- listParts.keyField === null ? factory.createNull() : factory.createStringLiteral(listParts.keyField),
2288
- visitWithStateOwners(listParts.callback, listParts.analysisStateOwners ?? new Map()),
2289
- factory.createStringLiteral(listParts.ownerField ?? ""),
2290
- jsonExpression(selector, factory),
2291
- listParts.indexed ? factory.createTrue() : factory.createFalse()
2292
- ]
2293
- if (listParts.selectorStates?.size || listParts.static) arguments_.push(factory.createArrayLiteralExpression([...(listParts.selectorStates ?? [])].map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), factory.createIdentifier(name)]))))
2294
- if (listParts.static) arguments_.push(factory.createTrue())
2295
- return factory.updateJsxExpression(node, factory.createCallExpression(factory.createIdentifier("__kList"), undefined, arguments_))
2296
- }
2322
+ const renderedList = keyedEntry(preparedRenderedLists, node)
2323
+ const nestedList = keyedEntry(activeKeyedBlock?.analysis.nested ?? [], unwrapExpression(node.expression))
2324
+ if (renderedList || nestedList) return compileKeyedBlock(node, renderedList ?? nestedList)
2297
2325
  const conditional = conditionalParts(node.expression)
2298
2326
  if (conditional) {
2299
2327
  const compiled = compileRenderExpression(node.expression, node)
@@ -2332,7 +2360,8 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
2332
2360
  setters,
2333
2361
  reducers: reducersForNode(node, reducersByFunction),
2334
2362
  functions: functionsForNode(node),
2335
- listItem: listEventItems.get(node),
2363
+ listItem: activeKeyedBlock ? { item: activeKeyedBlock.parts.item, index: activeKeyedBlock.parts.index } : undefined,
2364
+ keyedBlock: activeKeyedBlock?.block.slot,
2336
2365
  importBindings: new Map([...importBindings, ...packageBindings])
2337
2366
  })
2338
2367
  if (event) {
@@ -2567,10 +2596,11 @@ function insideJsxEventHandler(node, root) {
2567
2596
  return false
2568
2597
  }
2569
2598
 
2570
- function validateKeyedList(parts, sourceFile, listValues, listEventItems, listConditions, setters, rowStates, nestedLists, componentSpecializations, expandedRowSpecializations, nestedRowSpecializations, factory, prepareListCallback) {
2599
+ function validateKeyedList(parts, sourceFile, setters, rowStates, componentSpecializations, expandedRowSpecializations, nestedRowSpecializations, factory, prepareListCallback) {
2571
2600
  const fail = (node, message) => {
2572
2601
  throw sourceNodeError(node, sourceFile, message)
2573
2602
  }
2603
+ const analysis = { values: [], conditions: [], nested: [] }
2574
2604
  const root = parts.root
2575
2605
  const item = parts.item
2576
2606
  const nestedDiagnostic = "Nested keyed list collections must be a direct property of the parent item"
@@ -2585,7 +2615,6 @@ function validateKeyedList(parts, sourceFile, listValues, listEventItems, listCo
2585
2615
  if (node !== root && ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "map" && containsJsx(node)) fail(node, nestedDiagnostic)
2586
2616
  if (ts.isJsxSpreadAttribute(node) && referencesIdentifier(node.expression, item)) fail(node, "Keyed list item spreads are not supported")
2587
2617
  if (ts.isJsxAttribute(node) && /^on[A-Z]/.test(node.name.text)) {
2588
- listEventItems.set(node, { item, index: parts.index })
2589
2618
  return
2590
2619
  }
2591
2620
  if (ts.isJsxExpression(node) && node.expression) {
@@ -2610,16 +2639,26 @@ function validateKeyedList(parts, sourceFile, listValues, listEventItems, listCo
2610
2639
  ts.setParentRecursive(callback, false)
2611
2640
  callback.parent = nested.callback.parent
2612
2641
  }
2613
- callback = prepareListCallback(callback, root, specialization ?? { hookDeclarations: [], effects: [] }, nested.item)
2642
+ callback = prepareListCallback(callback, root, specialization ?? { hookDeclarations: [], effects: [] })
2614
2643
  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])]) }
2644
+ const nestedParts = {
2645
+ ...nested,
2646
+ root,
2647
+ callback,
2648
+ state: parts.state,
2649
+ nested: true,
2650
+ specializations: [specialization?.analysis?.slot, ...(specialization?.specializations ?? [])].filter(slot => slot !== undefined),
2651
+ rowStates: specializedStates,
2652
+ rowRefs: specialization?.rowRefs ?? [],
2653
+ analysisStateOwners: new Map([...(parts.analysisStateOwners ?? []), ...specializedStates.map(state => [state.state, state.analysisOwner])])
2654
+ }
2616
2655
  for (const calculation of specialization?.calculations ?? []) {
2617
2656
  ts.setParentRecursive(calculation, false)
2618
2657
  calculation.parent = callback
2619
2658
  validateListExpression(calculation, nested.item, nested.root, fail)
2620
2659
  }
2621
- nestedLists.set(expression, nestedParts)
2622
- validateKeyedList(nestedParts, sourceFile, listValues, listEventItems, listConditions, setters, specialization?.rowStates ?? [], nestedLists, componentSpecializations, expandedRowSpecializations, nestedRowSpecializations, factory, prepareListCallback)
2660
+ const nestedAnalysis = validateKeyedList(nestedParts, sourceFile, setters, specialization?.rowStates ?? [], componentSpecializations, expandedRowSpecializations, nestedRowSpecializations, factory, prepareListCallback)
2661
+ analysis.nested.push({ node: expression, parts: nestedParts, analysis: nestedAnalysis })
2623
2662
  return
2624
2663
  }
2625
2664
  const condition = conditionalParts(expression)
@@ -2631,7 +2670,7 @@ function validateKeyedList(parts, sourceFile, listValues, listEventItems, listCo
2631
2670
  }
2632
2671
  if (!referencesIdentifier(condition.condition, item) && !(parts.index && referencesIdentifier(condition.condition, parts.index))) fail(node, "Keyed list item conditions must read the item or index")
2633
2672
  validateListExpression(condition.condition, item, node, fail, parts.index)
2634
- listConditions.set(node.expression, { ...condition, item, index: parts.index })
2673
+ analysis.conditions.push({ node: node.expression, value: { ...condition, item, index: parts.index } })
2635
2674
  visit(condition.truthy)
2636
2675
  visit(condition.falsy)
2637
2676
  return
@@ -2642,7 +2681,7 @@ function validateKeyedList(parts, sourceFile, listValues, listEventItems, listCo
2642
2681
  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`)
2643
2682
  if (isRootKey) return
2644
2683
  if (field) {
2645
- listValues.set(node.expression, { field })
2684
+ analysis.values.push({ node: node.expression, value: { field } })
2646
2685
  return
2647
2686
  }
2648
2687
  if (referencesIdentifier(expression, item) || parts.index && referencesIdentifier(expression, parts.index)) {
@@ -2651,13 +2690,14 @@ function validateKeyedList(parts, sourceFile, listValues, listEventItems, listCo
2651
2690
  if (parts.nested && states.size) fail(node, "Nested keyed list item expressions cannot read parent state")
2652
2691
  validateListExpression(expression, item, node, fail, parts.index, states)
2653
2692
  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`)
2654
- listValues.set(node.expression, { item, index: parts.index, states })
2693
+ analysis.values.push({ node: node.expression, value: { item, index: parts.index, states } })
2655
2694
  return
2656
2695
  }
2657
2696
  }
2658
2697
  ts.forEachChild(node, visit)
2659
2698
  }
2660
2699
  visit(root)
2700
+ return analysis
2661
2701
  }
2662
2702
 
2663
2703
  function directConstObjectLiteral(expression, call) {
@@ -3369,30 +3409,6 @@ function localComponentDeclaration(sourceFile, name) {
3369
3409
  return undefined
3370
3410
  }
3371
3411
 
3372
- function validateEffectOwnedBrowserResources(callback, returns, fail) {
3373
- const observers = []
3374
- const frameAssignments = []
3375
- const cancellations = new Set()
3376
- const disconnected = new Set()
3377
- const insideCleanup = node => returns.cleanups.some(cleanup => {
3378
- for (let current = node; current; current = current.parent) if (current === cleanup) return true
3379
- return false
3380
- })
3381
- const visit = node => {
3382
- if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isNewExpression(unwrapExpression(node.initializer)) && ts.isIdentifier(unwrapExpression(node.initializer).expression) && unwrapExpression(node.initializer).expression.text === "IntersectionObserver") observers.push(node)
3383
- if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken && ts.isIdentifier(unwrapExpression(node.left)) && ts.isCallExpression(unwrapExpression(node.right)) && ts.isIdentifier(unwrapExpression(node.right).expression) && unwrapExpression(node.right).expression.text === "requestAnimationFrame") frameAssignments.push(node)
3384
- if (insideCleanup(node) && ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "cancelAnimationFrame" && node.arguments.length === 1 && ts.isIdentifier(unwrapExpression(node.arguments[0]))) cancellations.add(unwrapExpression(node.arguments[0]).text)
3385
- if (insideCleanup(node) && ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.expression) && node.expression.name.text === "disconnect" && node.arguments.length === 0) disconnected.add(node.expression.expression.text)
3386
- ts.forEachChild(node, visit)
3387
- }
3388
- visit(callback.body)
3389
- for (const observer of observers) if (!disconnected.has(observer.name.text)) fail(observer, `IntersectionObserver effects must disconnect ${JSON.stringify(observer.name.text)} in cleanup`)
3390
- for (const assignment of frameAssignments) {
3391
- const name = unwrapExpression(assignment.left).text
3392
- if (!cancellations.has(name)) fail(assignment, `Animation loop effects must cancel ${JSON.stringify(name)} in cleanup`)
3393
- }
3394
- }
3395
-
3396
3412
  async function collectClientModules(entries, sourceFiles) {
3397
3413
  const modules = new Set()
3398
3414
  const queue = [...new Set(entries)]