@kudzujs/core 0.6.27 → 0.6.28

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.
@@ -195,6 +195,7 @@ export async function build({ quiet = false, minify = true } = {}) {
195
195
  const nativeEvents = [...new Set(plans.flatMap(plan => plan.events.filter(event => event.native).map(event => event.event)))].sort()
196
196
  const hasTextBindings = plans.some(plan => plan.bindings.some(binding => binding.target === "text"))
197
197
  const hasListConditions = plans.some(plan => plan.lists.some(list => list.conditions))
198
+ const hasDeepListConditions = plans.some(plan => plan.lists.some(list => list.conditionHandlers))
198
199
  const hasListTextRanges = plans.some(plan => plan.lists.some(list => list.textRanges))
199
200
  const hasListAttributes = plans.some(plan => plan.lists.some(list => list.attributes))
200
201
  const hasListEvents = plans.some(plan => plan.lists.some(list => list.events))
@@ -202,8 +203,14 @@ export async function build({ quiet = false, minify = true } = {}) {
202
203
  const hasListExpressionAttributes = plans.some(plan => plan.lists.some(list => list.expressionAttributes))
203
204
  const hasListSeeds = plans.some(plan => plan.lists.some(list => list.seed))
204
205
  const hasListEffects = plans.some(plan => plan.lists.some(list => list.effects))
205
- const hasListRowStates = plans.some(plan => plan.lists.some(list => list.rowStates))
206
+ const hasListRowHooks = plans.some(plan => plan.lists.some(list => list.rowStates?.length || list.rowRefs?.length))
207
+ const hasListRowRefs = plans.some(plan => plan.lists.some(list => list.rowRefs?.length))
208
+ const hasComplexListRowState = plans.some(plan => plan.lists.some(list => list.rowStates?.some(state => state.initialValue !== null && typeof state.initialValue === "object")))
206
209
  const hasNestedLists = plans.some(plan => plan.lists.some(list => list.ownerField))
210
+ const hasCollectionSelectors = plans.some(plan => plan.lists.some(list => list.selector))
211
+ const hasListIndexes = plans.some(plan => plan.lists.some(list => list.indexed))
212
+ const hasListStableFastPaths = plans.some(plan => plan.lists.some(list => !list.children && !list.ownerField && list.key !== null && !list.indexed && !list.reducer && !list.selector))
213
+ const hasGeneralListRowHooks = hasListRowRefs || hasComplexListRowState || plans.some(plan => plan.lists.some(list => list.ownerField && (list.rowStates?.length || list.rowRefs?.length)))
207
214
  const hasItemDependencies = plans.some(plan => plan.effects.some(effect => effect.itemDependencies?.length))
208
215
  const hasListAsyncParts = hasListExpressions || hasListExpressionAttributes || hasListConditions
209
216
  const hasListMounts = hasListConditions || hasNestedLists || plans.some(plan => plan.lists.some(list => list.mount))
@@ -262,9 +269,49 @@ export async function build({ quiet = false, minify = true } = {}) {
262
269
  })
263
270
  }
