@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.
- package/GOAL_A.md +1 -1
- package/README.md +72 -38
- package/RELEASES.md +44 -0
- package/framework/README.md +9 -3
- package/framework/binding-runtime.js +4 -2
- package/framework/build.mjs +417 -156
- package/framework/collection-selector.js +63 -0
- package/framework/core.d.ts +6 -1
- package/framework/core.mjs +73 -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 +2 -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
|
|
@@ -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) &&
|
|
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
|
|
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
|
|
1810
|
-
if (!specialization.
|
|
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) &&
|
|
1891
|
+
if (isFunctionLike(current) && settersByFunction.has(current)) {
|
|
1814
1892
|
owner = current
|
|
1815
1893
|
break
|
|
1816
1894
|
}
|
|
1817
1895
|
}
|
|
1818
|
-
|
|
1819
|
-
setters.
|
|
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
|
-
|
|
1822
|
-
usesRowState
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
|
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
|
-
})
|
|
2061
|
+
}))
|
|
1996
2062
|
const keyedComponentCalls = new Set(rawRenderedLists.map(({ parts }) => parts.root))
|
|
1997
|
-
for (const call of
|
|
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
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
if (
|
|
2428
|
-
|
|
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))
|
|
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
|
|
2435
|
-
|
|
2436
|
-
|
|
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 (!
|
|
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
|
-
|
|
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))
|
|
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
|
|
2453
|
-
const keyField =
|
|
2454
|
-
|
|
2455
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
2568
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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 (
|
|
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
|
|
2673
|
-
|
|
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 (
|
|
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)
|
|
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
|
|
2702
|
-
|
|
2703
|
-
|
|
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
|
|
2714
|
-
const
|
|
2715
|
-
if (
|
|
2716
|
-
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text
|
|
2717
|
-
ts.forEachChild(node,
|
|
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
|
-
|
|
2720
|
-
for (const calculation of calculations)
|
|
2721
|
-
if (
|
|
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
|
-
|
|
2739
|
-
|
|
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.
|
|
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
|
|
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
|
)
|