@kudzujs/core 0.6.27 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
@@ -1632,6 +1694,7 @@ async function compile(file, sourceFiles, sourceIndex, base, workerReferences) {
1632
1694
  if (errors.length) {
1633
1695
  throw new Error(errors.map(error => ts.flattenDiagnosticMessageText(error.messageText, "\n")).join("\n"))
1634
1696
  }
1697
+ if (hasReactModuleReference(result.outputText, file)) throw new Error(`${relative(root, file)} Runtime React module references are not supported`)
1635
1698
 
1636
1699
  const output = compiledPath(file)
1637
1700
  await mkdir(resolve(output, ".."), { recursive: true })
@@ -1654,6 +1717,18 @@ async function compile(file, sourceFiles, sourceIndex, base, workerReferences) {
1654
1717
  return { path: handlerPath, code: moduleResult.outputText, hasNativeHandlers: nativeHandlers.length > 0, hasEffects: effectHandlers.length > 0, clientImports: [...clientImports] }
1655
1718
  }
1656
1719
 
1720
+ function hasReactModuleReference(source, file) {
1721
+ const sourceFile = ts.createSourceFile(file, source, ts.ScriptTarget.ES2022, true, ts.ScriptKind.JS)
1722
+ let found = false
1723
+ const visit = node => {
1724
+ if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text === "react") found = true
1725
+ if (ts.isCallExpression(node) && (node.expression.kind === ts.SyntaxKind.ImportKeyword || ts.isIdentifier(node.expression) && node.expression.text === "require") && ts.isStringLiteral(node.arguments[0]) && node.arguments[0].text === "react") found = true
1726
+ if (!found) ts.forEachChild(node, visit)
1727
+ }
1728
+ visit(sourceFile)
1729
+ return found
1730
+ }
1731
+
1657
1732
  function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings, listExpressions, handlerUrl, file, sourceFiles, sourceIndex, clientImports, workerReferences) {
1658
1733
  return context => sourceFile => {
1659
1734
  const factory = context.factory
@@ -1662,7 +1737,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
1662
1737
  ts.setParentRecursive(sourceFile, false)
1663
1738
  rejectOrdinaryWorkerImports(sourceFile, file, sourceFiles)
1664
1739
  const importBindings = clientImportBindings(sourceFile, file, sourceFiles)
1665
- const hasUseEffectImport = sourceFile.statements.some(statement => ts.isImportDeclaration(statement) && statement.moduleSpecifier.text === "@kudzujs/core" && statement.importClause?.namedBindings && ts.isNamedImports(statement.importClause.namedBindings) && statement.importClause.namedBindings.elements.some(entry => !entry.propertyName && entry.name.text === "useEffect"))
1740
+ const hasUseEffectImport = sourceFile.statements.some(statement => ts.isImportDeclaration(statement) && ["@kudzujs/core", "react"].includes(statement.moduleSpecifier.text) && statement.importClause?.namedBindings && ts.isNamedImports(statement.importClause.namedBindings) && statement.importClause.namedBindings.elements.some(entry => !entry.propertyName && entry.name.text === "useEffect"))
1666
1741
  const importedSources = new Map()
1667
1742
  const importedSource = target => {
1668
1743
  let imported = importedSources.get(target)
@@ -1694,6 +1769,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
1694
1769
  let usesListEffects = false
1695
1770
  let usesListItem = false
1696
1771
  let usesRowState = false
1772
+ let usesRowRef = false
1697
1773
 
1698
1774
  const collect = node => {
1699
1775
  if (ts.isVariableDeclaration(node) && node.initializer && ts.isCallExpression(node.initializer)) {
@@ -1779,7 +1855,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
1779
1855
  const setters = settersByFunction.get(owner) ?? new Map()
1780
1856
  for (const [name, entries] of declarations) {
1781
1857
  for (const declaration of entries) {
1782
- const parts = keyedListParts(declaration.initializer, setters)
1858
+ const parts = keyedListParts(declaration.initializer, setters, declarations, (target, message) => { throw sourceNodeError(target, sourceFile, message) })
1783
1859
  if (!parts) continue
1784
1860
  const uses = []
1785
1861
  const collectUses = node => {
@@ -1800,26 +1876,30 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
1800
1876
  throw sourceNodeError(node, sourceFile, message)
1801
1877
  }
1802
1878
  const componentSpecializations = new WeakMap()
1879
+ const expandedRowSpecializations = new WeakMap()
1880
+ const nestedRowSpecializations = new Map()
1803
1881
  const reducerComponentCalls = new WeakSet()
1804
- const reducerRowStateCalls = []
1882
+ const rowHookCalls = []
1805
1883
  const specializedDeclarations = new WeakSet()
1806
1884
  const stateBackedComponentFunctions = new WeakSet()
1807
1885
  const stateBackedComponentRoots = []
1808
1886
  let specializedImportIndex = 0
1809
- const registerReducerRowState = (call, specialization) => {
1810
- if (!specialization.rowState) return
1887
+ const registerRowHooks = (call, specialization) => {
1888
+ if (!specialization.rowStates.length && !specialization.rowRefs.length) return
1811
1889
  let owner
1812
1890
  for (let current = call.parent; current; current = current.parent) {
1813
- if (isFunctionLike(current) && reducersByFunction.has(current)) {
1891
+ if (isFunctionLike(current) && settersByFunction.has(current)) {
1814
1892
  owner = current
1815
1893
  break
1816
1894
  }
1817
1895
  }
1818
- const setters = settersByFunction.get(owner) ?? new Map()
1819
- setters.set(specialization.rowState.setter, specialization.rowState.state)
1896
+ if (!owner) owner = nearestFunction(call)
1897
+ const setters = new Map(settersByFunction.get(owner))
1898
+ for (const state of specialization.rowStates) setters.set(state.setter, state.state)
1820
1899
  settersByFunction.set(owner, setters)
1821
- reducerRowStateCalls.push(call)
1822
- usesRowState = true
1900
+ rowHookCalls.push(call)
1901
+ usesRowState ||= specialization.rowStates.length > 0
1902
+ usesRowRef ||= specialization.rowRefs.length > 0
1823
1903
  }
1824
1904
  const mergeSpecializedImports = (root, componentSource, call) => {
1825
1905
  const componentImports = clientImportBindings(componentSource, componentSource.fileName, sourceFiles)
@@ -1917,8 +1997,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
1917
1997
  for (const call of dispatchCalls) {
1918
1998
  if (componentSpecializations.has(call)) fail(call, "Reducer dispatch props cannot be combined with another component specialization")
1919
1999
  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)
2000
+ registerRowHooks(call, specialization)
1922
2001
  specialization.root = expandReducerCallbacks(specialization.root, component.function.getSourceFile(), call)
1923
2002
  componentSpecializations.set(call, specialization)
1924
2003
  reducerComponentCalls.add(call)
@@ -1941,8 +2020,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
1941
2020
  for (const call of dispatchCalls) {
1942
2021
  if (componentSpecializations.has(call)) fail(call, "Reducer dispatch props cannot be combined with another component specialization")
1943
2022
  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)
2023
+ registerRowHooks(call, specialization)
1946
2024
  specialization.root = expandReducerCallbacks(specialization.root, componentSource, call)
1947
2025
  specialization.root = mergeSpecializedImports(specialization.root, componentSource, call)
1948
2026
  synthesizeTree(specialization.root)
@@ -1958,8 +2036,11 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
1958
2036
  return
1959
2037
  }
1960
2038
  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 })
2039
+ const parts = listLocalUses.get(node) ?? keyedListParts(node.expression, settersForNode(node, settersByFunction), jsxLocalDeclarations.get(nearestFunction(node)), fail)
2040
+ if (parts) {
2041
+ for (const declaration of parts.aliasDeclarations ?? []) listLocalDeclarations.add(declaration)
2042
+ rawRenderedLists.push({ node, parts })
2043
+ }
1963
2044
  }
1964
2045
  ts.forEachChild(node, collectRenderedLists)
1965
2046
  }
@@ -1974,27 +2055,12 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
1974
2055
  ts.forEachChild(node, rejectUnsupportedRenderControl)
1975
2056
  }
1976
2057
  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 }) => {
2058
+ const listComponentNames = new Set(rawRenderedLists.flatMap(({ parts }) => {
1993
2059
  const tag = jsxTagName(parts.root)
1994
2060
  return tag && ts.isIdentifier(tag) && tag.text[0] === tag.text[0].toUpperCase() ? [tag.text] : []
1995
- }), ...[...nestedComponentCalls].map(call => jsxTagName(call).text)])
2061
+ }))
1996
2062
  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")