264
271
  if (listCount) {
272
+ if (hasCollectionSelectors) await writeJavaScript(join(assetsDirectory, "kudzu-collection-selector.js"), await readFile(new URL("./collection-selector.js", import.meta.url), "utf8"), minify)
265
273
  let listRuntime = (await readFile(new URL("./list-runtime.js", import.meta.url), "utf8"))
266
274
  .replace('"./shared-runtime.js"', '"./kudzu.js"')
275
+ listRuntime = hasCollectionSelectors
276
+ ? listRuntime.replace('"./collection-selector.js"', '"./kudzu-collection-selector.js"')
277
+ : listRuntime.replace(/^import \{ selectCollection \}[^\n]+\n/m, "")
278
+ if (!hasListIndexes) listRuntime = listRuntime
279
+ .replace("for (const [index, item] of items.entries()) {", "for (const item of items) {")
280
+ .replace("const key = list.descriptor.key === null ? index : item?.[list.descriptor.key]", "const key = item?.[list.descriptor.key]")
281
+ .replace("entries.push({ item, index, key, token, value:", "entries.push({ item, key, token, value:")
282
+ .replace("for (const { item, index, key, token, value } of entries) {", "for (const { item, key, token, value } of entries) {")
283
+ .replaceAll("fillListItem(node, item, list.descriptor.nested, index)", "fillListItem(node, item, list.descriptor.nested)")
284
+ .replace("function addListRoot(list, { item, index = list.roots.size, key, token, value })", "function addListRoot(list, { item, key, token, value })")
285
+ .replace("fillListParts(root, listItemParts(root), listItems.get(owner), 0, __KUDZU_LIST_INDEXES__ ? listIndexes.get(owner) ?? 0 : 0)", "fillListParts(root, listItemParts(root), listItems.get(owner), 0)")
286
+ .replace("function fillListItem(root, item, nested = false, index = 0)", "function fillListItem(root, item, nested = false)")
287
+ .replace("fillListParts(root, parts, item, revision, index)", "fillListParts(root, parts, item, revision)")
288
+ .replace("function fillListParts(root, parts, item, revision, index = 0)", "function fillListParts(root, parts, item, revision)")
289
+ .replaceAll('value?.type === "list-item" ? serializeItem(item) : value?.type === "list-index" ? index : value', 'value?.type === "list-item" ? serializeItem(item) : value')
290
+ .replaceAll("evaluate(descriptor, item, index)", "evaluate(descriptor, item)")
291
+ .replaceAll("evaluate({ module, handler }, item, index)", "evaluate({ module, handler }, item)")
292
+ .replace("updateListCondition(marker, descriptor.kind, value, item, index)", "updateListCondition(marker, descriptor.kind, value, item)")
293
+ .replace("function updateListCondition(marker, kind, value, item, index)", "function updateListCondition(marker, kind, value, item)")
294
+ .replace("fillListParts(marker, listItemParts(fragment), item, revision, index)", "fillListParts(marker, listItemParts(fragment), item, revision)")
295
+ .replace("function evaluate(descriptor, item, index)", "function evaluate(descriptor, item)")
296
+ .replace("exports[descriptor.handler](item, index)", "exports[descriptor.handler](item)")
297
+ if (!hasCollectionSelectors) listRuntime = listRuntime.replaceAll(" && !list.descriptor.selector", "")
298
+ if (!hasListIndexes) listRuntime = listRuntime
299
+ .replaceAll(" && !list.descriptor.indexed", "")
300
+ .replaceAll(" && list.descriptor.key !== null", "")
301
+ .replaceAll("list.descriptor.key !== null && !list.descriptor.indexed && ", "")
302
+ .replace("list.descriptor.key !== null && !list.descriptor.indexed && !list.descriptor.selector && list.values.size", "list.values.size")
303
+ .replace("(referenceOnly ? listItems.get(node) !== item : list.values.get(token) !== value) || list.descriptor.indexed || list.descriptor.key === null", "referenceOnly ? listItems.get(node) !== item : list.values.get(token) !== value")
304
+ if (hasListRowHooks && !hasGeneralListRowHooks) listRuntime = listRuntime
305
+ .replace(/\/\* general-row-hooks \*\/[\s\S]*?\/\* general-row-hooks-end \*\/\n/, "")
306
+ .replaceAll("initializeGeneralRowHooks", "initializeRowStates")
307
+ .replace("if (__KUDZU_LIST_ROW_HOOKS__) for (let index = 0; index < roots.length; index++) initializeRowStates(descriptor, descriptor.keys[index], roots[index], nested?.owner)", "if (__KUDZU_LIST_ROW_HOOKS__ && descriptor.rowStates) for (let index = 0; index < roots.length; index++) initializeRowStates(descriptor, descriptor.keys[index])")
308
+ .replaceAll("if (__KUDZU_LIST_ROW_HOOKS__) initializeRowStates(list.descriptor, key, node, list.owner)", "if (__KUDZU_LIST_ROW_HOOKS__ && list.descriptor.rowStates) initializeRowStates(list.descriptor, key, node)")
309
+ .replace("for (const node of registration.list.roots.values()) deleteRowStates(registration.list.descriptor, ownershipPaths.get(node))", "for (const token of registration.list.roots.keys()) deleteFlatRowStates(registration.list.descriptor, token)")
310
+ .replaceAll("deleteRowStates(list.descriptor, ownershipPaths.get(node))", "deleteFlatRowStates(list.descriptor, token)")
311
+ .replace(" if (__KUDZU_LIST_ROW_HOOKS__) replaceRowIds(root, rowReplacements.get(root))\n", "")
312
+ .replace(" if (!replacements) return\n", "")
267
313
  if (!hasItemDependencies) listRuntime = listRuntime.replace(", notifyListItem", "")
314
+ if (!hasListStableFastPaths) listRuntime = listRuntime.replace(/\/\* stable-list-fast-path \*\/[\s\S]*?\/\* stable-list-fast-path-end \*\/\n/, "")
268
315
  const stylePatch = ` if (target === "style") {
269
316
  const style = serializeStyle(value)
270
317
  if (style) node.setAttribute("style", style)
@@ -275,6 +322,7 @@ export async function build({ quiet = false, minify = true } = {}) {
275
322
  if (listStyleCount) listRuntime = `import { serializeStyle } from "./kudzu-style.js"\n${listRuntime}`
276
323
  await writeBundledJavaScript(join(assetsDirectory, "kudzu-list.js"), listRuntime, minify, {
277
324
  __KUDZU_LIST_CONDITIONS__: String(hasListConditions),
325
+ __KUDZU_DEEP_LIST_CONDITIONS__: String(hasDeepListConditions),
278
326
  __KUDZU_LIST_TEXT_RANGES__: String(hasListTextRanges),
279
327
  __KUDZU_LIST_ATTRIBUTES__: String(hasListAttributes),
280
328
  __KUDZU_LIST_EVENTS__: String(hasListEvents),
@@ -285,9 +333,15 @@ export async function build({ quiet = false, minify = true } = {}) {
285
333
  __KUDZU_LIST_ASYNC_PARTS__: String(hasListAsyncParts),
286
334
  __KUDZU_LIST_MOUNTS__: String(hasListMounts),
287
335
  __KUDZU_LIST_ITEM_HOOKS__: String(hasItemDependencies),
288
- __KUDZU_LIST_ROW_STATES__: String(hasListRowStates),
289
- __KUDZU_NESTED_LISTS__: String(hasNestedLists)
336
+ __KUDZU_LIST_ROW_HOOKS__: String(hasListRowHooks),
337
+ __KUDZU_LIST_ROW_REFS__: String(hasListRowRefs),
338
+ __KUDZU_COMPLEX_LIST_ROW_STATE__: String(hasComplexListRowState),
339
+ __KUDZU_NESTED_LISTS__: String(hasNestedLists),
340
+ __KUDZU_COLLECTION_SELECTORS__: String(hasCollectionSelectors),
341
+ __KUDZU_LIST_INDEXES__: String(hasListIndexes),
342
+ __KUDZU_LIST_STABLE_FAST_PATHS__: String(hasListStableFastPaths)
290
343
  })
344
+ if (hasCollectionSelectors) await rm(join(assetsDirectory, "kudzu-collection-selector.js"))
291
345
  }
292
346
  if (hasNativeHandlers) {
293
347
  const nativeRuntime = (await readFile(new URL("./native-runtime.js", import.meta.url), "utf8"))
@@ -1004,6 +1058,7 @@ function runtimeEffects(effects, lifetimes = false) {
1004
1058
  function printOwnedEffectEntry(imports, effects, entries) {
1005
1059
  const hasItemDependencies = effects.some(effect => effect.itemDependencies?.length)
1006
1060
  const hasOrdinaryDependencies = effects.some(effect => effect.dependencies?.length)
1061
+ const hasRowState = effects.some(effect => JSON.stringify([effect.dependencies, effect.states, effect.scope]).includes("$k"))
1007
1062
  return `${imports.join("\n")}
1008
1063
  const effects = ${inlineJson(effects)}
1009
1064
  const modules = new Map([${entries}])
@@ -1018,9 +1073,16 @@ let scheduled = false
1018
1073
  let flushing = false
1019
1074
  let active = true
1020
1075
  for (const record of records) registerDependencies(record)
1021
- function createRecord(effect, index) {
1022
- return { effect, index, ${hasItemDependencies ? "order: order++, " : ""}mounted: !effect.owner, marker: undefined, version: 0, values: undefined, cleanup: undefined, disposal: undefined, token: undefined }
1076
+ function createRecord(effect, index${hasRowState ? ", marker" : ""}) {
1077
+ return { effect: ${hasRowState ? "marker ? specializeRowEffect(effect, marker) : effect" : "effect"}, index, ${hasItemDependencies ? "order: order++, " : ""}mounted: !effect.owner, marker: undefined, version: 0, values: undefined, cleanup: undefined, disposal: undefined, token: undefined }
1023
1078
  }
1079
+ ${hasRowState ? `function specializeRowEffect(effect, marker) {
1080
+ const path = marker.dataset.kRowPath
1081
+ const id = value => typeof value === "string" ? value.replace("$k", path) : value
1082
+ const capture = value => value?.type === "state" || value?.type === "setter" || value?.type === "ref" ? { ...value, id: id(value.id) } : value?.type === "array" ? { ...value, value: value.value.map(capture) } : value?.type === "object" ? { ...value, value: value.value.map(([key, entry]) => [key, capture(entry)]) } : value
1083
+ return { ...effect, dependencies: effect.dependencies?.map(id), states: Object.fromEntries(Object.entries(effect.states).map(([name, value]) => [name, id(value)])), scope: Object.fromEntries(Object.entries(effect.scope).map(([name, value]) => [name, capture(value)])) }
1084
+ }
1085
+ ` : ""}
1024
1086
  function registerDependencies(record) {
1025
1087
  for (const id of record.effect.dependencies ?? []) {
1026
1088
  const subscribers = dependencies.get(id) ?? new Set()
@@ -1053,7 +1115,7 @@ ${hasItemDependencies ? `for (const listState of new Set(effects.filter(effect =
1053
1115
  const rowRecords = JSON.parse(marker.dataset.kEffects).map(owner => {
1054
1116
  const template = listTemplates.get(owner)
1055
1117
  if (!template) throw new Error("Keyed row effect template was not emitted")
1056
- const record = createRecord(template.effect, template.index)
1118
+ const record = createRecord(template.effect, template.index${hasRowState ? ", marker" : ""})
1057
1119
  registerDependencies(record)
1058
1120
  mount(record, marker)
1059
1121
  return record
@@ -1694,6 +1756,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
1694
1756
  let usesListEffects = false
1695
1757
  let usesListItem = false
1696
1758
  let usesRowState = false
1759
+ let usesRowRef = false
1697
1760
 
1698
1761
  const collect = node => {
1699
1762
  if (ts.isVariableDeclaration(node) && node.initializer && ts.isCallExpression(node.initializer)) {
@@ -1779,7 +1842,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
1779
1842
  const setters = settersByFunction.get(owner) ?? new Map()
1780
1843
  for (const [name, entries] of declarations) {
1781
1844
  for (const declaration of entries) {
1782
- const parts = keyedListParts(declaration.initializer, setters)
1845
+ const parts = keyedListParts(declaration.initializer, setters, declarations, (target, message) => { throw sourceNodeError(target, sourceFile, message) })
1783
1846
  if (!parts) continue
1784
1847
  const uses = []
1785
1848
  const collectUses = node => {
@@ -1800,26 +1863,30 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
1800
1863
  throw sourceNodeError(node, sourceFile, message)
1801
1864
  }
1802
1865
  const componentSpecializations = new WeakMap()
1866
+ const expandedRowSpecializations = new WeakMap()
1867
+ const nestedRowSpecializations = new Map()
1803
1868
  const reducerComponentCalls = new WeakSet()
1804
- const reducerRowStateCalls = []
1869
+ const rowHookCalls = []
1805
1870
  const specializedDeclarations = new WeakSet()
1806
1871
  const stateBackedComponentFunctions = new WeakSet()
1807
1872
  const stateBackedComponentRoots = []
1808
1873
  let specializedImportIndex = 0
1809
- const registerReducerRowState = (call, specialization) => {
1810
- if (!specialization.rowState) return
1874
+ const registerRowHooks = (call, specialization) => {
1875
+ if (!specialization.rowStates.length && !specialization.rowRefs.length) return
1811
1876
  let owner
1812
1877
  for (let current = call.parent; current; current = current.parent) {
1813
- if (isFunctionLike(current) && reducersByFunction.has(current)) {
1878
+ if (isFunctionLike(current) && settersByFunction.has(current)) {
1814
1879
  owner = current
1815
1880
  break
1816
1881
  }
1817
1882
  }
1818
- const setters = settersByFunction.get(owner) ?? new Map()
1819
- setters.set(specialization.rowState.setter, specialization.rowState.state)
1883
+ if (!owner) owner = nearestFunction(call)
1884
+ const setters = new Map(settersByFunction.get(owner))
1885
+ for (const state of specialization.rowStates) setters.set(state.setter, state.state)
1820
1886
  settersByFunction.set(owner, setters)
1821
- reducerRowStateCalls.push(call)
1822
- usesRowState = true
1887
+ rowHookCalls.push(call)
1888
+ usesRowState ||= specialization.rowStates.length > 0
1889
+ usesRowRef ||= specialization.rowRefs.length > 0
1823
1890
  }
1824
1891
  const mergeSpecializedImports = (root, componentSource, call) => {
1825
1892
  const componentImports = clientImportBindings(componentSource, componentSource.fileName, sourceFiles)
@@ -1917,8 +1984,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
1917
1984
  for (const call of dispatchCalls) {
1918
1985
  if (componentSpecializations.has(call)) fail(call, "Reducer dispatch props cannot be combined with another component specialization")
1919
1986
  const specialization = specializeComponentCall(call, component.function, sourceFile, factory, context, fail, "Reducer-dispatch")
1920
- if (specialization.effects.length) fail(call, "Reducer-dispatch components cannot declare effects")
1921
- registerReducerRowState(call, specialization)
1987
+ registerRowHooks(call, specialization)
1922
1988
  specialization.root = expandReducerCallbacks(specialization.root, component.function.getSourceFile(), call)
1923
1989
  componentSpecializations.set(call, specialization)
1924
1990
  reducerComponentCalls.add(call)
@@ -1941,8 +2007,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
1941
2007
  for (const call of dispatchCalls) {
1942
2008
  if (componentSpecializations.has(call)) fail(call, "Reducer dispatch props cannot be combined with another component specialization")
1943
2009
  const specialization = specializeComponentCall(call, component, sourceFile, factory, context, fail, "Reducer-dispatch")
1944
- if (specialization.effects.length) fail(call, "Reducer-dispatch components cannot declare effects")
1945
- registerReducerRowState(call, specialization)
2010
+ registerRowHooks(call, specialization)
1946
2011
  specialization.root = expandReducerCallbacks(specialization.root, componentSource, call)
1947
2012
  specialization.root = mergeSpecializedImports(specialization.root, componentSource, call)
1948
2013
  synthesizeTree(specialization.root)
@@ -1958,8 +2023,11 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
1958
2023
  return
1959
2024
  }
1960
2025
  if (ts.isJsxExpression(node) && node.initializer === undefined && node.expression && (ts.isJsxElement(node.parent) || ts.isJsxFragment(node.parent))) {
1961
- const parts = listLocalUses.get(node) ?? keyedListParts(node.expression, settersForNode(node, settersByFunction))
1962
- if (parts) rawRenderedLists.push({ node, parts })
2026
+ const parts = listLocalUses.get(node) ?? keyedListParts(node.expression, settersForNode(node, settersByFunction), jsxLocalDeclarations.get(nearestFunction(node)), fail)
2027
+ if (parts) {
2028
+ for (const declaration of parts.aliasDeclarations ?? []) listLocalDeclarations.add(declaration)
2029
+ rawRenderedLists.push({ node, parts })
2030
+ }
1963
2031
  }
1964
2032
  ts.forEachChild(node, collectRenderedLists)
1965
2033
  }
@@ -1974,27 +2042,12 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
1974
2042
  ts.forEachChild(node, rejectUnsupportedRenderControl)
1975
2043
  }
1976
2044
  rejectUnsupportedRenderControl(sourceFile)
1977
- const nestedComponentCalls = new Set()
1978
- for (const { parts } of rawRenderedLists) {
1979
- const collectNestedComponents = node => {
1980
- if (ts.isJsxExpression(node) && node.expression) {
1981
- const nested = nestedKeyedListParts(node.expression, parts.item)
1982
- if (nested) {
1983
- const tag = jsxTagName(nested.root)
1984
- if (tag && ts.isIdentifier(tag) && tag.text[0] === tag.text[0].toUpperCase()) nestedComponentCalls.add(nested.root)
1985
- return
1986
- }
1987
- }
1988
- ts.forEachChild(node, collectNestedComponents)
1989
- }
1990
- collectNestedComponents(parts.root)
1991
- }
1992
- const listComponentNames = new Set([...rawRenderedLists.flatMap(({ parts }) => {
2045
+ const listComponentNames = new Set(rawRenderedLists.flatMap(({ parts }) => {
1993
2046
  const tag = jsxTagName(parts.root)
1994
2047
  return tag && ts.isIdentifier(tag) && tag.text[0] === tag.text[0].toUpperCase() ? [tag.text] : []
1995
- }), ...[...nestedComponentCalls].map(call => jsxTagName(call).text)])
2048
+ }))
1996
2049
  const keyedComponentCalls = new Set(rawRenderedLists.map(({ parts }) => parts.root))
1997
- for (const call of reducerRowStateCalls) if (!keyedComponentCalls.has(call)) fail(call, "Reducer-dispatch component useState() is only supported in a direct keyed row")
2050
+ for (const call of rowHookCalls) if (!keyedComponentCalls.has(call)) fail(call, "Keyed row hooks are only supported in direct keyed map rows")
1998
2051
  for (const name of listComponentNames) {
1999
2052
  let component = components.get(name)
2000
2053
  const local = Boolean(component)
@@ -2007,28 +2060,100 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
2007
2060
  if (local && isExportedDeclaration(component.declaration)) fail(component.declaration, `Keyed list component ${name} cannot be exported`)
2008
2061
  const declaredCalls = jsxTagUses(sourceFile, name)
2009
2062
  if (local && identifierReferenceCount(sourceFile, name) !== declaredCalls.length) fail(component.declaration, `Keyed list component ${name} may only be referenced as JSX`)
2010
- const calls = [
2063
+ const calls = [...new Set([
2011
2064
  ...declaredCalls.filter(call => !stateBackedComponentFunctions.has(nearestFunction(call))),
2012
2065
  ...stateBackedComponentRoots.flatMap(root => jsxTagUses(root, name))
2013
- ]
2066
+ ])]
2014
2067
  for (const call of calls) {
2015
2068
  const specialization = reducerComponentCalls.has(call)
2016
2069
  ? componentSpecializations.get(call)
2017
- : specializeComponentCall(call, component.function, sourceFile, factory, context, fail)
2070
+ : specializeComponentCall(call, component.function, sourceFile, factory, context, fail, "Keyed list", true)
2071
+ registerRowHooks(call, specialization)
2018
2072
  if (specialization.effects.length && !keyedComponentCalls.has(call)) fail(call, "Effectful keyed row components may only be used directly as keyed map rows")
2019
- if (!local) {
2020
- specialization.root = mergeSpecializedImports(specialization.root, component.function.getSourceFile(), call)
2021
- synthesizeTree(specialization.root)
2022
- }
2073
+ specialization.component = component.function
2074
+ specialization.componentSource = component.function.getSourceFile()
2075
+ specialization.imported = !local
2023
2076
  componentSpecializations.set(call, specialization)
2024
2077
  }
2025
2078
  if (local) specializedDeclarations.add(component.declaration)
2026
2079
  }
2080
+ const expandKeyedComponents = (root, componentSource, trail = [], aggregate) => {
2081
+ const replacements = new WeakMap()
2082
+ let count = 0
2083
+ const visit = (node, currentAggregate = aggregate) => {
2084
+ if (node !== root && ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "map" && containsJsx(node)) {
2085
+ const nestedAggregate = { calculations: [], effects: [], hookDeclarations: [], rowStates: [], rowRefs: [] }
2086
+ for (const argument of node.arguments) visit(argument, nestedAggregate)
2087
+ if (nestedAggregate.hookDeclarations.length || nestedAggregate.effects.length) nestedRowSpecializations.set(`${node.pos}:${node.end}`, nestedAggregate)
2088
+ return
2089
+ }
2090
+ const tag = jsxTagName(node)
2091
+ if (tag && (!ts.isIdentifier(tag) || tag.text[0] !== tag.text[0].toLowerCase())) {
2092
+ if (!ts.isIdentifier(tag)) fail(node, "Keyed list components must use identifier JSX tags")
2093
+ const name = tag.text
2094
+ let component = localComponentDeclaration(componentSource, name)
2095
+ let imported = false
2096
+ if (!component) {
2097
+ const binding = clientImportBindings(componentSource, componentSource.fileName, sourceFiles).get(name)
2098
+ if (!binding || binding.kind === "namespace") fail(node, `Keyed list component ${name} must be declared locally or imported from a relative TypeScript module`)
2099
+ component = resolveComponentExport(binding.target, binding.kind === "default" ? "default" : binding.imported, importedSource, sourceFiles)
2100
+ imported = true
2101
+ }
2102
+ if (trail.includes(component)) {
2103
+ const chain = [...trail, component].map(entry => entry.name?.text || "anonymous").join(" -> ")
2104
+ fail(node, `Keyed list component cycle: ${chain}`)
2105
+ }
2106
+ const specialization = specializeComponentCall(node, component, sourceFile, factory, context, fail, "Keyed list", true)
2107
+ registerRowHooks(node, specialization)
2108
+ specialization.root = expandKeyedComponents(specialization.root, component.getSourceFile(), [...trail, component], specialization)
2109
+ if (imported) synthesizeTree(specialization.root = mergeSpecializedImports(specialization.root, component.getSourceFile(), node))
2110
+ expandedRowSpecializations.set(specialization.root, specialization)
2111
+ if (currentAggregate) {
2112
+ currentAggregate.effects.push(...specialization.effects)
2113
+ currentAggregate.hookDeclarations.push(...specialization.hookDeclarations)
2114
+ currentAggregate.rowStates.push(...specialization.rowStates)
2115
+ currentAggregate.rowRefs.push(...specialization.rowRefs)
2116
+ }
2117
+ replacements.set(node, specialization.root)
2118
+ count++
2119
+ return
2120
+ }
2121
+ ts.forEachChild(node, child => visit(child, currentAggregate))
2122
+ }
2123
+ visit(root)
2124
+ if (!count) return root
2125
+ const expanded = replaceSpecializedCalls(root, replacements, context)
2126
+ ts.setParentRecursive(expanded, false)
2127
+ expanded.parent = root.parent
2128
+ return expanded
2129
+ }
2027
2130
  const renderedLists = new WeakMap()
2131
+ const prepareListCallback = (callback, root, specialization, item) => {
2132
+ const statements = [...specialization.hookDeclarations]
2133
+ if (specialization.effects.length) {
2134
+ usesListEffects = true
2135
+ statements.push(...specialization.effects.map(entry => {
2136
+ const call = factory.updateCallExpression(entry.call, factory.createIdentifier("__kListUseEffect"), entry.call.typeArguments, entry.call.arguments)
2137
+ synthesizeTree(call)
2138
+ const effectSource = entry.source.getSourceFile()
2139
+ listEffectEntries.set(call, { item, source: entry.source, sourceFile: effectSource, imports: clientImportBindings(effectSource, effectSource.fileName, sourceFiles) })
2140
+ return factory.createExpressionStatement(call)
2141
+ }))
2142
+ }
2143
+ if (!statements.length) return callback
2144
+ const prepared = factory.updateArrowFunction(callback, callback.modifiers, callback.typeParameters, callback.parameters, callback.type, callback.equalsGreaterThanToken, factory.createBlock([...statements, factory.createReturnStatement(root)], true))
2145
+ ts.setParentRecursive(prepared, false)
2146
+ prepared.parent = callback.parent
2147
+ return prepared
2148
+ }
2028
2149
  for (const { node, parts: originalParts } of rawRenderedLists) {
2029
2150
  if (keyedListParentTag(node) === "table") throw new Error("Keyed table rows must be wrapped in <tbody>, <thead>, or <tfoot>")
2030
- const specialization = componentSpecializations.get(originalParts.root)
2031
- const root = specialization?.root ?? originalParts.root
2151
+ const specialization = componentSpecializations.get(originalParts.root) ?? { root: originalParts.root, calculations: [], effects: [], hookDeclarations: [], rowStates: [], rowRefs: [] }
2152
+ const componentSource = specialization.componentSource ?? sourceFile
2153
+ specialization.root = expandKeyedComponents(specialization.root, componentSource, specialization.component ? [specialization.component] : [], specialization)
2154
+ if (specialization.imported) synthesizeTree(specialization.root = mergeSpecializedImports(specialization.root, componentSource, originalParts.root))
2155
+ if (specialization.root !== originalParts.root) componentSpecializations.set(originalParts.root, specialization)
2156
+ const root = specialization.root
2032
2157
  let callback = root === originalParts.root ? originalParts.callback : factory.updateArrowFunction(
2033
2158
  originalParts.callback,
2034
2159
  originalParts.callback.modifiers,
@@ -2038,40 +2163,18 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
2038
2163
  originalParts.callback.equalsGreaterThanToken,
2039
2164
  root
2040
2165
  )
2041
- if (specialization?.stateDeclarations.length) callback = factory.updateArrowFunction(
2042
- callback,
2043
- callback.modifiers,
2044
- callback.typeParameters,
2045
- callback.parameters,
2046
- callback.type,
2047
- callback.equalsGreaterThanToken,
2048
- factory.createBlock([...specialization.stateDeclarations, factory.createReturnStatement(root)], true)
2049
- )
2050
2166
  if (callback !== originalParts.callback) {
2051
2167
  ts.setParentRecursive(callback, false)
2052
2168
  callback.parent = originalParts.callback.parent
2053
2169
  }
2170
+ callback = prepareListCallback(callback, root, specialization, originalParts.item)
2054
2171
  const parts = { ...originalParts, root, callback }
2055
- for (const calculation of specialization?.calculations ?? []) {
2172
+ for (const calculation of specialization.calculations) {
2056
2173
  ts.setParentRecursive(calculation, false)
2057
2174
  calculation.parent = callback
2058
2175
  validateListExpression(calculation, parts.item, originalParts.root, fail)
2059
2176
  }
2060
- validateKeyedList(parts, sourceFile, listValues, listEventItems, listConditions, settersForNode(originalParts.root, settersByFunction), specialization?.rowState, nestedLists, componentSpecializations, factory)
2061
- if (specialization?.effects.length) {
2062
- usesListEffects = true
2063
- const statements = specialization.effects.map(entry => {
2064
- const call = factory.updateCallExpression(entry.call, factory.createIdentifier("__kListUseEffect"), entry.call.typeArguments, entry.call.arguments)
2065
- synthesizeTree(call)
2066
- const effectSource = entry.source.getSourceFile()
2067
- listEffectEntries.set(call, { item: parts.item, source: entry.source, sourceFile: effectSource, imports: clientImportBindings(effectSource, effectSource.fileName, sourceFiles) })
2068
- return factory.createExpressionStatement(call)
2069
- })
2070
- callback = factory.updateArrowFunction(callback, callback.modifiers, callback.typeParameters, callback.parameters, callback.type, callback.equalsGreaterThanToken, factory.createBlock([...statements, factory.createReturnStatement(root)], true))
2071
- ts.setParentRecursive(callback, false)
2072
- callback.parent = originalParts.callback.parent
2073
- parts.callback = callback
2074
- }
2177
+ validateKeyedList(parts, sourceFile, listValues, listEventItems, listConditions, settersForNode(originalParts.root, settersByFunction), specialization.rowStates, nestedLists, componentSpecializations, expandedRowSpecializations, nestedRowSpecializations, factory, prepareListCallback)
2075
2178
  renderedLists.set(node, parts)
2076
2179
  }
2077
2180
 
@@ -2231,9 +2334,11 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
2231
2334
  usesList = true
2232
2335
  return factory.updateJsxExpression(node, factory.createCallExpression(factory.createIdentifier("__kList"), undefined, [
2233
2336
  listParts.state,
2234
- factory.createStringLiteral(listParts.keyField),
2337
+ listParts.keyField === null ? factory.createNull() : factory.createStringLiteral(listParts.keyField),
2235
2338
  ts.visitNode(listParts.callback, visitor),
2236
- ...(nestedParts ? [factory.createStringLiteral(listParts.ownerField)] : [])
2339
+ factory.createStringLiteral(listParts.ownerField ?? ""),
2340
+ jsonExpression(listParts.selector ?? [], factory),
2341
+ listParts.indexed ? factory.createTrue() : factory.createFalse()
2237
2342
  ]))
2238
2343
  }
2239
2344
  const conditional = conditionalParts(node.expression)
@@ -2297,11 +2402,13 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
2297
2402
  behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("listExpression"), factory.createIdentifier("__kListExpression")))
2298
2403
  behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("listField"), factory.createIdentifier("__kListField")))
2299
2404
  behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("listItem"), factory.createIdentifier("__kListItem")))
2405
+ behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("listIndex"), factory.createIdentifier("__kListIndex")))
2300
2406
  behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("listConditional"), factory.createIdentifier("__kListConditional")))
2301
2407
  }
2302
2408
  if (usesListItem && !usesList) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("listItem"), factory.createIdentifier("__kListItem")))
2303
2409
  if (usesListEffects) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("useEffect"), factory.createIdentifier("__kListUseEffect")))
2304
2410
  if (usesRowState) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("useState"), factory.createIdentifier("__kRowUseState")))
2411
+ if (usesRowRef) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("useRef"), factory.createIdentifier("__kRowUseRef")))
2305
2412
  if (usesBinding || usesConditional) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("bindingValue"), factory.createIdentifier("__kBindingValue")))
2306
2413
  const behaviorImport = factory.createImportDeclaration(
2307
2414
  undefined,
@@ -2418,41 +2525,159 @@ function containsRenderControl(root, knownLocals) {
2418
2525
  return found
2419
2526
  }
2420
2527
 
2421
- function keyedListParts(expression, setters) {
2528
+ function keyedListParts(expression, setters, declarations, fail, aliases = new Set()) {
2422
2529
  const value = unwrapExpression(expression)
2423
- if (!ts.isCallExpression(value) || value.arguments.length !== 1 || !ts.isPropertyAccessExpression(value.expression) || value.expression.name.text !== "map" || !ts.isIdentifier(value.expression.expression)) return undefined
2424
- const state = value.expression.expression
2425
- if (![...setters.values()].includes(state.text)) return undefined
2426
- const callback = value.arguments[0]
2427
- if (!ts.isArrowFunction(callback) || callback.parameters.length !== 1 || !ts.isIdentifier(callback.parameters[0].name)) {
2428
- throw new Error("Keyed list map callback must be an arrow function with one identifier parameter")
2429
- }
2530
+ const directFrom = isArrayFromCall(value) && value.arguments.length === 2 && containsJsx(value.arguments[1])
2531
+ if (!directFrom && (!ts.isCallExpression(value) || value.arguments.length !== 1 || !ts.isPropertyAccessExpression(value.expression) || value.expression.name.text !== "map")) return undefined
2532
+ const collection = renderedCollectionSource(directFrom ? value.arguments[0] : value.expression.expression, setters, declarations, fail, aliases)
2533
+ if (!collection?.state) return undefined
2534
+ if (directFrom) collection.selector.push(["from", undefined])
2535
+ const callback = directFrom ? value.arguments[1] : value.arguments[0]
2536
+ const parameters = collectionParameters(callback, "Keyed list map", fail)
2430
2537
  const root = unwrapExpression(callback.body)
2431
- if (!ts.isJsxElement(root) && !ts.isJsxSelfClosingElement(root)) throw new Error("Keyed list map callback must return one JSX element")
2538
+ if (!ts.isJsxElement(root) && !ts.isJsxSelfClosingElement(root)) fail(callback.body, "Keyed list map callback must return one JSX element")
2432
2539
  const attributes = ts.isJsxElement(root) ? root.openingElement.attributes : root.attributes
2433
2540
  const key = attributes.properties.find(attribute => ts.isJsxAttribute(attribute) && ts.isIdentifier(attribute.name) && attribute.name.text === "key")
2434
- const field = key && ts.isJsxAttribute(key) && key.initializer && ts.isJsxExpression(key.initializer) && key.initializer.expression && directProperty(key.initializer.expression, callback.parameters[0].name.text)
2435
- if (!field) throw new Error(`Keyed list root must have key={${callback.parameters[0].name.text}.<field>}`)
2436
- return { state, callback, root, item: callback.parameters[0].name.text, keyField: field }
2541
+ const keyExpression = key && ts.isJsxAttribute(key) && key.initializer && ts.isJsxExpression(key.initializer) && key.initializer.expression
2542
+ const field = keyExpression && directProperty(keyExpression, parameters.item)
2543
+ const positional = Boolean(keyExpression && parameters.index && ts.isIdentifier(unwrapExpression(keyExpression)) && unwrapExpression(keyExpression).text === parameters.index)
2544
+ if (!field && !positional) fail(key ?? root, `Keyed list root must have key={${parameters.item}.<field>} or key={${parameters.index ?? "index"}}`)
2545
+ return { ...collection, callback, root, item: parameters.item, index: parameters.index, indexed: Boolean(parameters.index), keyField: positional ? null : field }
2437
2546
  }
2438
2547
 
2439
- function nestedKeyedListParts(expression, parentItem) {
2548
+ function nestedKeyedListParts(expression, parentItem, fail) {
2440
2549
  const value = unwrapExpression(expression)
2441
2550
  if (!ts.isCallExpression(value) || value.arguments.length !== 1 || !ts.isPropertyAccessExpression(value.expression) || value.expression.name.text !== "map") return undefined
2442
- const collection = value.expression.expression
2443
- if (!ts.isPropertyAccessExpression(collection) || !ts.isIdentifier(collection.expression) || collection.expression.text !== parentItem) return undefined
2551
+ const collection = renderedCollectionSource(value.expression.expression, new Map(), undefined, fail, new Set())
2552
+ if (!collection?.ownerField || collection.parentItem !== parentItem) return undefined
2444
2553
  const callback = value.arguments[0]
2445
- if (!ts.isArrowFunction(callback) || callback.parameters.length !== 1 || !ts.isIdentifier(callback.parameters[0].name)) {
2446
- throw new Error("Nested keyed list map callback must be an arrow function with one identifier parameter")
2447
- }
2554
+ const parameters = collectionParameters(callback, "Nested keyed list map", fail)
2448
2555
  const root = unwrapExpression(callback.body)
2449
- if (!ts.isJsxElement(root) && !ts.isJsxSelfClosingElement(root)) throw new Error("Nested keyed list map callback must return one JSX element")
2556
+ if (!ts.isJsxElement(root) && !ts.isJsxSelfClosingElement(root)) fail(callback.body, "Nested keyed list map callback must return one JSX element")
2450
2557
  const attributes = ts.isJsxElement(root) ? root.openingElement.attributes : root.attributes
2451
2558
  const key = attributes.properties.find(attribute => ts.isJsxAttribute(attribute) && ts.isIdentifier(attribute.name) && attribute.name.text === "key")
2452
- const item = callback.parameters[0].name.text
2453
- const keyField = key && ts.isJsxAttribute(key) && key.initializer && ts.isJsxExpression(key.initializer) && key.initializer.expression && directProperty(key.initializer.expression, item)
2454
- if (!keyField) throw new Error(`Nested keyed list root must have key={${item}.<field>}`)
2455
- return { callback, root, item, keyField, ownerField: collection.name.text }
2559
+ const keyExpression = key && ts.isJsxAttribute(key) && key.initializer && ts.isJsxExpression(key.initializer) && key.initializer.expression
2560
+ const keyField = keyExpression && directProperty(keyExpression, parameters.item)
2561
+ const positional = Boolean(keyExpression && parameters.index && ts.isIdentifier(unwrapExpression(keyExpression)) && unwrapExpression(keyExpression).text === parameters.index)
2562
+ if (!keyField && !positional) fail(key ?? root, `Nested keyed list root must have key={${parameters.item}.<field>} or key={${parameters.index ?? "index"}}`)
2563
+ return { ...collection, callback, root, item: parameters.item, index: parameters.index, indexed: Boolean(parameters.index), keyField: positional ? null : keyField }
2564
+ }
2565
+
2566
+ function renderedCollectionSource(expression, setters, declarations, fail, aliases) {
2567
+ const value = unwrapExpression(expression)
2568
+ if (ts.isIdentifier(value)) {
2569
+ if ([...setters.values()].includes(value.text)) return { state: value, selector: [] }
2570
+ const entries = declarations?.get(value.text)
2571
+ if (!entries) return undefined
2572
+ if (entries.length !== 1 || aliases.has(value.text) || entries[0].node.parent?.parent?.parent !== nearestFunction(entries[0].node)?.body) fail(value, `Rendered collection alias "${value.text}" must be one top-level immutable local`)
2573
+ if (identifierReferenceCount(nearestFunction(entries[0].node).body, value.text) !== 1) fail(value, `Rendered collection alias "${value.text}" may only be rendered once`)
2574
+ aliases.add(value.text)
2575
+ const source = renderedCollectionSource(entries[0].initializer, setters, declarations, fail, aliases)
2576
+ aliases.delete(value.text)
2577
+ return source && { ...source, aliasDeclarations: [...(source.aliasDeclarations ?? []), entries[0].node] }
2578
+ }
2579
+ if (ts.isPropertyAccessExpression(value) && ts.isIdentifier(value.expression)) return { state: undefined, ownerField: value.name.text, selector: [], parentItem: value.expression.text }
2580
+ if (ts.isCallExpression(value) && ts.isPropertyAccessExpression(value.expression)) {
2581
+ const method = value.expression.name.text
2582
+ if (method === "filter") {
2583
+ if (value.arguments.length !== 1) fail(value, "Rendered collection filter() requires one inline predicate")
2584
+ const source = renderedCollectionSource(value.expression.expression, setters, declarations, fail, aliases)
2585
+ if (!source) return undefined
2586
+ const parameters = collectionParameters(value.arguments[0], "Rendered collection filter()", fail)
2587
+ return { ...source, selector: [...source.selector, ["filter", collectionExpression(unwrapExpression(value.arguments[0].body), parameters, fail)]] }
2588
+ }
2589
+ if (method === "flatMap") {
2590
+ if (value.arguments.length !== 1) fail(value, "Rendered collection flatMap() requires one inline projector")
2591
+ const source = renderedCollectionSource(value.expression.expression, setters, declarations, fail, aliases)
2592
+ if (!source) return undefined
2593
+ const parameters = collectionParameters(value.arguments[0], "Rendered collection flatMap()", fail)
2594
+ const field = directProperty(value.arguments[0].body, parameters.item)
2595
+ if (!field) fail(value.arguments[0].body, `Rendered collection flatMap() projector must be ${parameters.item}.<field>`)
2596
+ if (["__proto__", "constructor", "prototype"].includes(field)) fail(value.arguments[0].body, `Rendered collection property "${field}" is not supported`)
2597
+ return { ...source, selector: [...source.selector, ["flatMap", field]] }
2598
+ }
2599
+ }
2600
+ if (isArrayFromCall(value)) {
2601
+ if (value.arguments.length < 1 || value.arguments.length > 2) fail(value, "Rendered Array.from() requires an anchor and optional inline mapper")
2602
+ const source = renderedCollectionSource(value.arguments[0], setters, declarations, fail, aliases)
2603
+ if (!source) return undefined
2604
+ let mapper
2605
+ if (value.arguments[1]) {
2606
+ const parameters = collectionParameters(value.arguments[1], "Rendered Array.from() mapper", fail)
2607
+ mapper = collectionExpression(unwrapExpression(value.arguments[1].body), parameters, fail)
2608
+ }
2609
+ return { ...source, selector: [...source.selector, ["from", mapper]] }
2610
+ }
2611
+ }
2612
+
2613
+ function isArrayFromCall(value) {
2614
+ return ts.isCallExpression(value) && ts.isPropertyAccessExpression(value.expression) && ts.isIdentifier(value.expression.expression) && value.expression.expression.text === "Array" && value.expression.name.text === "from"
2615
+ }
2616
+
2617
+ function collectionParameters(callback, label, fail) {
2618
+ if (!ts.isArrowFunction(callback) || callback.parameters.length < 1 || callback.parameters.length > 2 || callback.parameters.some(parameter => !ts.isIdentifier(parameter.name))) fail(callback, `${label} callback must be an arrow function with (item) or (item, index) identifier parameters`)
2619
+ return { item: callback.parameters[0].name.text, index: callback.parameters[1]?.name.text }
2620
+ }
2621
+
2622
+ function collectionExpression(expression, parameters, fail) {
2623
+ const encode = node => {
2624
+ node = unwrapExpression(node)
2625
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) || ts.isNumericLiteral(node)) return ["value", ts.isNumericLiteral(node) ? Number(node.text) : node.text]
2626
+ if (node.kind === ts.SyntaxKind.TrueKeyword) return ["value", true]
2627
+ if (node.kind === ts.SyntaxKind.FalseKeyword) return ["value", false]
2628
+ if (node.kind === ts.SyntaxKind.NullKeyword) return ["value", null]
2629
+ if (ts.isIdentifier(node)) {
2630
+ if (node.text === parameters.item) return ["item"]
2631
+ if (node.text === parameters.index) return ["index"]
2632
+ if (node.text === "undefined") return ["undefined"]
2633
+ fail(node, `Rendered collection expression identifier "${node.text}" is not allowed`)
2634
+ }
2635
+ if (ts.isPropertyAccessExpression(node)) {
2636
+ if (["__proto__", "constructor", "prototype"].includes(node.name.text)) fail(node, `Rendered collection property "${node.name.text}" is not supported`)
2637
+ return ["get", encode(node.expression), node.name.text, Boolean(node.questionDotToken)]
2638
+ }
2639
+ if (ts.isElementAccessExpression(node)) {
2640
+ const key = node.argumentExpression
2641
+ if (!ts.isStringLiteral(key) && !ts.isNumericLiteral(key)) fail(node, "Rendered collection computed properties require a direct string or numeric literal key")
2642
+ if (ts.isStringLiteral(key) && ["__proto__", "constructor", "prototype"].includes(key.text)) fail(node, `Rendered collection property "${key.text}" is not supported`)
2643
+ return ["get", encode(node.expression), ts.isNumericLiteral(key) ? Number(key.text) : key.text, Boolean(node.questionDotToken)]
2644
+ }
2645
+ if (ts.isPrefixUnaryExpression(node)) {
2646
+ const operator = node.operator === ts.SyntaxKind.ExclamationToken ? "!" : node.operator === ts.SyntaxKind.PlusToken ? "+" : node.operator === ts.SyntaxKind.MinusToken ? "-" : undefined
2647
+ if (!operator) fail(node, "Rendered collection expression uses an unsupported unary operator")
2648
+ return ["unary", operator, encode(node.operand)]
2649
+ }
2650
+ if (ts.isTypeOfExpression(node)) return ["unary", "typeof", encode(node.expression)]
2651
+ if (ts.isBinaryExpression(node)) {
2652
+ const operator = node.operatorToken.getText()
2653
+ if (!new Set(["&&", "||", "??", "===", "!==", "==", "!=", "<", "<=", ">", ">=", "+", "-", "*", "/", "%"]).has(operator)) fail(node, `Rendered collection expression operator "${operator}" is not supported`)
2654
+ return ["binary", operator, encode(node.left), encode(node.right)]
2655
+ }
2656
+ if (ts.isConditionalExpression(node)) return ["conditional", encode(node.condition), encode(node.whenTrue), encode(node.whenFalse)]
2657
+ if (ts.isArrayLiteralExpression(node) && !node.elements.some(ts.isSpreadElement)) return ["array", ...node.elements.map(encode)]
2658
+ if (ts.isObjectLiteralExpression(node)) return ["object", ...node.properties.map(property => {
2659
+ if (!ts.isPropertyAssignment(property) || !ts.isIdentifier(property.name) && !ts.isStringLiteral(property.name) && !ts.isNumericLiteral(property.name)) fail(property, "Rendered collection mapper objects require direct properties")
2660
+ return [property.name.text, encode(property.initializer)]
2661
+ })]
2662
+ if (ts.isTemplateExpression(node)) return ["template", [node.head.text, ...node.templateSpans.map(span => span.literal.text)], node.templateSpans.map(span => encode(span.expression))]
2663
+ if (ts.isCallExpression(node)) {
2664
+ if (ts.isIdentifier(node.expression) && ["Boolean", "Number", "String"].includes(node.expression.text)) return ["global", node.expression.text, ...node.arguments.map(encode)]
2665
+ if (ts.isPropertyAccessExpression(node.expression)) {
2666
+ const method = node.expression.name.text
2667
+ if (ts.isIdentifier(node.expression.expression) && node.expression.expression.text === "Math" && pureMathMethods.has(method)) return ["math", method, ...node.arguments.map(encode)]
2668
+ if (pureListMethods.has(method)) return ["call", encode(node.expression.expression), method, ...node.arguments.map(encode)]
2669
+ if (mutatingListMethods.has(method)) fail(node, `Rendered collection expressions cannot call mutating method "${method}"`)
2670
+ }
2671
+ fail(node, "Rendered collection expressions cannot call arbitrary functions")
2672
+ }
2673
+ if (ts.isArrowFunction(node) || ts.isFunctionExpression(node) || ts.isAwaitExpression(node) || ts.isNewExpression(node) || ts.isYieldExpression(node) || ts.isDeleteExpression(node) || ts.isPostfixUnaryExpression(node)) fail(node, "Rendered collection expressions must be pure and synchronous")
2674
+ fail(node, "Rendered collection expression is not supported")
2675
+ }
2676
+ return encode(expression)
2677
+ }
2678
+
2679
+ function jsonExpression(value, factory) {
2680
+ return factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("JSON"), "parse"), undefined, [factory.createStringLiteral(JSON.stringify(value))])
2456
2681
  }
2457
2682
 
2458
2683
  function isStateBackedListComponentCall(call, component, setters) {
@@ -2539,39 +2764,36 @@ function insideJsxEventHandler(node, root) {
2539
2764
  return false
2540
2765
  }
2541
2766
 
2542
- function validateKeyedList(parts, sourceFile, listValues, listEventItems, listConditions, setters, rowState, nestedLists, componentSpecializations, factory) {
2767
+ function validateKeyedList(parts, sourceFile, listValues, listEventItems, listConditions, setters, rowStates, nestedLists, componentSpecializations, expandedRowSpecializations, nestedRowSpecializations, factory, prepareListCallback) {
2543
2768
  const fail = (node, message) => {
2544
2769
  throw sourceNodeError(node, sourceFile, message)
2545
2770
  }
2546
2771
  const root = parts.root
2547
2772
  const item = parts.item
2773
+ const nestedDiagnostic = "Nested keyed list collections must be a direct property of the parent item"
2548
2774
  const validateElement = node => {
2549
2775
  const tag = ts.isJsxElement(node) ? node.openingElement.tagName : node.tagName
2550
2776
  if (!ts.isIdentifier(tag) || tag.text[0] !== tag.text[0].toLowerCase()) fail(node, "Keyed list items must use intrinsic JSX elements")
2551
2777
  }
2552
- let conditionDepth = 0
2553
- let nestedList
2554
2778
  const visit = node => {
2555
2779
  if (ts.isJsxFragment(node)) fail(node, "Fragments are not supported in keyed lists")
2556
2780
  if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) validateElement(node)
2557
- if (node !== root && ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "map" && containsJsx(node)) fail(node, parts.nested ? "Keyed lists support at most one nested level" : "Nested keyed list collections must be a direct property of the parent item")
2781
+ if (node !== root && ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "map" && containsJsx(node)) fail(node, nestedDiagnostic)
2558
2782
  if (ts.isJsxSpreadAttribute(node) && referencesIdentifier(node.expression, item)) fail(node, "Keyed list item spreads are not supported")
2559
2783
  if (ts.isJsxAttribute(node) && /^on[A-Z]/.test(node.name.text)) {
2560
- listEventItems.set(node, item)
2784
+ listEventItems.set(node, { item, index: parts.index })
2561
2785
  return
2562
2786
  }
2563
2787
  if (ts.isJsxExpression(node) && node.expression) {
2564
2788
  const expression = unwrapExpression(node.expression)
2565
2789
  if (containsJsx(expression) && ts.isCallExpression(expression) && ts.isPropertyAccessExpression(expression.expression) && expression.expression.name.text === "map") {
2566
- const nested = nestedKeyedListParts(expression, item)
2567
- if (!nested) fail(expression, parts.nested ? "Keyed lists support at most one nested level" : "Nested keyed list collections must be a direct property of the parent item")
2568
- if (parts.nested) fail(expression, "Keyed lists support at most one nested level")
2569
- if (nestedList) fail(expression, "Keyed list rows support one nested keyed list")
2790
+ const nested = nestedKeyedListParts(expression, item, fail)
2791
+ if (!nested) fail(expression, nestedDiagnostic)
2792
+ if (["__proto__", "constructor", "prototype"].includes(nested.ownerField)) fail(expression, `Nested keyed list owner property "${nested.ownerField}" is not supported`)
2570
2793
  if (referenceIdentifiers(nested.callback, item).length) fail(nested.root, "Nested keyed list rows cannot capture the parent item")
2571
- nestedList = nested
2572
- const specialization = componentSpecializations.get(nested.root)
2794
+ const specialization = componentSpecializations.get(nested.root) ?? expandedRowSpecializations.get(nested.root) ?? nestedRowSpecializations.get(`${expression.pos}:${expression.end}`)
2573
2795
  const root = specialization?.root ?? nested.root
2574
- const callback = root === nested.root ? nested.callback : factory.updateArrowFunction(
2796
+ let callback = root === nested.root ? nested.callback : factory.updateArrowFunction(
2575
2797
  nested.callback,
2576
2798
  nested.callback.modifiers,
2577
2799
  nested.callback.typeParameters,
@@ -2584,6 +2806,7 @@ function validateKeyedList(parts, sourceFile, listValues, listEventItems, listCo
2584
2806
  ts.setParentRecursive(callback, false)
2585
2807
  callback.parent = nested.callback.parent
2586
2808
  }
2809
+ callback = prepareListCallback(callback, root, specialization ?? { hookDeclarations: [], effects: [] }, nested.item)
2587
2810
  const nestedParts = { ...nested, root, callback, state: parts.state, nested: true }
2588
2811
  for (const calculation of specialization?.calculations ?? []) {
2589
2812
  ts.setParentRecursive(calculation, false)
@@ -2591,26 +2814,21 @@ function validateKeyedList(parts, sourceFile, listValues, listEventItems, listCo
2591
2814
  validateListExpression(calculation, nested.item, nested.root, fail)
2592
2815
  }
2593
2816
  nestedLists.set(expression, nestedParts)
2594
- validateKeyedList(nestedParts, sourceFile, listValues, listEventItems, listConditions, setters, undefined, nestedLists, componentSpecializations, factory)
2817
+ validateKeyedList(nestedParts, sourceFile, listValues, listEventItems, listConditions, setters, specialization?.rowStates ?? [], nestedLists, componentSpecializations, expandedRowSpecializations, nestedRowSpecializations, factory, prepareListCallback)
2595
2818
  return
2596
2819
  }
2597
2820
  const condition = conditionalParts(expression)
2598
2821
  if (condition && containsJsx(expression)) {
2599
- if (conditionDepth) fail(node, "Nested item conditions are not supported in keyed lists")
2600
- if (rowState && referencedStateNames(condition.condition, setters).has(rowState.state)) {
2601
- conditionDepth++
2822
+ if (rowStates.some(rowState => referencedStateNames(condition.condition, setters).has(rowState.state))) {
2602
2823
  visit(condition.truthy)
2603
2824
  visit(condition.falsy)
2604
- conditionDepth--
2605
2825
  return
2606
2826
  }
2607
- if (!referencesIdentifier(condition.condition, item)) fail(node, "Keyed list item conditions must read the item")
2608
- validateListExpression(condition.condition, item, node, fail)
2609
- listConditions.set(node.expression, { ...condition, item })
2610
- conditionDepth++
2827
+ if (!referencesIdentifier(condition.condition, item) && !(parts.index && referencesIdentifier(condition.condition, parts.index))) fail(node, "Keyed list item conditions must read the item or index")
2828
+ validateListExpression(condition.condition, item, node, fail, parts.index)
2829
+ listConditions.set(node.expression, { ...condition, item, index: parts.index })
2611
2830
  visit(condition.truthy)
2612
2831
  visit(condition.falsy)
2613
- conditionDepth--
2614
2832
  return
2615
2833
  }
2616
2834
  const field = directProperty(expression, item)
@@ -2622,10 +2840,10 @@ function validateKeyedList(parts, sourceFile, listValues, listEventItems, listCo
2622
2840
  listValues.set(node.expression, { field })
2623
2841
  return
2624
2842
  }
2625
- if (referencesIdentifier(expression, item)) {
2626
- validateListExpression(expression, item, node, fail)
2843
+ if (referencesIdentifier(expression, item) || parts.index && referencesIdentifier(expression, parts.index)) {
2844
+ validateListExpression(expression, item, node, fail, parts.index)
2627
2845
  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`)
2628
- listValues.set(node.expression, { item })
2846
+ listValues.set(node.expression, { item, index: parts.index })
2629
2847
  return
2630
2848
  }
2631
2849
  }
@@ -2634,7 +2852,7 @@ function validateKeyedList(parts, sourceFile, listValues, listEventItems, listCo
2634
2852
  visit(root)
2635
2853
  }
2636
2854
 
2637
- function specializeComponentCall(call, component, sourceFile, factory, context, fail, label = "Keyed list") {
2855
+ function specializeComponentCall(call, component, sourceFile, factory, context, fail, label = "Keyed list", allowComponentRoot = false) {
2638
2856
  if (component.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword) || component.asteriskToken) fail(component, `${label} components must be synchronous`)
2639
2857
  if (component.parameters.length !== 1 || !ts.isObjectBindingPattern(component.parameters[0].name)) fail(component, `${label} components must use one destructured props parameter`)
2640
2858
  if (ts.isJsxElement(call) && call.children.some(child => !ts.isJsxText(child) || child.text.trim())) fail(call, `${label} component children are not supported`)
@@ -2669,8 +2887,9 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
2669
2887
  let returned
2670
2888
  const calculations = []
2671
2889
  const effectCalls = []
2672
- const stateDeclarations = []
2673
- let rowState
2890
+ const hookDeclarations = []
2891
+ const rowStates = []
2892
+ const rowRefs = []
2674
2893
  if (!ts.isBlock(component.body)) {
2675
2894
  returned = component.body
2676
2895
  } else {
@@ -2685,11 +2904,9 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
2685
2904
  if (!ts.isVariableStatement(statement) || (statement.declarationList.flags & ts.NodeFlags.Const) === 0 || statement.declarationList.declarations.length !== 1) fail(statement, `${label} component locals must be single const declarations`)
2686
2905
  const declaration = statement.declarationList.declarations[0]
2687
2906
  if (declaration.initializer && ts.isCallExpression(declaration.initializer) && ts.isIdentifier(declaration.initializer.expression) && declaration.initializer.expression.text === "useState") {
2688
- if (label !== "Reducer-dispatch") fail(declaration, `${label} components cannot declare local state`)
2689
- if (rowState) throw sourceNodeError(declaration, component.getSourceFile(), "Reducer-dispatch keyed row components may declare exactly one top-level useState()")
2690
- if (declaration.initializer.arguments.length !== 1 || !isPrimitiveDefaultLiteral(declaration.initializer.arguments[0])) throw sourceNodeError(declaration.initializer, component.getSourceFile(), "Reducer-dispatch keyed row useState() must use one primitive literal initial value; lazy initialization is not supported")
2691
- if (!ts.isArrayBindingPattern(declaration.name) || declaration.name.elements.length !== 2 || declaration.name.elements.some(element => !element || !ts.isBindingElement(element) || !ts.isIdentifier(element.name) || element.initializer || element.dotDotDotToken)) throw sourceNodeError(declaration.name, component.getSourceFile(), "Reducer-dispatch keyed row useState() must use [state, setter] identifier destructuring")
2692
- const suffix = Math.max(0, call.pos)
2907
+ if (declaration.initializer.arguments.length !== 1 || !isSerializableStateLiteral(declaration.initializer.arguments[0])) throw sourceNodeError(declaration.initializer, component.getSourceFile(), "Keyed row useState() must use one directly serializable primitive, plain object, or array initial value; lazy and dynamic initializers are not supported")
2908
+ if (!ts.isArrayBindingPattern(declaration.name) || declaration.name.elements.length !== 2 || declaration.name.elements.some(element => !element || !ts.isBindingElement(element) || !ts.isIdentifier(element.name) || element.initializer || element.dotDotDotToken)) throw sourceNodeError(declaration.name, component.getSourceFile(), "Keyed row useState() must use [state, setter] identifier destructuring")
2909
+ const suffix = `${Math.max(0, call.pos)}_${rowStates.length}`
2693
2910
  const state = `__kRowState${suffix}`
2694
2911
  const setter = `__kRowSetter${suffix}`
2695
2912
  substitutions.set(declaration.name.elements[0].name.text, factory.createIdentifier(state))
@@ -2698,9 +2915,21 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
2698
2915
  factory.createBindingElement(undefined, undefined, factory.createIdentifier(state)),
2699
2916
  factory.createBindingElement(undefined, undefined, factory.createIdentifier(setter))
2700
2917
  ])
2701
- const initializer = factory.createCallExpression(factory.createIdentifier("__kRowUseState"), undefined, [cloneAst(declaration.initializer.arguments[0], factory, context)])
2702
- stateDeclarations.push(factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(binding, undefined, undefined, initializer)], ts.NodeFlags.Const)))
2703
- rowState = { state, setter }
2918
+ const initialValue = cloneAst(declaration.initializer.arguments[0], factory, context)
2919
+ synthesizeTree(initialValue)
2920
+ const initializer = factory.createCallExpression(factory.createIdentifier("__kRowUseState"), undefined, [initialValue])
2921
+ hookDeclarations.push(factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(binding, undefined, undefined, initializer)], ts.NodeFlags.Const)))
2922
+ rowStates.push({ state, setter })
2923
+ continue
2924
+ }
2925
+ if (declaration.initializer && ts.isCallExpression(declaration.initializer) && ts.isIdentifier(declaration.initializer.expression) && declaration.initializer.expression.text === "useRef") {
2926
+ if (declaration.initializer.arguments.length !== 1 || declaration.initializer.arguments[0].kind !== ts.SyntaxKind.NullKeyword) throw sourceNodeError(declaration.initializer, component.getSourceFile(), "Keyed row useRef() must use the direct initial value null")
2927
+ if (!ts.isIdentifier(declaration.name)) throw sourceNodeError(declaration.name, component.getSourceFile(), "Keyed row useRef() must be assigned to one identifier")
2928
+ const name = `__kRowRef${Math.max(0, call.pos)}_${rowRefs.length}`
2929
+ substitutions.set(declaration.name.text, factory.createIdentifier(name))
2930
+ const initializer = factory.createCallExpression(factory.createIdentifier("__kRowUseRef"), declaration.initializer.typeArguments?.map(type => cloneAst(type, factory, context)), [factory.createNull()])
2931
+ hookDeclarations.push(factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(factory.createIdentifier(name), undefined, undefined, initializer)], ts.NodeFlags.Const)))
2932
+ rowRefs.push({ name })
2704
2933
  continue
