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