2063
+ for (const call of rowHookCalls) if (!keyedComponentCalls.has(call)) fail(call, "Keyed row hooks are only supported in direct keyed map rows")
1998
2064
  for (const name of listComponentNames) {
1999
2065
  let component = components.get(name)
2000
2066
  const local = Boolean(component)
@@ -2007,28 +2073,100 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
2007
2073
  if (local && isExportedDeclaration(component.declaration)) fail(component.declaration, `Keyed list component ${name} cannot be exported`)
2008
2074
  const declaredCalls = jsxTagUses(sourceFile, name)
2009
2075
  if (local && identifierReferenceCount(sourceFile, name) !== declaredCalls.length) fail(component.declaration, `Keyed list component ${name} may only be referenced as JSX`)
2010
- const calls = [
2076
+ const calls = [...new Set([
2011
2077
  ...declaredCalls.filter(call => !stateBackedComponentFunctions.has(nearestFunction(call))),
2012
2078
  ...stateBackedComponentRoots.flatMap(root => jsxTagUses(root, name))
2013
- ]
2079
+ ])]
2014
2080
  for (const call of calls) {
2015
2081
  const specialization = reducerComponentCalls.has(call)
2016
2082
  ? componentSpecializations.get(call)
2017
- : specializeComponentCall(call, component.function, sourceFile, factory, context, fail)
2083
+ : specializeComponentCall(call, component.function, sourceFile, factory, context, fail, "Keyed list", true)
2084
+ registerRowHooks(call, specialization)
2018
2085
  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
- }
2086
+ specialization.component = component.function
2087
+ specialization.componentSource = component.function.getSourceFile()
2088
+ specialization.imported = !local
2023
2089
  componentSpecializations.set(call, specialization)
2024
2090
  }
2025
2091
  if (local) specializedDeclarations.add(component.declaration)
2026
2092
  }
2093
+ const expandKeyedComponents = (root, componentSource, trail = [], aggregate) => {
2094
+ const replacements = new WeakMap()
2095
+ let count = 0
2096
+ const visit = (node, currentAggregate = aggregate) => {
2097
+ if (node !== root && ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "map" && containsJsx(node)) {
2098
+ const nestedAggregate = { calculations: [], effects: [], hookDeclarations: [], rowStates: [], rowRefs: [] }
2099
+ for (const argument of node.arguments) visit(argument, nestedAggregate)
2100
+ if (nestedAggregate.hookDeclarations.length || nestedAggregate.effects.length) nestedRowSpecializations.set(`${node.pos}:${node.end}`, nestedAggregate)
2101
+ return
2102
+ }
2103
+ const tag = jsxTagName(node)
2104
+ if (tag && (!ts.isIdentifier(tag) || tag.text[0] !== tag.text[0].toLowerCase())) {
2105
+ if (!ts.isIdentifier(tag)) fail(node, "Keyed list components must use identifier JSX tags")
2106
+ const name = tag.text
2107
+ let component = localComponentDeclaration(componentSource, name)
2108
+ let imported = false
2109
+ if (!component) {
2110
+ const binding = clientImportBindings(componentSource, componentSource.fileName, sourceFiles).get(name)
2111
+ if (!binding || binding.kind === "namespace") fail(node, `Keyed list component ${name} must be declared locally or imported from a relative TypeScript module`)
2112
+ component = resolveComponentExport(binding.target, binding.kind === "default" ? "default" : binding.imported, importedSource, sourceFiles)
2113
+ imported = true
2114
+ }
2115
+ if (trail.includes(component)) {
2116
+ const chain = [...trail, component].map(entry => entry.name?.text || "anonymous").join(" -> ")
2117
+ fail(node, `Keyed list component cycle: ${chain}`)
2118
+ }
2119
+ const specialization = specializeComponentCall(node, component, sourceFile, factory, context, fail, "Keyed list", true)
2120
+ registerRowHooks(node, specialization)
2121
+ specialization.root = expandKeyedComponents(specialization.root, component.getSourceFile(), [...trail, component], specialization)
2122
+ if (imported) synthesizeTree(specialization.root = mergeSpecializedImports(specialization.root, component.getSourceFile(), node))
2123
+ expandedRowSpecializations.set(specialization.root, specialization)
2124
+ if (currentAggregate) {
2125
+ currentAggregate.effects.push(...specialization.effects)
2126
+ currentAggregate.hookDeclarations.push(...specialization.hookDeclarations)
2127
+ currentAggregate.rowStates.push(...specialization.rowStates)
2128
+ currentAggregate.rowRefs.push(...specialization.rowRefs)
2129
+ }
2130
+ replacements.set(node, specialization.root)
2131
+ count++
2132
+ return
2133
+ }
2134
+ ts.forEachChild(node, child => visit(child, currentAggregate))
2135
+ }
2136
+ visit(root)
2137
+ if (!count) return root
2138
+ const expanded = replaceSpecializedCalls(root, replacements, context)
2139
+ ts.setParentRecursive(expanded, false)
2140
+ expanded.parent = root.parent
2141
+ return expanded
2142
+ }
2027
2143
  const renderedLists = new WeakMap()