2705
2934
  }
2706
2935
  if (!ts.isIdentifier(declaration.name) || !declaration.initializer) fail(declaration, `${label} component locals must be initialized identifiers`)
@@ -2710,19 +2939,19 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
2710
2939
  }
2711
2940
  returned = last.expression
2712
2941
  }
2713
- let unsupportedState
2714
- const findUnsupportedState = node => {
2715
- if (unsupportedState) return
2716
- if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "useState") unsupportedState = node
2717
- ts.forEachChild(node, findUnsupportedState)
2942
+ let unsupportedHook
2943
+ const findUnsupportedHook = node => {
2944
+ if (unsupportedHook) return
2945
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && ["useState", "useRef"].includes(node.expression.text)) unsupportedHook = node
2946
+ ts.forEachChild(node, findUnsupportedHook)
2718
2947
  }
2719
- findUnsupportedState(returned)
2720
- for (const calculation of calculations) findUnsupportedState(calculation.expression)
2721
- if (unsupportedState) throw sourceNodeError(unsupportedState, component.getSourceFile(), label === "Reducer-dispatch" ? "Reducer-dispatch keyed row useState() must be one top-level const declaration" : `${label} components cannot declare local state`)
2948
+ findUnsupportedHook(returned)
2949
+ for (const calculation of calculations) findUnsupportedHook(calculation.expression)
2950
+ if (unsupportedHook) throw sourceNodeError(unsupportedHook, component.getSourceFile(), `Keyed row ${unsupportedHook.expression.text}() must be one top-level const declaration`)
2722
2951
  let root = unwrapExpression(substituteClone(returned, substitutions, factory, context))
