@kudzujs/core 0.8.18 → 0.8.20
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 +14 -0
- package/PERFORMANCE.md +88 -0
- package/README.md +1 -1
- package/RELEASES.md +65 -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 +2 -2
- package/docs/next-architecture/versioning.md +1 -1
- package/framework/README.md +5 -4
- package/framework/build.mjs +140 -76
- package/framework/compiler/descriptor-session.mjs +100 -21
- package/framework/compiler/handler-codegen.mjs +6 -279
- package/framework/compiler/handler-lowering.mjs +278 -0
- package/framework/compiler/ir/module-ir.mjs +25 -1
- package/framework/core.d.ts +52 -1
- package/package.json +1 -1
package/framework/build.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import { normalizeCustomHookTimerRefs } from "./compiler/custom-hook-timer-pass.
|
|
|
13
13
|
import { captureNames, createDescriptorSession, createSemanticArtifact, nativeCaptureNames, referencedReducerDispatches, referencedStateNames } from "./compiler/descriptor-session.mjs"
|
|
14
14
|
import { createEffectCodegen } from "./compiler/effect-codegen.mjs"
|
|
15
15
|
import { createHandlerCodegen } from "./compiler/handler-codegen.mjs"
|
|
16
|
+
import { createHandlerLowering } from "./compiler/handler-lowering.mjs"
|
|
16
17
|
import { createCommandSpecializer } from "./compiler/optimize/command-specialization.mjs"
|
|
17
18
|
import { applyNormalizationPasses } from "./compiler/normalization-pipeline.mjs"
|
|
18
19
|
import { createReactMigrationPass, reactMemoExpression } from "./compiler/react-migration-pass.mjs"
|
|
@@ -704,18 +705,18 @@ async function compile(file, sourceFiles, sourceIndex, staticFiles, importedAsse
|
|
|
704
705
|
await mkdir(resolve(output, ".."), { recursive: true })
|
|
705
706
|
await writeFile(output, result.outputText)
|
|
706
707
|
|
|
707
|
-
const { componentAnalysis,
|
|
708
|
-
const sourceResult = { file: relative(root, file).replaceAll(sep, "/"), componentAnalysis }
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
const moduleSource = printHandlerModule({
|
|
708
|
+
const { componentAnalysis, moduleIR } = semantic
|
|
709
|
+
const sourceResult = { file: relative(root, file).replaceAll(sep, "/"), componentAnalysis, moduleIR }
|
|
710
|
+
const moduleHandlers = moduleIR.handlers.filter(handler => handler.kind === "module-export")
|
|
711
|
+
if (!moduleHandlers.length && !moduleIR.bindings.length) return sourceResult
|
|
712
|
+
const moduleSource = printHandlerModule({ moduleIR, handlerPath })
|
|
712
713
|
const moduleResult = ts.transpileModule(moduleSource, {
|
|
713
714
|
compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext },
|
|
714
715
|
reportDiagnostics: true
|
|
715
716
|
})
|
|
716
717
|
const moduleErrors = moduleResult.diagnostics?.filter(diagnostic => diagnostic.category === ts.DiagnosticCategory.Error) ?? []
|
|
717
718
|
if (moduleErrors.length) throw new Error(moduleErrors.map(error => ts.flattenDiagnosticMessageText(error.messageText, "\n")).join("\n"))
|
|
718
|
-
sourceResult.handlerModule = { path: handlerPath, code: moduleResult.outputText, hasNativeHandlers:
|
|
719
|
+
sourceResult.handlerModule = { path: handlerPath, code: moduleResult.outputText, hasNativeHandlers: moduleHandlers.some(handler => handler.role === "native"), hasEffects: moduleHandlers.some(handler => handler.role === "effect"), clientImports: moduleIR.clientModules, hasPackageImports: moduleIR.imports.some(entry => entry.package) }
|
|
719
720
|
return sourceResult
|
|
720
721
|
}
|
|
721
722
|
|
|
@@ -882,7 +883,7 @@ function normalizeCompilerSource(sourceFile, { base, context, file, importedColl
|
|
|
882
883
|
}
|
|
883
884
|
|
|
884
885
|
function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourceIndex, staticFiles, importedAssets, cssModules, base, workerReferences }) {
|
|
885
|
-
const {
|
|
886
|
+
const { moduleIR } = semantic
|
|
886
887
|
return context => sourceFile => {
|
|
887
888
|
const hasLinkElements = /<link/i.test(sourceFile.text)
|
|
888
889
|
const importedStaticCollections = importedSerializableCollections(sourceFile, file, sourceFiles, sourceIndex)
|
|
@@ -899,6 +900,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
899
900
|
factory,
|
|
900
901
|
context,
|
|
901
902
|
compileEventCommand,
|
|
903
|
+
handlerLowering,
|
|
902
904
|
isPrimitiveLiteral: isPrimitiveDefaultLiteral,
|
|
903
905
|
sourceName,
|
|
904
906
|
rejectWorkerConstructions: expression => workerCompiler.rejectConstructions(expression, expression.getSourceFile(), "Relative TypeScript Worker construction is only supported directly inside an inline useEffect() callback")
|
|
@@ -950,13 +952,8 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
950
952
|
const customHooks = new Map()
|
|
951
953
|
const jsxLocalDeclarations = new Map()
|
|
952
954
|
const jsxLocalsByFunction = new Map()
|
|
953
|
-
const listLocalDeclarations =
|
|
954
|
-
const listLocalUses =
|
|
955
|
-
const listValues = new WeakMap()
|
|
956
|
-
const listEventItems = new WeakMap()
|
|
957
|
-
const listConditions = new WeakMap()
|
|
958
|
-
const nestedLists = new WeakMap()
|
|
959
|
-
const listEffectEntries = new WeakMap()
|
|
955
|
+
const listLocalDeclarations = []
|
|
956
|
+
const listLocalUses = []
|
|
960
957
|
const componentEffectEntries = new WeakMap()
|
|
961
958
|
const analysisSource = node => {
|
|
962
959
|
const original = ts.getOriginalNode(node)
|
|
@@ -1338,8 +1335,8 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1338
1335
|
const position = sourceFile.getLineAndCharacterOfPosition(declaration.node.getStart(sourceFile))
|
|
1339
1336
|
if (uses.length > 1) throw new Error(`${sourceFile.fileName}:${position.line + 1}:${position.character + 1} Keyed list local "${name}" must be rendered exactly once`)
|
|
1340
1337
|
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`)
|
|
1341
|
-
listLocalDeclarations.
|
|
1342
|
-
if (uses.length) listLocalUses.
|
|
1338
|
+
listLocalDeclarations.push(declaration.node)
|
|
1339
|
+
if (uses.length) listLocalUses.push({ node: uses[0], parts })
|
|
1343
1340
|
}
|
|
1344
1341
|
}
|
|
1345
1342
|
}
|
|
@@ -1482,6 +1479,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1482
1479
|
ids: result.ordinaryIds.map(({ name, source }) => ({ name, ...(analysisSource(source) ? { source: analysisSource(source) } : {}) }))
|
|
1483
1480
|
})
|
|
1484
1481
|
for (const state of [...result.rowStates, ...result.ordinaryStates]) state.analysisOwner = `specialization:${result.analysis.slot}`
|
|
1482
|
+
for (const ref of [...result.rowRefs, ...result.ordinaryRefs]) ref.analysisOwner = `specialization:${result.analysis.slot}`
|
|
1485
1483
|
return result
|
|
1486
1484
|
}
|
|
1487
1485
|
const registerRowHooks = (call, specialization) => {
|
|
@@ -1845,20 +1843,20 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1845
1843
|
if (expanded === value || !referencedStateNames(expanded, setters).size) fail(value, "Calculated collection fields must directly depend on local state")
|
|
1846
1844
|
return expanded
|
|
1847
1845
|
}
|
|
1848
|
-
const parts = listLocalUses.
|
|
1846
|
+
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)
|
|
1849
1847
|
if (parts) {
|
|
1850
|
-
for (const declaration of parts.aliasDeclarations ?? []) listLocalDeclarations.
|
|
1848
|
+
for (const declaration of parts.aliasDeclarations ?? []) if (!listLocalDeclarations.includes(declaration)) listLocalDeclarations.push(declaration)
|
|
1851
1849
|
rawRenderedLists.push({ node, parts })
|
|
1852
1850
|
}
|
|
1853
1851
|
}
|
|
1854
1852
|
ts.forEachChild(node, collectRenderedLists)
|
|
1855
1853
|
}
|
|
1856
1854
|
collectRenderedLists(sourceFile)
|
|
1857
|
-
const collectionAliasUses =
|
|
1855
|
+
const collectionAliasUses = rawRenderedLists.flatMap(({ parts }) => parts.aliasUses ?? [])
|
|
1858
1856
|
const collectionAliasDeclarations = new Set(rawRenderedLists.flatMap(({ parts }) => parts.aliasDeclarations ?? []))
|
|
1859
1857
|
for (const declaration of collectionAliasDeclarations) {
|
|
1860
1858
|
const owner = nearestFunction(declaration)
|
|
1861
|
-
const unsupported = identifierReferences(owner.body, declaration.name.text).find(reference => !collectionAliasUses.
|
|
1859
|
+
const unsupported = identifierReferences(owner.body, declaration.name.text).find(reference => !collectionAliasUses.includes(reference))
|
|
1862
1860
|
if (unsupported) fail(unsupported, `Rendered collection alias "${declaration.name.text}" may only be used as a rendered collection source`)
|
|
1863
1861
|
}
|
|
1864
1862
|
const rejectUnsupportedRenderControl = node => {
|
|
@@ -1910,7 +1908,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1910
1908
|
let count = 0
|
|
1911
1909
|
const visit = (node, currentAggregate = aggregate) => {
|
|
1912
1910
|
if (node !== root && ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "map" && containsJsx(node)) {
|
|
1913
|
-
const nestedAggregate = { calculations: [], effects: [], hookDeclarations: [], rowStates: [], rowRefs: [] }
|
|
1911
|
+
const nestedAggregate = { calculations: [], effects: [], hookDeclarations: [], rowStates: [], rowRefs: [], specializations: [] }
|
|
1914
1912
|
for (const argument of node.arguments) visit(argument, nestedAggregate)
|
|
1915
1913
|
if (nestedAggregate.hookDeclarations.length || nestedAggregate.effects.length) nestedRowSpecializations.set(`${node.pos}:${node.end}`, nestedAggregate)
|
|
1916
1914
|
return
|
|
@@ -1937,6 +1935,8 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1937
1935
|
if (imported) synthesizeTree(specialization.root = mergeSpecializedImports(specialization.root, component.getSourceFile(), node, specialization.effects))
|
|
1938
1936
|
expandedRowSpecializations.set(specialization.root, specialization)
|
|
1939
1937
|
if (currentAggregate) {
|
|
1938
|
+
currentAggregate.specializations ??= []
|
|
1939
|
+
currentAggregate.specializations.push(specialization.analysis.slot, ...(specialization.specializations ?? []))
|
|
1940
1940
|
currentAggregate.effects.push(...specialization.effects)
|
|
1941
1941
|
currentAggregate.hookDeclarations.push(...specialization.hookDeclarations)
|
|
1942
1942
|
currentAggregate.rowStates.push(...specialization.rowStates)
|
|
@@ -1955,8 +1955,8 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1955
1955
|
expanded.parent = root.parent
|
|
1956
1956
|
return expanded
|
|
1957
1957
|
}
|
|
1958
|
-
const
|
|
1959
|
-
const prepareListCallback = (callback, root, specialization, item) => {
|
|
1958
|
+
const preparedRenderedLists = []
|
|
1959
|
+
const prepareListCallback = (callback, root, specialization, item, effectEntries) => {
|
|
1960
1960
|
const statements = [...specialization.hookDeclarations]
|
|
1961
1961
|
if (specialization.effects.length) {
|
|
1962
1962
|
usesListEffects = true
|
|
@@ -1964,7 +1964,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1964
1964
|
const call = factory.updateCallExpression(entry.call, factory.createIdentifier("__kListUseEffect"), entry.call.typeArguments, entry.call.arguments)
|
|
1965
1965
|
synthesizeTree(call)
|
|
1966
1966
|
const effectSource = entry.source.getSourceFile()
|
|
1967
|
-
|
|
1967
|
+
effectEntries.push({ node: call, item, source: entry.source, sourceFile: effectSource, imports: clientImportBindings(effectSource, effectSource.fileName, sourceFiles) })
|
|
1968
1968
|
return factory.createExpressionStatement(call)
|
|
1969
1969
|
}))
|
|
1970
1970
|
}
|
|
@@ -1995,15 +1995,25 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1995
1995
|
ts.setParentRecursive(callback, false)
|
|
1996
1996
|
callback.parent = originalParts.callback.parent
|
|
1997
1997
|
}
|
|
1998
|
-
|
|
1999
|
-
|
|
1998
|
+
const effectEntries = []
|
|
1999
|
+
callback = prepareListCallback(callback, root, specialization, originalParts.item, effectEntries)
|
|
2000
|
+
const parts = {
|
|
2001
|
+
...originalParts,
|
|
2002
|
+
root,
|
|
2003
|
+
callback,
|
|
2004
|
+
effectEntries,
|
|
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
|
+
}
|
|
2000
2010
|
for (const calculation of specialization.calculations) {
|
|
2001
2011
|
ts.setParentRecursive(calculation, false)
|
|
2002
2012
|
calculation.parent = callback
|
|
2003
2013
|
validateListExpression(calculation, parts.item, originalParts.root, fail)
|
|
2004
2014
|
}
|
|
2005
|
-
validateKeyedList(parts, sourceFile,
|
|
2006
|
-
|
|
2015
|
+
const analysis = validateKeyedList(parts, sourceFile, settersForNode(originalParts.root, settersByFunction), specialization.rowStates, componentSpecializations, expandedRowSpecializations, nestedRowSpecializations, factory, prepareListCallback)
|
|
2016
|
+
preparedRenderedLists.push({ node, parts, analysis })
|
|
2007
2017
|
}
|
|
2008
2018
|
|
|
2009
2019
|
const compileRenderExpression = (expression, anchor) => {
|
|
@@ -2025,6 +2035,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
2025
2035
|
}
|
|
2026
2036
|
|
|
2027
2037
|
let activeStateOwners
|
|
2038
|
+
let activeKeyedBlock
|
|
2028
2039
|
const visitWithStateOwners = (node, stateOwners) => {
|
|
2029
2040
|
const previous = activeStateOwners
|
|
2030
2041
|
activeStateOwners = new Map([...(previous ?? []), ...stateOwners])
|
|
@@ -2032,6 +2043,58 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
2032
2043
|
activeStateOwners = previous
|
|
2033
2044
|
return result
|
|
2034
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
|
+
}
|
|
2035
2098
|
const visitor = node => {
|
|
2036
2099
|
if (ts.isVariableDeclaration(node) && ts.isObjectBindingPattern(node.name) && customHookPrivateFields.has(node)) {
|
|
2037
2100
|
const privateFields = customHookPrivateFields.get(node)
|
|
@@ -2075,7 +2138,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
2075
2138
|
return factory.updateExportDeclaration(node, node.modifiers, node.isTypeOnly, node.exportClause, factory.createStringLiteral(relativeModulePath(compiledPath(file), compiledPath(target))), node.attributes)
|
|
2076
2139
|
}
|
|
2077
2140
|
|
|
2078
|
-
const listEffect = ts.isCallExpression(node) ?
|
|
2141
|
+
const listEffect = ts.isCallExpression(node) ? keyedEntry(activeKeyedBlock?.analysis.effects ?? [], node) : undefined
|
|
2079
2142
|
const componentEffect = ts.isCallExpression(node) ? componentEffectEntries.get(node) : undefined
|
|
2080
2143
|
const specializedEffect = listEffect ?? componentEffect
|
|
2081
2144
|
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && (hasUseEffectImport && node.expression.text === "useEffect" || specializedEffect)) {
|
|
@@ -2118,7 +2181,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
2118
2181
|
}
|
|
2119
2182
|
const invalidDependency = ordinaryDependencies.find(dependency => !ts.isIdentifier(dependency))
|
|
2120
2183
|
if (invalidDependency) effectFail(invalidDependency, "useEffect() dependencies must be direct state or runtime parameter identifiers")
|
|
2121
|
-
const
|
|
2184
|
+
const dependencyDerived = []
|
|
2122
2185
|
const dependencyStates = new Map()
|
|
2123
2186
|
const dependencySubstitutions = new Map()
|
|
2124
2187
|
const subscriptionDependencies = []
|
|
@@ -2134,7 +2197,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
2134
2197
|
const usedStates = new Set()
|
|
2135
2198
|
const expression = collectionExpression(initializer, { fail: effectFail, stateNames, selectorStates: usedStates })
|
|
2136
2199
|
if (!usedStates.size) effectFail(dependency, `useEffect() derived dependency "${dependency.text}" must read direct primitive state`)
|
|
2137
|
-
|
|
2200
|
+
dependencyDerived.push({ expression, states: usedStates, source: initializer })
|
|
2138
2201
|
for (const name of usedStates) {
|
|
2139
2202
|
subscriptionDependencies.push(factory.createIdentifier(name))
|
|
2140
2203
|
dependencyStates.set(name, factory.createIdentifier(name))
|
|
@@ -2143,12 +2206,12 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
2143
2206
|
hasDerivedDependency = true
|
|
2144
2207
|
} else {
|
|
2145
2208
|
subscriptionDependencies.push(dependency)
|
|
2146
|
-
|
|
2209
|
+
dependencyDerived.push({ expression: ["state", dependency.text], states: [dependency.text], source: dependency })
|
|
2147
2210
|
dependencyStates.set(dependency.text, dependency)
|
|
2148
2211
|
}
|
|
2149
2212
|
}
|
|
2150
2213
|
if (!hasDerivedDependency) {
|
|
2151
|
-
|
|
2214
|
+
dependencyDerived.length = 0
|
|
2152
2215
|
dependencyStates.clear()
|
|
2153
2216
|
}
|
|
2154
2217
|
if (!effectOwner) fail(node, "useEffect() cannot be used outside a Kudzu component")
|
|
@@ -2193,6 +2256,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
2193
2256
|
reducers: reducersForNode(node, reducersByFunction),
|
|
2194
2257
|
importBindings: specializedEffect?.imports ?? importBindings,
|
|
2195
2258
|
listItem: dependencyItem,
|
|
2259
|
+
keyedBlock: activeKeyedBlock?.block.slot,
|
|
2196
2260
|
deferValues: true,
|
|
2197
2261
|
snapshotNested: returns.cleanup,
|
|
2198
2262
|
liveStates: customHookTimerStates
|
|
@@ -2200,6 +2264,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
2200
2264
|
for (const reference of workerReferences.slice(workerStart)) Object.assign(reference, { module: handlerUrl, handler: descriptor.exportName })
|
|
2201
2265
|
usesListItem ||= Boolean(itemDependencies.length && !listEffect)
|
|
2202
2266
|
usesBehavior = true
|
|
2267
|
+
const derivedDependencies = hasDerivedDependency ? dependencyDerived.map(entry => descriptors.registerDerived("expression", entry.expression, entry.states, entry.source)) : []
|
|
2203
2268
|
return factory.updateCallExpression(node, node.expression, node.typeArguments, [
|
|
2204
2269
|
callback,
|
|
2205
2270
|
factory.createArrayLiteralExpression(hasDerivedDependency ? subscriptionDependencies : ordinaryDependencies),
|
|
@@ -2210,7 +2275,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
2210
2275
|
factory.createStringLiteral(specializedEffect ? sourceLocation(specializedEffect.source, specializedEffect.sourceFile) : sourceLocation(node, sourceFile)),
|
|
2211
2276
|
returns.cleanup ? factory.createTrue() : factory.createFalse(),
|
|
2212
2277
|
factory.createArrayLiteralExpression(itemDependencies.map(field => factory.createStringLiteral(field))),
|
|
2213
|
-
hasDerivedDependency ? jsonExpression(
|
|
2278
|
+
hasDerivedDependency ? jsonExpression(derivedDependencies.map(entry => entry.expression), factory) : factory.createArrayLiteralExpression(),
|
|
2214
2279
|
factory.createArrayLiteralExpression([...dependencyStates].map(([name, state]) => factory.createArrayLiteralExpression([factory.createStringLiteral(name), state])))
|
|
2215
2280
|
])
|
|
2216
2281
|
}
|
|
@@ -2225,7 +2290,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
2225
2290
|
return factory.updateVariableDeclaration(node, node.name, node.exclamationToken, node.type, initializer)
|
|
2226
2291
|
}
|
|
2227
2292
|
|
|
2228
|
-
if (ts.isVariableDeclaration(node) && listLocalDeclarations.
|
|
2293
|
+
if (ts.isVariableDeclaration(node) && listLocalDeclarations.includes(node)) {
|
|
2229
2294
|
return factory.updateVariableDeclaration(node, node.name, node.exclamationToken, node.type, factory.createIdentifier("undefined"))
|
|
2230
2295
|
}
|
|
2231
2296
|
|
|
@@ -2250,46 +2315,31 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
2250
2315
|
if (compiled !== node.expression) return factory.updateReturnStatement(node, compiled)
|
|
2251
2316
|
}
|
|
2252
2317
|
|
|
2253
|
-
|
|
2254
|
-
|
|
2318
|
+
const listCondition = ts.isJsxExpression(node) && node.expression ? keyedEntry(activeKeyedBlock?.analysis.conditions ?? [], node.expression) : undefined
|
|
2319
|
+
if (listCondition) {
|
|
2320
|
+
const entry = listCondition.value
|
|
2255
2321
|
return factory.updateJsxExpression(node, descriptors.compileListConditional({
|
|
2256
2322
|
...entry,
|
|
2323
|
+
keyedBlock: activeKeyedBlock.block.slot,
|
|
2257
2324
|
truthy: ts.visitNode(entry.truthy, visitor),
|
|
2258
2325
|
falsy: ts.visitNode(entry.falsy, visitor)
|
|
2259
2326
|
}))
|
|
2260
2327
|
}
|
|
2261
2328
|
|
|
2262
|
-
|
|
2263
|
-
|
|
2329
|
+
const listValue = ts.isJsxExpression(node) && node.expression ? keyedEntry(activeKeyedBlock?.analysis.values ?? [], node.expression) : undefined
|
|
2330
|
+
if (listValue) {
|
|
2331
|
+
return factory.updateJsxExpression(node, descriptors.compileListValue(node.expression, { ...listValue.value, keyedBlock: activeKeyedBlock.block.slot }))
|
|
2264
2332
|
}
|
|
2265
2333
|
|
|
2266
|
-
|
|
2267
|
-
|
|
2334
|
+
const attributeListValue = ts.isJsxAttribute(node) && node.initializer && ts.isJsxExpression(node.initializer) && node.initializer.expression ? keyedEntry(activeKeyedBlock?.analysis.values ?? [], node.initializer.expression) : undefined
|
|
2335
|
+
if (attributeListValue) {
|
|
2336
|
+
return factory.updateJsxAttribute(node, node.name, factory.createJsxExpression(undefined, descriptors.compileListValue(node.initializer.expression, { ...attributeListValue.value, keyedBlock: activeKeyedBlock.block.slot })))
|
|
2268
2337
|
}
|
|
2269
2338
|
|
|
2270
2339
|
if (ts.isJsxExpression(node) && node.initializer === undefined && node.expression && (ts.isJsxElement(node.parent) || ts.isJsxFragment(node.parent))) {
|
|
2271
|
-
const
|
|
2272
|
-
const
|
|
2273
|
-
if (
|
|
2274
|
-
usesBehavior = true
|
|
2275
|
-
usesList = true
|
|
2276
|
-
let listSource = listParts.state
|
|
2277
|
-
if (listParts.calculation) {
|
|
2278
|
-
usesBinding = true
|
|
2279
|
-
listSource = descriptors.compileReactiveBinding(listParts.calculation, { setters: settersForNode(node, settersByFunction), importBindings })
|
|
2280
|
-
}
|
|
2281
|
-
const arguments_ = [
|
|
2282
|
-
listSource,
|
|
2283
|
-
listParts.keyField === null ? factory.createNull() : factory.createStringLiteral(listParts.keyField),
|
|
2284
|
-
visitWithStateOwners(listParts.callback, listParts.analysisStateOwners ?? new Map()),
|
|
2285
|
-
factory.createStringLiteral(listParts.ownerField ?? ""),
|
|
2286
|
-
jsonExpression(listParts.selector ?? [], factory),
|
|
2287
|
-
listParts.indexed ? factory.createTrue() : factory.createFalse()
|
|
2288
|
-
]
|
|
2289
|
-
if (listParts.selectorStates?.size || listParts.static) arguments_.push(factory.createArrayLiteralExpression([...(listParts.selectorStates ?? [])].map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), factory.createIdentifier(name)]))))
|
|
2290
|
-
if (listParts.static) arguments_.push(factory.createTrue())
|
|
2291
|
-
return factory.updateJsxExpression(node, factory.createCallExpression(factory.createIdentifier("__kList"), undefined, arguments_))
|
|
2292
|
-
}
|
|
2340
|
+
const renderedList = keyedEntry(preparedRenderedLists, node)
|
|
2341
|
+
const nestedList = keyedEntry(activeKeyedBlock?.analysis.nested ?? [], unwrapExpression(node.expression))
|
|
2342
|
+
if (renderedList || nestedList) return compileKeyedBlock(node, renderedList ?? nestedList)
|
|
2293
2343
|
const conditional = conditionalParts(node.expression)
|
|
2294
2344
|
if (conditional) {
|
|
2295
2345
|
const compiled = compileRenderExpression(node.expression, node)
|
|
@@ -2328,7 +2378,8 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
2328
2378
|
setters,
|
|
2329
2379
|
reducers: reducersForNode(node, reducersByFunction),
|
|
2330
2380
|
functions: functionsForNode(node),
|
|
2331
|
-
listItem:
|
|
2381
|
+
listItem: activeKeyedBlock ? { item: activeKeyedBlock.parts.item, index: activeKeyedBlock.parts.index } : undefined,
|
|
2382
|
+
keyedBlock: activeKeyedBlock?.block.slot,
|
|
2332
2383
|
importBindings: new Map([...importBindings, ...packageBindings])
|
|
2333
2384
|
})
|
|
2334
2385
|
if (event) {
|
|
@@ -2344,10 +2395,11 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
2344
2395
|
}
|
|
2345
2396
|
|
|
2346
2397
|
const transformed = ts.visitNode(sourceFile, visitor)
|
|
2398
|
+
descriptors.finalize()
|
|
2347
2399
|
if (!usesBehavior) return transformed
|
|
2348
2400
|
|
|
2349
2401
|
const behaviorImports = [factory.createImportSpecifier(false, factory.createIdentifier("behavior"), factory.createIdentifier("__kBehavior"))]
|
|
2350
|
-
if (
|
|
2402
|
+
if (moduleIR.handlers.some(handler => handler.kind === "module-export" && handler.role === "native")) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("nativeBehavior"), factory.createIdentifier("__kNativeBehavior")))
|
|
2351
2403
|
if (usesBinding) {
|
|
2352
2404
|
behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("binding"), factory.createIdentifier("__kBinding")))
|
|
2353
2405
|
behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("select"), factory.createIdentifier("__kSelect")))
|
|
@@ -2562,10 +2614,11 @@ function insideJsxEventHandler(node, root) {
|
|
|
2562
2614
|
return false
|
|
2563
2615
|
}
|
|
2564
2616
|
|
|
2565
|
-
function validateKeyedList(parts, sourceFile,
|
|
2617
|
+
function validateKeyedList(parts, sourceFile, setters, rowStates, componentSpecializations, expandedRowSpecializations, nestedRowSpecializations, factory, prepareListCallback) {
|
|
2566
2618
|
const fail = (node, message) => {
|
|
2567
2619
|
throw sourceNodeError(node, sourceFile, message)
|
|
2568
2620
|
}
|
|
2621
|
+
const analysis = { values: [], conditions: [], nested: [], effects: parts.effectEntries ?? [] }
|
|
2569
2622
|
const root = parts.root
|
|
2570
2623
|
const item = parts.item
|
|
2571
2624
|
const nestedDiagnostic = "Nested keyed list collections must be a direct property of the parent item"
|
|
@@ -2580,7 +2633,6 @@ function validateKeyedList(parts, sourceFile, listValues, listEventItems, listCo
|
|
|
2580
2633
|
if (node !== root && ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "map" && containsJsx(node)) fail(node, nestedDiagnostic)
|
|
2581
2634
|
if (ts.isJsxSpreadAttribute(node) && referencesIdentifier(node.expression, item)) fail(node, "Keyed list item spreads are not supported")
|
|
2582
2635
|
if (ts.isJsxAttribute(node) && /^on[A-Z]/.test(node.name.text)) {
|
|
2583
|
-
listEventItems.set(node, { item, index: parts.index })
|
|
2584
2636
|
return
|
|
2585
2637
|
}
|
|
2586
2638
|
if (ts.isJsxExpression(node) && node.expression) {
|
|
@@ -2605,16 +2657,28 @@ function validateKeyedList(parts, sourceFile, listValues, listEventItems, listCo
|
|
|
2605
2657
|
ts.setParentRecursive(callback, false)
|
|
2606
2658
|
callback.parent = nested.callback.parent
|
|
2607
2659
|
}
|
|
2608
|
-
|
|
2660
|
+
const effectEntries = []
|
|
2661
|
+
callback = prepareListCallback(callback, root, specialization ?? { hookDeclarations: [], effects: [] }, nested.item, effectEntries)
|
|
2609
2662
|
const specializedStates = [...(specialization?.rowStates ?? []), ...(specialization?.ordinaryStates ?? [])]
|
|
2610
|
-
const nestedParts = {
|
|
2663
|
+
const nestedParts = {
|
|
2664
|
+
...nested,
|
|
2665
|
+
root,
|
|
2666
|
+
callback,
|
|
2667
|
+
state: parts.state,
|
|
2668
|
+
nested: true,
|
|
2669
|
+
effectEntries,
|
|
2670
|
+
specializations: [specialization?.analysis?.slot, ...(specialization?.specializations ?? [])].filter(slot => slot !== undefined),
|
|
2671
|
+
rowStates: specializedStates,
|
|
2672
|
+
rowRefs: specialization?.rowRefs ?? [],
|
|
2673
|
+
analysisStateOwners: new Map([...(parts.analysisStateOwners ?? []), ...specializedStates.map(state => [state.state, state.analysisOwner])])
|
|
2674
|
+
}
|
|
2611
2675
|
for (const calculation of specialization?.calculations ?? []) {
|
|
2612
2676
|
ts.setParentRecursive(calculation, false)
|
|
2613
2677
|
calculation.parent = callback
|
|
2614
2678
|
validateListExpression(calculation, nested.item, nested.root, fail)
|
|
2615
2679
|
}
|
|
2616
|
-
|
|
2617
|
-
|
|
2680
|
+
const nestedAnalysis = validateKeyedList(nestedParts, sourceFile, setters, specialization?.rowStates ?? [], componentSpecializations, expandedRowSpecializations, nestedRowSpecializations, factory, prepareListCallback)
|
|
2681
|
+
analysis.nested.push({ node: expression, parts: nestedParts, analysis: nestedAnalysis })
|
|
2618
2682
|
return
|
|
2619
2683
|
}
|
|
2620
2684
|
const condition = conditionalParts(expression)
|
|
@@ -2626,7 +2690,7 @@ function validateKeyedList(parts, sourceFile, listValues, listEventItems, listCo
|
|
|
2626
2690
|
}
|
|
2627
2691
|
if (!referencesIdentifier(condition.condition, item) && !(parts.index && referencesIdentifier(condition.condition, parts.index))) fail(node, "Keyed list item conditions must read the item or index")
|
|
2628
2692
|
validateListExpression(condition.condition, item, node, fail, parts.index)
|
|
2629
|
-
|
|
2693
|
+
analysis.conditions.push({ node: node.expression, value: { ...condition, item, index: parts.index } })
|
|
2630
2694
|
visit(condition.truthy)
|
|
2631
2695
|
visit(condition.falsy)
|
|
2632
2696
|
return
|
|
@@ -2637,7 +2701,7 @@ function validateKeyedList(parts, sourceFile, listValues, listEventItems, listCo
|
|
|
2637
2701
|
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`)
|
|
2638
2702
|
if (isRootKey) return
|
|
2639
2703
|
if (field) {
|
|
2640
|
-
|
|
2704
|
+
analysis.values.push({ node: node.expression, value: { field } })
|
|
2641
2705
|
return
|
|
2642
2706
|
}
|
|
2643
2707
|
if (referencesIdentifier(expression, item) || parts.index && referencesIdentifier(expression, parts.index)) {
|
|
@@ -2646,13 +2710,14 @@ function validateKeyedList(parts, sourceFile, listValues, listEventItems, listCo
|
|
|
2646
2710
|
if (parts.nested && states.size) fail(node, "Nested keyed list item expressions cannot read parent state")
|
|
2647
2711
|
validateListExpression(expression, item, node, fail, parts.index, states)
|
|
2648
2712
|
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`)
|
|
2649
|
-
|
|
2713
|
+
analysis.values.push({ node: node.expression, value: { item, index: parts.index, states } })
|
|
2650
2714
|
return
|
|
2651
2715
|
}
|
|
2652
2716
|
}
|
|
2653
2717
|
ts.forEachChild(node, visit)
|
|
2654
2718
|
}
|
|
2655
2719
|
visit(root)
|
|
2720
|
+
return analysis
|
|
2656
2721
|
}
|
|
2657
2722
|
|
|
2658
2723
|
function directConstObjectLiteral(expression, call) {
|
|
@@ -3885,9 +3950,8 @@ const workerCompiler = createWorkerCompiler({
|
|
|
3885
3950
|
})
|
|
3886
3951
|
|
|
3887
3952
|
const printEffectEntry = createEffectCodegen({ assetPath, inlineJson, relativeModulePath })
|
|
3953
|
+
const handlerLowering = createHandlerLowering({ cloneAst, synthesizeTree })
|
|
3888
3954
|
const printHandlerModule = createHandlerCodegen({
|
|
3889
|
-
cloneAst,
|
|
3890
|
-
synthesizeTree,
|
|
3891
3955
|
resolveClientImport: (entry, handlerPath) => entry.package ? entry.target : relativeModulePath(handlerPath, clientModulePath(entry.target))
|
|
3892
3956
|
})
|
|
3893
3957
|
const { normalizeReactMigrationSyntax, validateUseIdSyntax } = createReactMigrationPass({ cloneAst, jsxTagName })
|