2144
+ const prepareListCallback = (callback, root, specialization, item) => {
2145
+ const statements = [...specialization.hookDeclarations]
2146
+ if (specialization.effects.length) {
2147
+ usesListEffects = true
2148
+ statements.push(...specialization.effects.map(entry => {
2149
+ const call = factory.updateCallExpression(entry.call, factory.createIdentifier("__kListUseEffect"), entry.call.typeArguments, entry.call.arguments)
2150
+ synthesizeTree(call)
2151
+ const effectSource = entry.source.getSourceFile()
2152
+ listEffectEntries.set(call, { item, source: entry.source, sourceFile: effectSource, imports: clientImportBindings(effectSource, effectSource.fileName, sourceFiles) })
2153
+ return factory.createExpressionStatement(call)
2154
+ }))
2155
+ }
2156
+ if (!statements.length) return callback
2157
+ const prepared = factory.updateArrowFunction(callback, callback.modifiers, callback.typeParameters, callback.parameters, callback.type, callback.equalsGreaterThanToken, factory.createBlock([...statements, factory.createReturnStatement(root)], true))
2158
+ ts.setParentRecursive(prepared, false)
2159
+ prepared.parent = callback.parent
2160
+ return prepared
2161
+ }
2028
2162
  for (const { node, parts: originalParts } of rawRenderedLists) {
2029
2163
  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
2164
+ const specialization = componentSpecializations.get(originalParts.root) ?? { root: originalParts.root, calculations: [], effects: [], hookDeclarations: [], rowStates: [], rowRefs: [] }
2165
+ const componentSource = specialization.componentSource ?? sourceFile
2166
+ specialization.root = expandKeyedComponents(specialization.root, componentSource, specialization.component ? [specialization.component] : [], specialization)
2167
+ if (specialization.imported) synthesizeTree(specialization.root = mergeSpecializedImports(specialization.root, componentSource, originalParts.root))
2168
+ if (specialization.root !== originalParts.root) componentSpecializations.set(originalParts.root, specialization)
2169
+ const root = specialization.root
2032
2170
  let callback = root === originalParts.root ? originalParts.callback : factory.updateArrowFunction(
2033
2171
  originalParts.callback,
2034
2172
  originalParts.callback.modifiers,
@@ -2038,40 +2176,18 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
2038
2176
  originalParts.callback.equalsGreaterThanToken,
2039
2177
  root
2040
2178
  )
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
2179
  if (callback !== originalParts.callback) {
2051
2180
  ts.setParentRecursive(callback, false)
2052
2181
  callback.parent = originalParts.callback.parent
2053
2182
  }
2183
+ callback = prepareListCallback(callback, root, specialization, originalParts.item)
2054
2184
  const parts = { ...originalParts, root, callback }
2055
- for (const calculation of specialization?.calculations ?? []) {
2185
+ for (const calculation of specialization.calculations) {
2056
2186
  ts.setParentRecursive(calculation, false)
2057
2187
  calculation.parent = callback
2058
2188
  validateListExpression(calculation, parts.item, originalParts.root, fail)
2059
2189
  }
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
- }
2190
+ validateKeyedList(parts, sourceFile, listValues, listEventItems, listConditions, settersForNode(originalParts.root, settersByFunction), specialization.rowStates, nestedLists, componentSpecializations, expandedRowSpecializations, nestedRowSpecializations, factory, prepareListCallback)
2075
2191
  renderedLists.set(node, parts)
2076
2192
  }
2077
2193
 
@@ -2105,6 +2221,12 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
2105
2221
  fail(node, "Stylesheets must be placed under src/ or declared in kudzu.config styles so Kudzu can emit them in <head>")
2106
2222
  }
2107
2223
 
2224
+ if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text === "react") {
2225
+ if (!node.importClause) fail(node, "Side-effect React imports are not supported because Kudzu does not load the React runtime")
2226
+ if (node.importClause.isTypeOnly) return node
2227
+ return factory.updateImportDeclaration(node, node.modifiers, node.importClause, factory.createStringLiteral("@kudzujs/core"), node.attributes)
2228
+ }
2229
+
2108
2230
  if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text.startsWith(".")) {
2109
2231
  const target = resolveSourceImport(file, node.moduleSpecifier.text, sourceFiles)
2110
2232
  return factory.updateImportDeclaration(node, node.modifiers, node.importClause, factory.createStringLiteral(relativeModulePath(compiledPath(file), compiledPath(target))), node.attributes)
@@ -2231,9 +2353,11 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
2231
2353
  usesList = true
2232
2354
  return factory.updateJsxExpression(node, factory.createCallExpression(factory.createIdentifier("__kList"), undefined, [
2233
2355
  listParts.state,
2234
- factory.createStringLiteral(listParts.keyField),
2356
+ listParts.keyField === null ? factory.createNull() : factory.createStringLiteral(listParts.keyField),
2235
2357
  ts.visitNode(listParts.callback, visitor),
2236
- ...(nestedParts ? [factory.createStringLiteral(listParts.ownerField)] : [])
2358
+ factory.createStringLiteral(listParts.ownerField ?? ""),
2359
+ jsonExpression(listParts.selector ?? [], factory),
2360
+ listParts.indexed ? factory.createTrue() : factory.createFalse()
2237
2361
  ]))
2238
2362
  }
2239
2363
  const conditional = conditionalParts(node.expression)
@@ -2297,11 +2421,13 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
2297
2421
  behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("listExpression"), factory.createIdentifier("__kListExpression")))
2298
2422
  behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("listField"), factory.createIdentifier("__kListField")))
2299
2423
  behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("listItem"), factory.createIdentifier("__kListItem")))