2723
2952
  if (!ts.isJsxElement(root) && !ts.isJsxSelfClosingElement(root)) fail(returned, `${label} component must return one JSX element`)
2724
2953
  const tag = jsxTagName(root)
2725
- if (!ts.isIdentifier(tag) || tag.text[0] !== tag.text[0].toLowerCase()) fail(returned, `${label} component must directly return an intrinsic JSX element`)
2954
+ if (!ts.isIdentifier(tag) || !allowComponentRoot && tag.text[0] !== tag.text[0].toLowerCase()) fail(returned, `${label} component must directly return an intrinsic JSX element`)
2726
2955
  const rootAttributes = ts.isJsxElement(root) ? root.openingElement.attributes : root.attributes
2727
2956
  if (rootAttributes.properties.some(attribute => ts.isJsxAttribute(attribute) && attribute.name.text === "key")) fail(root, `${label} component intrinsic root cannot declare key`)
2728
2957
  if (key) root = addJsxAttribute(root, cloneAst(key, factory, context), factory)
@@ -2735,11 +2964,20 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
2735
2964
  .filter(calculation => label !== "Reducer-dispatch" || !isFunctionLike(calculation.expression) || !isEventOnlyComponentLocal(returned, calculation.name))
2736
2965
  .map(calculation => calculation.expression),
