@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.
- package/GOAL_A.md +1 -1
- package/README.md +53 -37
- package/framework/README.md +7 -3
- package/framework/binding-runtime.js +4 -2
- package/framework/build.mjs +397 -155
- package/framework/collection-selector.js +63 -0
- package/framework/core.d.ts +2 -1
- package/framework/core.mjs +70 -38
- package/framework/list-runtime.js +321 -95
- package/framework/native-runtime.js +13 -2
- package/framework/shared-runtime.js +18 -5
- package/package.json +1 -1
package/framework/build.mjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
289
|
-
|
|
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
|
|
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
|
|
1810
|
-
if (!specialization.
|
|
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) &&
|
|
1878
|
+
if (isFunctionLike(current) && settersByFunction.has(current)) {
|
|
1814
1879
|
owner = current
|
|
1815
1880
|
break
|
|
1816
1881
|
}
|
|
1817
1882
|
}
|
|
1818
|
-
|
|
1819
|
-
setters.
|
|
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
|
-
|
|
1822
|
-
usesRowState
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
|
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
|
-
})
|
|
2048
|
+
}))
|
|
1996
2049
|
const keyedComponentCalls = new Set(rawRenderedLists.map(({ parts }) => parts.root))
|
|
1997
|
-
for (const call of
|
|
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
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
if (
|
|
2428
|
-
|
|
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))
|
|
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
|
|
2435
|
-
|
|
2436
|
-
|
|
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 (!
|
|
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
|
-
|
|
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))
|
|
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
|
|
2453
|
-
const keyField =
|
|
2454
|
-
|
|
2455
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
2568
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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 (
|
|
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
|
|
2673
|
-
|
|
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 (
|
|
2689
|
-
if (
|
|
2690
|
-
|
|
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
|
|
2702
|
-
|
|
2703
|
-
|
|
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
|
|
2714
|
-
const
|
|
2715
|
-
if (
|
|
2716
|
-
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text
|
|
2717
|
-
ts.forEachChild(node,
|
|
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
|
-
|
|
2720
|
-
for (const calculation of calculations)
|
|
2721
|
-
if (
|
|
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
|
-
|
|
2739
|
-
|
|
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.
|
|
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
|
|
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
|
)
|