2424
+ behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("listIndex"), factory.createIdentifier("__kListIndex")))
2300
2425
  behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("listConditional"), factory.createIdentifier("__kListConditional")))
2301
2426
  }
2302
2427
  if (usesListItem && !usesList) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("listItem"), factory.createIdentifier("__kListItem")))
2303
2428
  if (usesListEffects) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("useEffect"), factory.createIdentifier("__kListUseEffect")))
2304
2429
  if (usesRowState) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("useState"), factory.createIdentifier("__kRowUseState")))
2430
+ if (usesRowRef) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("useRef"), factory.createIdentifier("__kRowUseRef")))
2305
2431
  if (usesBinding || usesConditional) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("bindingValue"), factory.createIdentifier("__kBindingValue")))
2306
2432
  const behaviorImport = factory.createImportDeclaration(
2307
2433
  undefined,
@@ -2418,41 +2544,159 @@ function containsRenderControl(root, knownLocals) {
2418
2544
  return found
2419
2545
  }
2420
2546
 
2421
- function keyedListParts(expression, setters) {
2547
+ function keyedListParts(expression, setters, declarations, fail, aliases = new Set()) {
2422
2548
  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
- }
2549
+ const directFrom = isArrayFromCall(value) && value.arguments.length === 2 && containsJsx(value.arguments[1])
2550
+ if (!directFrom && (!ts.isCallExpression(value) || value.arguments.length !== 1 || !ts.isPropertyAccessExpression(value.expression) || value.expression.name.text !== "map")) return undefined
2551
+ const collection = renderedCollectionSource(directFrom ? value.arguments[0] : value.expression.expression, setters, declarations, fail, aliases)
2552
+ if (!collection?.state) return undefined
2553
+ if (directFrom) collection.selector.push(["from", undefined])
2554
+ const callback = directFrom ? value.arguments[1] : value.arguments[0]
2555
+ const parameters = collectionParameters(callback, "Keyed list map", fail)
2430
2556
  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")
2557
+ if (!ts.isJsxElement(root) && !ts.isJsxSelfClosingElement(root)) fail(callback.body, "Keyed list map callback must return one JSX element")
2432
2558
  const attributes = ts.isJsxElement(root) ? root.openingElement.attributes : root.attributes
2433
2559
  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 }
2560
+ const keyExpression = key && ts.isJsxAttribute(key) && key.initializer && ts.isJsxExpression(key.initializer) && key.initializer.expression
2561
+ const field = keyExpression && directProperty(keyExpression, parameters.item)
2562
+ const positional = Boolean(keyExpression && parameters.index && ts.isIdentifier(unwrapExpression(keyExpression)) && unwrapExpression(keyExpression).text === parameters.index)
2563
+ if (!field && !positional) fail(key ?? root, `Keyed list root must have key={${parameters.item}.<field>} or key={${parameters.index ?? "index"}}`)
2564
+ return { ...collection, callback, root, item: parameters.item, index: parameters.index, indexed: Boolean(parameters.index), keyField: positional ? null : field }
2437
2565
  }
2438
2566
 
2439
- function nestedKeyedListParts(expression, parentItem) {
2567
+ function nestedKeyedListParts(expression, parentItem, fail) {
2440
2568
  const value = unwrapExpression(expression)
2441
2569
  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
2570
+ const collection = renderedCollectionSource(value.expression.expression, new Map(), undefined, fail, new Set())
2571
+ if (!collection?.ownerField || collection.parentItem !== parentItem) return undefined
2444
2572
  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
- }
2573
+ const parameters = collectionParameters(callback, "Nested keyed list map", fail)
2448
2574
  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")
2575
+ if (!ts.isJsxElement(root) && !ts.isJsxSelfClosingElement(root)) fail(callback.body, "Nested keyed list map callback must return one JSX element")
2450
2576
  const attributes = ts.isJsxElement(root) ? root.openingElement.attributes : root.attributes
2451
2577
  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 }