2737
2966
  effects,
2738
- stateDeclarations,
2739
- rowState
2967
+ hookDeclarations,
2968
+ rowStates,
2969
+ rowRefs
2740
2970
  }
2741
2971
  }
2742
2972
 
2973
+ function isSerializableStateLiteral(node) {
2974
+ const value = unwrapExpression(node)
2975
+ if (isPrimitiveDefaultLiteral(value)) return true
2976
+ if (ts.isArrayLiteralExpression(value)) return value.elements.every(element => !ts.isSpreadElement(element) && !ts.isOmittedExpression(element) && isSerializableStateLiteral(element))
2977
+ if (!ts.isObjectLiteralExpression(value)) return false
2978
+ return value.properties.every(property => ts.isPropertyAssignment(property) && !ts.isComputedPropertyName(property.name) && property.name.text !== "__proto__" && isSerializableStateLiteral(property.initializer))
2979
+ }
2980
+
2743
2981
  function isPrimitiveDefaultLiteral(node) {
2744
2982
  return ts.isStringLiteral(node) || ts.isNumericLiteral(node) ||
2745
2983
  (ts.isPrefixUnaryExpression(node) && (node.operator === ts.SyntaxKind.PlusToken || node.operator === ts.SyntaxKind.MinusToken) && ts.isNumericLiteral(node.operand)) ||
@@ -2840,7 +3078,7 @@ function isStylesheetLink(node) {
2840
3078
  }
2841
3079
 
2842
3080
  function isContextProviderValue(node, contexts) {
2843
- if (node.name.getText() !== "value") return false
3081
+ if (node.name.text !== "value") return false
2844
3082
  const element = node.parent?.parent
2845
3083
  const tag = ts.isJsxOpeningElement(element) || ts.isJsxSelfClosingElement(element) ? element.tagName : undefined
2846
3084
  return ts.isPropertyAccessExpression(tag) && tag.name.text === "Provider" && ts.isIdentifier(tag.expression) && contexts.has(tag.expression.text)
@@ -2888,7 +3126,7 @@ const assignmentOperators = new Set([
2888
3126
  ts.SyntaxKind.QuestionQuestionEqualsToken
2889
3127
  ])
2890
3128
 
2891
- function validateListExpression(expression, item, source, fail) {
3129
+ function validateListExpression(expression, item, source, fail, index) {
2892
3130
  const visit = node => {
2893
3131
  if (ts.isTypeNode(node)) return
2894
3132
  if (ts.isElementAccessExpression(node) && referencesIdentifier(node.expression, item)) {
@@ -2919,7 +3157,7 @@ function validateListExpression(expression, item, source, fail) {
2919
3157
  fail(source, "Derived keyed list item expressions cannot call arbitrary functions")
2920
3158
  }
2921
3159
  }
2922
- if (ts.isIdentifier(node) && isReferenceIdentifier(node) && node.text !== item && !pureListGlobals.has(node.text)) {
3160
+ if (ts.isIdentifier(node) && isReferenceIdentifier(node) && node.text !== item && node.text !== index && !pureListGlobals.has(node.text)) {
2923
3161
  fail(source, `Derived keyed list item expression identifier "${node.text}" is not allowed`)
2924
3162
  }
2925
3163
  ts.forEachChild(node, visit)
@@ -2937,15 +3175,15 @@ function containsJsx(root) {
2937
3175
  return found
2938
3176
  }
2939
3177
 
2940
- function compileListExpression(read, expression, item, factory, listExpressions, handlerUrl) {
3178
+ function compileListExpression(read, expression, item, factory, listExpressions, handlerUrl, index) {
2941
3179
  const exportName = `listExpression${listExpressions.length}`
2942
- listExpressions.push({ exportName, expression, item })
3180
+ listExpressions.push({ exportName, expression, item, index })
2943
3181
  return factory.createCallExpression(factory.createIdentifier("__kListExpression"), undefined, [read, factory.createStringLiteral(handlerUrl), factory.createStringLiteral(exportName)])
2944
3182
  }
2945
3183
 
2946
3184
  function compileListConditional(entry, factory, listExpressions, handlerUrl) {
2947
3185
  const exportName = `listExpression${listExpressions.length}`
2948
- listExpressions.push({ exportName, expression: entry.condition, item: entry.item })
3186
+ listExpressions.push({ exportName, expression: entry.condition, item: entry.item, index: entry.index })
2949
3187
  const read = factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), entry.condition)
2950
3188
  const thunk = branch => factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), branch)
2951
3189
  return factory.createCallExpression(factory.createIdentifier("__kListConditional"), undefined, [
@@ -2957,7 +3195,7 @@ function compileListValue(expression, entry, factory, listExpressions, handlerUr
2957
3195
  const read = factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), expression)
2958
3196
  return entry.field
2959
3197
  ? factory.createCallExpression(factory.createIdentifier("__kListField"), undefined, [read, factory.createStringLiteral(entry.field)])
2960
- : compileListExpression(read, expression, entry.item, factory, listExpressions, handlerUrl)
3198
+ : compileListExpression(read, expression, entry.item, factory, listExpressions, handlerUrl, entry.index)
2961
3199
  }
2962
3200
 
2963
3201
  function directProperty(expression, objectName) {
@@ -3126,7 +3364,11 @@ function compileNativeCallback(expression, setters, reducers, factory, entries,
3126
3364
  ]))),
3127
3365
  scope: factory.createArrayLiteralExpression([...captures].map(name => factory.createArrayLiteralExpression([
3128
3366
  factory.createStringLiteral(name),
3129
- name === listItem ? factory.createCallExpression(factory.createIdentifier("__kListItem"), undefined, []) : value(name)
3367
+ name === (typeof listItem === "string" ? listItem : listItem?.item)
3368
+ ? factory.createCallExpression(factory.createIdentifier("__kListItem"), undefined, [])
3369
+ : name === listItem?.index
3370
+ ? factory.createCallExpression(factory.createIdentifier("__kListIndex"), undefined, [])
3371
+ : value(name)
3130
3372
  ])))
3131
3373
  }
3132
3374
  }
@@ -3879,13 +4121,13 @@ function printReactiveBinding({ exportName, expression, captures, states }) {
3879
4121
  }
3880
4122
  }
3881
4123
 
3882
- function printListExpression({ exportName, expression, item }) {
4124
+ function printListExpression({ exportName, expression, item, index }) {
3883
4125
  const declaration = ts.factory.createFunctionDeclaration(
3884
4126
  [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)],
3885
4127
  undefined,
3886
4128
  exportName,
3887
4129
  undefined,
3888
- [ts.factory.createParameterDeclaration(undefined, undefined, item)],
4130
+ [ts.factory.createParameterDeclaration(undefined, undefined, item), ts.factory.createParameterDeclaration(undefined, undefined, index ?? "__kIndex")],
3889
4131
  undefined,
3890
4132
  ts.factory.createBlock([ts.factory.createReturnStatement(expression)], true)
3891
4133
  )