2578
+ const keyExpression = key && ts.isJsxAttribute(key) && key.initializer && ts.isJsxExpression(key.initializer) && key.initializer.expression
2579
+ const keyField = keyExpression && directProperty(keyExpression, parameters.item)
2580
+ const positional = Boolean(keyExpression && parameters.index && ts.isIdentifier(unwrapExpression(keyExpression)) && unwrapExpression(keyExpression).text === parameters.index)
2581
+ if (!keyField && !positional) fail(key ?? root, `Nested keyed list root must have key={${parameters.item}.<field>} or key={${parameters.index ?? "index"}}`)
2582
+ return { ...collection, callback, root, item: parameters.item, index: parameters.index, indexed: Boolean(parameters.index), keyField: positional ? null : keyField }
2583
+ }
2584
+
2585
+ function renderedCollectionSource(expression, setters, declarations, fail, aliases) {
2586
+ const value = unwrapExpression(expression)
2587
+ if (ts.isIdentifier(value)) {
2588
+ if ([...setters.values()].includes(value.text)) return { state: value, selector: [] }
2589
+ const entries = declarations?.get(value.text)
2590
+ if (!entries) return undefined
2591
+ 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`)
2592
+ if (identifierReferenceCount(nearestFunction(entries[0].node).body, value.text) !== 1) fail(value, `Rendered collection alias "${value.text}" may only be rendered once`)
2593
+ aliases.add(value.text)
2594
+ const source = renderedCollectionSource(entries[0].initializer, setters, declarations, fail, aliases)
2595
+ aliases.delete(value.text)
2596
+ return source && { ...source, aliasDeclarations: [...(source.aliasDeclarations ?? []), entries[0].node] }
2597
+ }
2598
+ if (ts.isPropertyAccessExpression(value) && ts.isIdentifier(value.expression)) return { state: undefined, ownerField: value.name.text, selector: [], parentItem: value.expression.text }
2599
+ if (ts.isCallExpression(value) && ts.isPropertyAccessExpression(value.expression)) {
2600
+ const method = value.expression.name.text
2601
+ if (method === "filter") {
2602
+ if (value.arguments.length !== 1) fail(value, "Rendered collection filter() requires one inline predicate")
2603
+ const source = renderedCollectionSource(value.expression.expression, setters, declarations, fail, aliases)
2604
+ if (!source) return undefined
2605
+ const parameters = collectionParameters(value.arguments[0], "Rendered collection filter()", fail)
2606
+ return { ...source, selector: [...source.selector, ["filter", collectionExpression(unwrapExpression(value.arguments[0].body), parameters, fail)]] }
2607
+ }
2608
+ if (method === "flatMap") {
2609
+ if (value.arguments.length !== 1) fail(value, "Rendered collection flatMap() requires one inline projector")
2610
+ const source = renderedCollectionSource(value.expression.expression, setters, declarations, fail, aliases)
2611
+ if (!source) return undefined
2612
+ const parameters = collectionParameters(value.arguments[0], "Rendered collection flatMap()", fail)
2613
+ const field = directProperty(value.arguments[0].body, parameters.item)
2614
+ if (!field) fail(value.arguments[0].body, `Rendered collection flatMap() projector must be ${parameters.item}.<field>`)
2615
+ if (["__proto__", "constructor", "prototype"].includes(field)) fail(value.arguments[0].body, `Rendered collection property "${field}" is not supported`)
2616
+ return { ...source, selector: [...source.selector, ["flatMap", field]] }
2617
+ }
2618
+ }
2619
+ if (isArrayFromCall(value)) {
2620
+ if (value.arguments.length < 1 || value.arguments.length > 2) fail(value, "Rendered Array.from() requires an anchor and optional inline mapper")
2621
+ const source = renderedCollectionSource(value.arguments[0], setters, declarations, fail, aliases)
2622
+ if (!source) return undefined
2623
+ let mapper
2624
+ if (value.arguments[1]) {
2625
+ const parameters = collectionParameters(value.arguments[1], "Rendered Array.from() mapper", fail)
2626
+ mapper = collectionExpression(unwrapExpression(value.arguments[1].body), parameters, fail)
2627
+ }
2628
+ return { ...source, selector: [...source.selector, ["from", mapper]] }
2629
+ }
2630
+ }
2631
+
2632
+ function isArrayFromCall(value) {
2633
+ return ts.isCallExpression(value) && ts.isPropertyAccessExpression(value.expression) && ts.isIdentifier(value.expression.expression) && value.expression.expression.text === "Array" && value.expression.name.text === "from"
2634
+ }
2635
+
2636
+ function collectionParameters(callback, label, fail) {
2637
+ 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`)
2638
+ return { item: callback.parameters[0].name.text, index: callback.parameters[1]?.name.text }
2639
+ }
2640
+
2641
+ function collectionExpression(expression, parameters, fail) {
2642
+ const encode = node => {
2643
+ node = unwrapExpression(node)
2644
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) || ts.isNumericLiteral(node)) return ["value", ts.isNumericLiteral(node) ? Number(node.text) : node.text]
2645
+ if (node.kind === ts.SyntaxKind.TrueKeyword) return ["value", true]
2646
+ if (node.kind === ts.SyntaxKind.FalseKeyword) return ["value", false]
2647
+ if (node.kind === ts.SyntaxKind.NullKeyword) return ["value", null]
2648
+ if (ts.isIdentifier(node)) {
2649
+ if (node.text === parameters.item) return ["item"]
2650
+ if (node.text === parameters.index) return ["index"]
2651
+ if (node.text === "undefined") return ["undefined"]
2652
+ fail(node, `Rendered collection expression identifier "${node.text}" is not allowed`)
2653
+ }
2654
+ if (ts.isPropertyAccessExpression(node)) {
2655
+ if (["__proto__", "constructor", "prototype"].includes(node.name.text)) fail(node, `Rendered collection property "${node.name.text}" is not supported`)
2656
+ return ["get", encode(node.expression), node.name.text, Boolean(node.questionDotToken)]
2657
+ }
2658
+ if (ts.isElementAccessExpression(node)) {
2659
+ const key = node.argumentExpression
2660
+ if (!ts.isStringLiteral(key) && !ts.isNumericLiteral(key)) fail(node, "Rendered collection computed properties require a direct string or numeric literal key")
2661
+ if (ts.isStringLiteral(key) && ["__proto__", "constructor", "prototype"].includes(key.text)) fail(node, `Rendered collection property "${key.text}" is not supported`)
2662
+ return ["get", encode(node.expression), ts.isNumericLiteral(key) ? Number(key.text) : key.text, Boolean(node.questionDotToken)]
2663
+ }
2664
+ if (ts.isPrefixUnaryExpression(node)) {
2665
+ const operator = node.operator === ts.SyntaxKind.ExclamationToken ? "!" : node.operator === ts.SyntaxKind.PlusToken ? "+" : node.operator === ts.SyntaxKind.MinusToken ? "-" : undefined
2666
+ if (!operator) fail(node, "Rendered collection expression uses an unsupported unary operator")
2667
+ return ["unary", operator, encode(node.operand)]
2668
+ }
2669
+ if (ts.isTypeOfExpression(node)) return ["unary", "typeof", encode(node.expression)]
2670
+ if (ts.isBinaryExpression(node)) {
2671
+ const operator = node.operatorToken.getText()
2672
+ if (!new Set(["&&", "||", "??", "===", "!==", "==", "!=", "<", "<=", ">", ">=", "+", "-", "*", "/", "%"]).has(operator)) fail(node, `Rendered collection expression operator "${operator}" is not supported`)
2673
+ return ["binary", operator, encode(node.left), encode(node.right)]
2674
+ }
2675
+ if (ts.isConditionalExpression(node)) return ["conditional", encode(node.condition), encode(node.whenTrue), encode(node.whenFalse)]
2676
+ if (ts.isArrayLiteralExpression(node) && !node.elements.some(ts.isSpreadElement)) return ["array", ...node.elements.map(encode)]
2677
+ if (ts.isObjectLiteralExpression(node)) return ["object", ...node.properties.map(property => {
2678
+ 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")
2679
+ return [property.name.text, encode(property.initializer)]
2680
+ })]
2681
+ if (ts.isTemplateExpression(node)) return ["template", [node.head.text, ...node.templateSpans.map(span => span.literal.text)], node.templateSpans.map(span => encode(span.expression))]
2682
+ if (ts.isCallExpression(node)) {
2683
+ if (ts.isIdentifier(node.expression) && ["Boolean", "Number", "String"].includes(node.expression.text)) return ["global", node.expression.text, ...node.arguments.map(encode)]
2684
+ if (ts.isPropertyAccessExpression(node.expression)) {
2685
+ const method = node.expression.name.text
2686
+ if (ts.isIdentifier(node.expression.expression) && node.expression.expression.text === "Math" && pureMathMethods.has(method)) return ["math", method, ...node.arguments.map(encode)]
2687
+ if (pureListMethods.has(method)) return ["call", encode(node.expression.expression), method, ...node.arguments.map(encode)]
2688
+ if (mutatingListMethods.has(method)) fail(node, `Rendered collection expressions cannot call mutating method "${method}"`)
2689
+ }
2690
+ fail(node, "Rendered collection expressions cannot call arbitrary functions")
2691
+ }
2692
+ 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")
2693
+ fail(node, "Rendered collection expression is not supported")
2694
+ }
2695
+ return encode(expression)
2696
+ }
2697
+
2698
+ function jsonExpression(value, factory) {
2699
+ return factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("JSON"), "parse"), undefined, [factory.createStringLiteral(JSON.stringify(value))])
2456
2700
  }
2457
2701
 
2458
2702
  function isStateBackedListComponentCall(call, component, setters) {
@@ -2539,39 +2783,36 @@ function insideJsxEventHandler(node, root) {
2539
2783
  return false
2540
2784
  }
2541
2785
 
2542
- function validateKeyedList(parts, sourceFile, listValues, listEventItems, listConditions, setters, rowState, nestedLists, componentSpecializations, factory) {
2786
+ function validateKeyedList(parts, sourceFile, listValues, listEventItems, listConditions, setters, rowStates, nestedLists, componentSpecializations, expandedRowSpecializations, nestedRowSpecializations, factory, prepareListCallback) {
2543
2787
  const fail = (node, message) => {
2544
2788
  throw sourceNodeError(node, sourceFile, message)
2545
2789
  }
2546
2790
  const root = parts.root
2547
2791
  const item = parts.item
2792
+ const nestedDiagnostic = "Nested keyed list collections must be a direct property of the parent item"
2548
2793
  const validateElement = node => {
2549
2794
  const tag = ts.isJsxElement(node) ? node.openingElement.tagName : node.tagName
2550
2795
  if (!ts.isIdentifier(tag) || tag.text[0] !== tag.text[0].toLowerCase()) fail(node, "Keyed list items must use intrinsic JSX elements")
2551
2796
  }
2552
- let conditionDepth = 0
2553
- let nestedList
2554
2797
  const visit = node => {
2555
2798
  if (ts.isJsxFragment(node)) fail(node, "Fragments are not supported in keyed lists")
2556
2799
  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")
2800
+ if (node !== root && ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "map" && containsJsx(node)) fail(node, nestedDiagnostic)
2558
2801
  if (ts.isJsxSpreadAttribute(node) && referencesIdentifier(node.expression, item)) fail(node, "Keyed list item spreads are not supported")
2559
2802
  if (ts.isJsxAttribute(node) && /^on[A-Z]/.test(node.name.text)) {
2560
- listEventItems.set(node, item)
2803
+ listEventItems.set(node, { item, index: parts.index })
2561
2804
  return
2562
2805
  }
2563
2806
  if (ts.isJsxExpression(node) && node.expression) {
2564
2807
  const expression = unwrapExpression(node.expression)
2565
2808
  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")
2809
+ const nested = nestedKeyedListParts(expression, item, fail)
2810
+ if (!nested) fail(expression, nestedDiagnostic)
2811
+ if (["__proto__", "constructor", "prototype"].includes(nested.ownerField)) fail(expression, `Nested keyed list owner property "${nested.ownerField}" is not supported`)
2570
2812
  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)
2813
+ const specialization = componentSpecializations.get(nested.root) ?? expandedRowSpecializations.get(nested.root) ?? nestedRowSpecializations.get(`${expression.pos}:${expression.end}`)
2573
2814
  const root = specialization?.root ?? nested.root
2574
- const callback = root === nested.root ? nested.callback : factory.updateArrowFunction(
2815
+ let callback = root === nested.root ? nested.callback : factory.updateArrowFunction(
2575
2816
  nested.callback,
2576
2817
  nested.callback.modifiers,
2577
2818
  nested.callback.typeParameters,
@@ -2584,6 +2825,7 @@ function validateKeyedList(parts, sourceFile, listValues, listEventItems, listCo
2584
2825
  ts.setParentRecursive(callback, false)
2585
2826
  callback.parent = nested.callback.parent
2586
2827
  }
2828
+ callback = prepareListCallback(callback, root, specialization ?? { hookDeclarations: [], effects: [] }, nested.item)
2587
2829
  const nestedParts = { ...nested, root, callback, state: parts.state, nested: true }
2588
2830
  for (const calculation of specialization?.calculations ?? []) {
2589
2831
  ts.setParentRecursive(calculation, false)
@@ -2591,26 +2833,21 @@ function validateKeyedList(parts, sourceFile, listValues, listEventItems, listCo
2591
2833
  validateListExpression(calculation, nested.item, nested.root, fail)
2592
2834
  }
2593
2835
  nestedLists.set(expression, nestedParts)
2594
- validateKeyedList(nestedParts, sourceFile, listValues, listEventItems, listConditions, setters, undefined, nestedLists, componentSpecializations, factory)
2836
+ validateKeyedList(nestedParts, sourceFile, listValues, listEventItems, listConditions, setters, specialization?.rowStates ?? [], nestedLists, componentSpecializations, expandedRowSpecializations, nestedRowSpecializations, factory, prepareListCallback)
2595
2837
  return
2596
2838
  }
2597
2839
  const condition = conditionalParts(expression)
2598
2840
  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++
2841
+ if (rowStates.some(rowState => referencedStateNames(condition.condition, setters).has(rowState.state))) {
2602
2842
  visit(condition.truthy)
2603
2843
  visit(condition.falsy)
2604
- conditionDepth--
2605
2844
  return
2606
2845
  }
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++
2846
+ if (!referencesIdentifier(condition.condition, item) && !(parts.index && referencesIdentifier(condition.condition, parts.index))) fail(node, "Keyed list item conditions must read the item or index")
2847
+ validateListExpression(condition.condition, item, node, fail, parts.index)
2848
+ listConditions.set(node.expression, { ...condition, item, index: parts.index })
2611
2849
  visit(condition.truthy)
2612
2850
  visit(condition.falsy)
2613
- conditionDepth--
2614
2851
  return
2615
2852
  }
2616
2853
  const field = directProperty(expression, item)
@@ -2622,10 +2859,10 @@ function validateKeyedList(parts, sourceFile, listValues, listEventItems, listCo
2622
2859
  listValues.set(node.expression, { field })
2623
2860
  return
2624
2861
  }
2625
- if (referencesIdentifier(expression, item)) {
2626
- validateListExpression(expression, item, node, fail)
2862
+ if (referencesIdentifier(expression, item) || parts.index && referencesIdentifier(expression, parts.index)) {
2863
+ validateListExpression(expression, item, node, fail, parts.index)
2627
2864
  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 })
2865
+ listValues.set(node.expression, { item, index: parts.index })
2629
2866
  return
2630
2867
  }
2631
2868
  }
@@ -2634,7 +2871,7 @@ function validateKeyedList(parts, sourceFile, listValues, listEventItems, listCo
2634
2871
  visit(root)
2635
2872
  }
2636
2873
 
2637
- function specializeComponentCall(call, component, sourceFile, factory, context, fail, label = "Keyed list") {
2874
+ function specializeComponentCall(call, component, sourceFile, factory, context, fail, label = "Keyed list", allowComponentRoot = false) {
2638
2875
  if (component.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword) || component.asteriskToken) fail(component, `${label} components must be synchronous`)
2639
2876
  if (component.parameters.length !== 1 || !ts.isObjectBindingPattern(component.parameters[0].name)) fail(component, `${label} components must use one destructured props parameter`)
2640
2877
  if (ts.isJsxElement(call) && call.children.some(child => !ts.isJsxText(child) || child.text.trim())) fail(call, `${label} component children are not supported`)
@@ -2669,8 +2906,9 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
2669
2906
  let returned
2670
2907
  const calculations = []
2671
2908
  const effectCalls = []
2672
- const stateDeclarations = []
2673
- let rowState
2909
+ const hookDeclarations = []
2910
+ const rowStates = []
2911
+ const rowRefs = []
2674
2912
  if (!ts.isBlock(component.body)) {
2675
2913
  returned = component.body
2676
2914
  } else {
@@ -2685,11 +2923,9 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
2685
2923
  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
2924
  const declaration = statement.declarationList.declarations[0]
2687
2925
  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)
2926
+ 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")
2927
+ 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")
2928
+ const suffix = `${Math.max(0, call.pos)}_${rowStates.length}`
2693
2929
  const state = `__kRowState${suffix}`
2694
2930
  const setter = `__kRowSetter${suffix}`
2695
2931
  substitutions.set(declaration.name.elements[0].name.text, factory.createIdentifier(state))
@@ -2698,9 +2934,21 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
2698
2934
  factory.createBindingElement(undefined, undefined, factory.createIdentifier(state)),
2699
2935
  factory.createBindingElement(undefined, undefined, factory.createIdentifier(setter))
2700
2936
  ])
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 }
2937
+ const initialValue = cloneAst(declaration.initializer.arguments[0], factory, context)
2938
+ synthesizeTree(initialValue)
2939
+ const initializer = factory.createCallExpression(factory.createIdentifier("__kRowUseState"), undefined, [initialValue])
2940
+ hookDeclarations.push(factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(binding, undefined, undefined, initializer)], ts.NodeFlags.Const)))
2941
+ rowStates.push({ state, setter })
2942
+ continue
2943
+ }
2944
+ if (declaration.initializer && ts.isCallExpression(declaration.initializer) && ts.isIdentifier(declaration.initializer.expression) && declaration.initializer.expression.text === "useRef") {
2945
+ 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")
2946
+ if (!ts.isIdentifier(declaration.name)) throw sourceNodeError(declaration.name, component.getSourceFile(), "Keyed row useRef() must be assigned to one identifier")
2947
+ const name = `__kRowRef${Math.max(0, call.pos)}_${rowRefs.length}`
2948
+ substitutions.set(declaration.name.text, factory.createIdentifier(name))
2949
+ const initializer = factory.createCallExpression(factory.createIdentifier("__kRowUseRef"), declaration.initializer.typeArguments?.map(type => cloneAst(type, factory, context)), [factory.createNull()])
2950
+ hookDeclarations.push(factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(factory.createIdentifier(name), undefined, undefined, initializer)], ts.NodeFlags.Const)))
2951
+ rowRefs.push({ name })
2704
2952
  continue
2705
2953
  }
2706
2954
  if (!ts.isIdentifier(declaration.name) || !declaration.initializer) fail(declaration, `${label} component locals must be initialized identifiers`)
@@ -2710,19 +2958,19 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
2710
2958
  }
2711
2959
  returned = last.expression
2712
2960
  }
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)
2961
+ let unsupportedHook
2962
+ const findUnsupportedHook = node => {
2963
+ if (unsupportedHook) return
2964
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && ["useState", "useRef"].includes(node.expression.text)) unsupportedHook = node
2965
+ ts.forEachChild(node, findUnsupportedHook)
2718
2966
  }
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`)
2967
+ findUnsupportedHook(returned)
2968
+ for (const calculation of calculations) findUnsupportedHook(calculation.expression)
2969
+ if (unsupportedHook) throw sourceNodeError(unsupportedHook, component.getSourceFile(), `Keyed row ${unsupportedHook.expression.text}() must be one top-level const declaration`)
2722
2970
  let root = unwrapExpression(substituteClone(returned, substitutions, factory, context))
2723
2971
  if (!ts.isJsxElement(root) && !ts.isJsxSelfClosingElement(root)) fail(returned, `${label} component must return one JSX element`)
2724
2972
  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`)
2973
+ if (!ts.isIdentifier(tag) || !allowComponentRoot && tag.text[0] !== tag.text[0].toLowerCase()) fail(returned, `${label} component must directly return an intrinsic JSX element`)
2726
2974
  const rootAttributes = ts.isJsxElement(root) ? root.openingElement.attributes : root.attributes
2727
2975
  if (rootAttributes.properties.some(attribute => ts.isJsxAttribute(attribute) && attribute.name.text === "key")) fail(root, `${label} component intrinsic root cannot declare key`)
2728
2976
  if (key) root = addJsxAttribute(root, cloneAst(key, factory, context), factory)
@@ -2735,11 +2983,20 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
2735
2983
  .filter(calculation => label !== "Reducer-dispatch" || !isFunctionLike(calculation.expression) || !isEventOnlyComponentLocal(returned, calculation.name))
2736
2984
  .map(calculation => calculation.expression),
2737
2985
  effects,
2738
- stateDeclarations,
2739
- rowState
2986
+ hookDeclarations,
2987
+ rowStates,
2988
+ rowRefs
2740
2989
  }
2741
2990
  }
2742
2991
 
2992
+ function isSerializableStateLiteral(node) {
2993
+ const value = unwrapExpression(node)
2994
+ if (isPrimitiveDefaultLiteral(value)) return true
2995
+ if (ts.isArrayLiteralExpression(value)) return value.elements.every(element => !ts.isSpreadElement(element) && !ts.isOmittedExpression(element) && isSerializableStateLiteral(element))
2996
+ if (!ts.isObjectLiteralExpression(value)) return false
2997
+ return value.properties.every(property => ts.isPropertyAssignment(property) && !ts.isComputedPropertyName(property.name) && property.name.text !== "__proto__" && isSerializableStateLiteral(property.initializer))
2998
+ }
2999
+
2743
3000
  function isPrimitiveDefaultLiteral(node) {
2744
3001
  return ts.isStringLiteral(node) || ts.isNumericLiteral(node) ||
2745
3002
  (ts.isPrefixUnaryExpression(node) && (node.operator === ts.SyntaxKind.PlusToken || node.operator === ts.SyntaxKind.MinusToken) && ts.isNumericLiteral(node.operand)) ||
@@ -2840,7 +3097,7 @@ function isStylesheetLink(node) {
2840
3097
  }
2841
3098
 
2842
3099
  function isContextProviderValue(node, contexts) {
2843
- if (node.name.getText() !== "value") return false
3100
+ if (node.name.text !== "value") return false
2844
3101
  const element = node.parent?.parent
2845
3102
  const tag = ts.isJsxOpeningElement(element) || ts.isJsxSelfClosingElement(element) ? element.tagName : undefined
2846
3103
  return ts.isPropertyAccessExpression(tag) && tag.name.text === "Provider" && ts.isIdentifier(tag.expression) && contexts.has(tag.expression.text)
@@ -2888,7 +3145,7 @@ const assignmentOperators = new Set([
2888
3145
  ts.SyntaxKind.QuestionQuestionEqualsToken
2889
3146
  ])
2890
3147
 
2891
- function validateListExpression(expression, item, source, fail) {
3148
+ function validateListExpression(expression, item, source, fail, index) {
2892
3149
  const visit = node => {
2893
3150
  if (ts.isTypeNode(node)) return
2894
3151
  if (ts.isElementAccessExpression(node) && referencesIdentifier(node.expression, item)) {
@@ -2919,7 +3176,7 @@ function validateListExpression(expression, item, source, fail) {
2919
3176
  fail(source, "Derived keyed list item expressions cannot call arbitrary functions")
2920
3177
  }
2921
3178
  }
2922
- if (ts.isIdentifier(node) && isReferenceIdentifier(node) && node.text !== item && !pureListGlobals.has(node.text)) {
3179
+ if (ts.isIdentifier(node) && isReferenceIdentifier(node) && node.text !== item && node.text !== index && !pureListGlobals.has(node.text)) {
2923
3180
  fail(source, `Derived keyed list item expression identifier "${node.text}" is not allowed`)
2924
3181
  }
2925
3182
  ts.forEachChild(node, visit)
@@ -2937,15 +3194,15 @@ function containsJsx(root) {
2937
3194
  return found
2938
3195
  }
2939
3196
 
2940
- function compileListExpression(read, expression, item, factory, listExpressions, handlerUrl) {
3197
+ function compileListExpression(read, expression, item, factory, listExpressions, handlerUrl, index) {
2941
3198
  const exportName = `listExpression${listExpressions.length}`
2942
- listExpressions.push({ exportName, expression, item })
3199
+ listExpressions.push({ exportName, expression, item, index })
2943
3200
  return factory.createCallExpression(factory.createIdentifier("__kListExpression"), undefined, [read, factory.createStringLiteral(handlerUrl), factory.createStringLiteral(exportName)])
2944
3201
  }
2945
3202
 
2946
3203
  function compileListConditional(entry, factory, listExpressions, handlerUrl) {
2947
3204
  const exportName = `listExpression${listExpressions.length}`
2948
- listExpressions.push({ exportName, expression: entry.condition, item: entry.item })
3205
+ listExpressions.push({ exportName, expression: entry.condition, item: entry.item, index: entry.index })
2949
3206
  const read = factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), entry.condition)
2950
3207
  const thunk = branch => factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), branch)
2951
3208
  return factory.createCallExpression(factory.createIdentifier("__kListConditional"), undefined, [
@@ -2957,7 +3214,7 @@ function compileListValue(expression, entry, factory, listExpressions, handlerUr
2957
3214
  const read = factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), expression)
2958
3215
  return entry.field
2959
3216
  ? factory.createCallExpression(factory.createIdentifier("__kListField"), undefined, [read, factory.createStringLiteral(entry.field)])
2960
- : compileListExpression(read, expression, entry.item, factory, listExpressions, handlerUrl)
3217
+ : compileListExpression(read, expression, entry.item, factory, listExpressions, handlerUrl, entry.index)
2961
3218
  }
2962
3219
 
2963
3220
  function directProperty(expression, objectName) {
@@ -3126,7 +3383,11 @@ function compileNativeCallback(expression, setters, reducers, factory, entries,
3126
3383
  ]))),
3127
3384
  scope: factory.createArrayLiteralExpression([...captures].map(name => factory.createArrayLiteralExpression([
3128
3385
  factory.createStringLiteral(name),
3129
- name === listItem ? factory.createCallExpression(factory.createIdentifier("__kListItem"), undefined, []) : value(name)
3386
+ name === (typeof listItem === "string" ? listItem : listItem?.item)
3387
+ ? factory.createCallExpression(factory.createIdentifier("__kListItem"), undefined, [])
3388
+ : name === listItem?.index
3389
+ ? factory.createCallExpression(factory.createIdentifier("__kListIndex"), undefined, [])
3390
+ : value(name)
3130
3391
  ])))
3131
3392
  }
3132
3393
  }
@@ -3879,13 +4140,13 @@ function printReactiveBinding({ exportName, expression, captures, states }) {
3879
4140
  }
3880
4141
  }
3881
4142
 
3882
- function printListExpression({ exportName, expression, item }) {
4143
+ function printListExpression({ exportName, expression, item, index }) {
3883
4144
  const declaration = ts.factory.createFunctionDeclaration(
3884
4145
  [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)],
3885
4146
  undefined,
3886
4147
  exportName,
3887
4148
  undefined,
3888
- [ts.factory.createParameterDeclaration(undefined, undefined, item)],
4149
+ [ts.factory.createParameterDeclaration(undefined, undefined, item), ts.factory.createParameterDeclaration(undefined, undefined, index ?? "__kIndex")],
3889
4150
  undefined,
3890
4151
  ts.factory.createBlock([ts.factory.createReturnStatement(expression)], true)
3891
4152
  )