@kudzujs/core 0.8.17 → 0.8.19
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 +79 -0
- package/README.md +1 -1
- package/RELEASES.md +66 -0
- package/docs/next-architecture/README.md +1 -1
- package/docs/next-architecture/compiler-current-architecture.md +7 -5
- package/docs/next-architecture/goal-a-compiler-foundation.md +2 -2
- package/docs/next-architecture/versioning.md +1 -1
- package/framework/README.md +4 -2
- package/framework/build.mjs +194 -49
- package/framework/compiler/analysis/component-analysis.mjs +49 -0
- package/framework/compiler/descriptor-session.mjs +88 -21
- package/framework/compiler/handler-codegen.mjs +6 -279
- package/framework/compiler/handler-lowering.mjs +278 -0
- package/framework/compiler/ir/module-ir.mjs +23 -5
- package/package.json +1 -1
package/framework/build.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { dirname, extname, isAbsolute, join, relative, resolve, sep } from "node
|
|
|
4
4
|
import { pathToFileURL } from "node:url"
|
|
5
5
|
import { build as bundle, transform } from "esbuild"
|
|
6
6
|
import ts from "typescript"
|
|
7
|
+
import { createComponentAnalysisSession } from "./compiler/analysis/component-analysis.mjs"
|
|
7
8
|
import { normalizeEffectAnimationFrameRefs } from "./compiler/animation-frame-pass.mjs"
|
|
8
9
|
import { bindingNames, containsJsx, effectReturns, functionVarDeclaresName, importDeclarationNames, isFunctionLike, isLocalConst, isReferenceIdentifier, isShadowedByParameter, isShadowedIdentifier, isUnshadowedGlobal, nearestFunction, referenceIdentifiers, referencesIdentifier, sourceLocation, sourceNodeError, statementDeclaresName, unwrapExpression } from "./compiler/ast-helpers.mjs"
|
|
9
10
|
import { normalizeMediaQueryExternalStores, normalizeNavigatorCapabilityConditions } from "./compiler/browser-signal-passes.mjs"
|
|
@@ -12,6 +13,7 @@ import { normalizeCustomHookTimerRefs } from "./compiler/custom-hook-timer-pass.
|
|
|
12
13
|
import { captureNames, createDescriptorSession, createSemanticArtifact, nativeCaptureNames, referencedReducerDispatches, referencedStateNames } from "./compiler/descriptor-session.mjs"
|
|
13
14
|
import { createEffectCodegen } from "./compiler/effect-codegen.mjs"
|
|
14
15
|
import { createHandlerCodegen } from "./compiler/handler-codegen.mjs"
|
|
16
|
+
import { createHandlerLowering } from "./compiler/handler-lowering.mjs"
|
|
15
17
|
import { createCommandSpecializer } from "./compiler/optimize/command-specialization.mjs"
|
|
16
18
|
import { applyNormalizationPasses } from "./compiler/normalization-pipeline.mjs"
|
|
17
19
|
import { createReactMigrationPass, reactMemoExpression } from "./compiler/react-migration-pass.mjs"
|
|
@@ -72,13 +74,13 @@ export async function build({ quiet = false, minify = true } = {}) {
|
|
|
72
74
|
const importedAssets = new Set()
|
|
73
75
|
const { cssModules, cssOutputs } = await prepareSourceStyles(cssFiles, staticFiles, importedAssets, base)
|
|
74
76
|
|
|
75
|
-
const
|
|
77
|
+
const sourceResults = []
|
|
76
78
|
const workerReferences = []
|
|
77
79
|
for (const file of sourceFiles) {
|
|
78
80
|
if (file.endsWith(".worker.ts")) continue
|
|
79
|
-
|
|
80
|
-
if (handlerModule) handlerModules.push(handlerModule)
|
|
81
|
+
sourceResults.push(await compile(file, sourceFileSet, sourceIndex, staticFiles, importedAssets, cssModules, base, workerReferences))
|
|
81
82
|
}
|
|
83
|
+
const handlerModules = sourceResults.flatMap(result => result.handlerModule ? [result.handlerModule] : [])
|
|
82
84
|
|
|
83
85
|
const plans = []
|
|
84
86
|
const routeCapabilities = new Map()
|
|
@@ -466,6 +468,7 @@ export async function build({ quiet = false, minify = true } = {}) {
|
|
|
466
468
|
}
|
|
467
469
|
|
|
468
470
|
if (!quiet) console.log(`Built ${plans.length} page(s), ${behaviorCount} interactive page(s) into dist/`)
|
|
471
|
+
return { sourceResults }
|
|
469
472
|
}
|
|
470
473
|
|
|
471
474
|
function preloadModules(html) {
|
|
@@ -702,17 +705,19 @@ async function compile(file, sourceFiles, sourceIndex, staticFiles, importedAsse
|
|
|
702
705
|
await mkdir(resolve(output, ".."), { recursive: true })
|
|
703
706
|
await writeFile(output, result.outputText)
|
|
704
707
|
|
|
705
|
-
const {
|
|
706
|
-
|
|
707
|
-
const
|
|
708
|
-
|
|
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 })
|
|
709
713
|
const moduleResult = ts.transpileModule(moduleSource, {
|
|
710
714
|
compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext },
|
|
711
715
|
reportDiagnostics: true
|
|
712
716
|
})
|
|
713
717
|
const moduleErrors = moduleResult.diagnostics?.filter(diagnostic => diagnostic.category === ts.DiagnosticCategory.Error) ?? []
|
|
714
718
|
if (moduleErrors.length) throw new Error(moduleErrors.map(error => ts.flattenDiagnosticMessageText(error.messageText, "\n")).join("\n"))
|
|
715
|
-
|
|
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) }
|
|
720
|
+
return sourceResult
|
|
716
721
|
}
|
|
717
722
|
|
|
718
723
|
function emittedPackageReference(source, file, packages) {
|
|
@@ -878,7 +883,7 @@ function normalizeCompilerSource(sourceFile, { base, context, file, importedColl
|
|
|
878
883
|
}
|
|
879
884
|
|
|
880
885
|
function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourceIndex, staticFiles, importedAssets, cssModules, base, workerReferences }) {
|
|
881
|
-
const {
|
|
886
|
+
const { moduleIR } = semantic
|
|
882
887
|
return context => sourceFile => {
|
|
883
888
|
const hasLinkElements = /<link/i.test(sourceFile.text)
|
|
884
889
|
const importedStaticCollections = importedSerializableCollections(sourceFile, file, sourceFiles, sourceIndex)
|
|
@@ -887,14 +892,17 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
887
892
|
sourceFile = normalized.sourceFile
|
|
888
893
|
const { customHookTimerStates } = normalized
|
|
889
894
|
const factory = context.factory
|
|
895
|
+
const sourceName = source => relative(root, source.fileName).replaceAll(sep, "/")
|
|
896
|
+
const componentAnalysis = createComponentAnalysisSession(semantic.componentAnalysis)
|
|
890
897
|
const descriptors = createDescriptorSession({
|
|
891
898
|
semantic,
|
|
892
899
|
handlerUrl,
|
|
893
900
|
factory,
|
|
894
901
|
context,
|
|
895
902
|
compileEventCommand,
|
|
903
|
+
handlerLowering,
|
|
896
904
|
isPrimitiveLiteral: isPrimitiveDefaultLiteral,
|
|
897
|
-
sourceName
|
|
905
|
+
sourceName,
|
|
898
906
|
rejectWorkerConstructions: expression => workerCompiler.rejectConstructions(expression, expression.getSourceFile(), "Relative TypeScript Worker construction is only supported directly inside an inline useEffect() callback")
|
|
899
907
|
})
|
|
900
908
|
const importBindings = clientImportBindings(sourceFile, file, sourceFiles)
|
|
@@ -923,6 +931,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
923
931
|
} catch {}
|
|
924
932
|
}
|
|
925
933
|
const settersByFunction = new Map()
|
|
934
|
+
const stateOwnersByFunction = new Map()
|
|
926
935
|
const localStateSettersByFunction = new Map()
|
|
927
936
|
const reducersByFunction = new Map()
|
|
928
937
|
const zustandStores = new Map()
|
|
@@ -951,6 +960,42 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
951
960
|
const nestedLists = new WeakMap()
|
|
952
961
|
const listEffectEntries = new WeakMap()
|
|
953
962
|
const componentEffectEntries = new WeakMap()
|
|
963
|
+
const analysisSource = node => {
|
|
964
|
+
const original = ts.getOriginalNode(node)
|
|
965
|
+
return original.pos >= 0 && original.end >= 0 ? { file: sourceName(original.getSourceFile()), start: original.getStart(), end: original.end } : undefined
|
|
966
|
+
}
|
|
967
|
+
const analyzedProps = owner => {
|
|
968
|
+
if (owner.parameters.length !== 1 || !ts.isObjectBindingPattern(owner.parameters[0].name)) return []
|
|
969
|
+
return owner.parameters[0].name.elements.map(element => ({
|
|
970
|
+
name: (element.propertyName ?? element.name).getText(),
|
|
971
|
+
local: element.name.getText(),
|
|
972
|
+
...(element.dotDotDotToken ? { rest: true } : {}),
|
|
973
|
+
...(element.initializer ? { hasDefault: true } : {})
|
|
974
|
+
}))
|
|
975
|
+
}
|
|
976
|
+
const ownerName = owner => owner.name?.text ?? (ts.isVariableDeclaration(owner.parent) && ts.isIdentifier(owner.parent.name) ? owner.parent.name.text : "anonymous")
|
|
977
|
+
const ensureOwner = (owner, kind = "component") => componentAnalysis.registerOwner(owner, { kind, name: ownerName(owner), props: analyzedProps(owner), source: analysisSource(owner) })
|
|
978
|
+
const registerState = (owner, state, setter, kind, node, externalOwner) => {
|
|
979
|
+
const ownerRecord = ensureOwner(owner)
|
|
980
|
+
const stateOwner = externalOwner ?? `owner:${ownerRecord.slot}`
|
|
981
|
+
const stateOwners = stateOwnersByFunction.get(owner) ?? new Map()
|
|
982
|
+
stateOwners.set(state, stateOwner)
|
|
983
|
+
stateOwnersByFunction.set(owner, stateOwners)
|
|
984
|
+
return componentAnalysis.registerState(owner, { name: state, setter, kind, ...(externalOwner ? { owner: externalOwner } : {}), source: analysisSource(node) })
|
|
985
|
+
}
|
|
986
|
+
const stateOwnersForNode = node => {
|
|
987
|
+
for (let current = node.parent; current; current = current.parent) {
|
|
988
|
+
if (isFunctionLike(current) && stateOwnersByFunction.has(current)) return stateOwnersByFunction.get(current)
|
|
989
|
+
}
|
|
990
|
+
return new Map()
|
|
991
|
+
}
|
|
992
|
+
const fallbackOwner = node => {
|
|
993
|
+
for (let current = node.parent; current; current = current.parent) {
|
|
994
|
+
const owner = isFunctionLike(current) ? componentAnalysis.owner(current) : undefined
|
|
995
|
+
if (owner) return `owner:${owner.slot}`
|
|
996
|
+
}
|
|
997
|
+
return "module"
|
|
998
|
+
}
|
|
954
999
|
let usesBehavior = false
|
|
955
1000
|
let usesBinding = false
|
|
956
1001
|
let usesConditional = false
|
|
@@ -996,6 +1041,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
996
1041
|
if (!value || !ts.isObjectLiteralExpression(value)) throw sourceNodeError(provider, providerSource, "Context Provider value must be one direct object literal")
|
|
997
1042
|
const owner = nearestFunction(provider)
|
|
998
1043
|
if (!owner) throw sourceNodeError(provider, providerSource, "Context Provider value must be returned by a component")
|
|
1044
|
+
const stateOwner = `external:${sourceName(providerSource)}:${owner.getStart(providerSource)}`
|
|
999
1045
|
|
|
1000
1046
|
const states = new Map()
|
|
1001
1047
|
const callbacks = new Map()
|
|
@@ -1033,7 +1079,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1033
1079
|
if (!setter || !fields.has(state) || !fields.has(setter)) throw sourceNodeError(callback, providerSource, `Context action ${JSON.stringify(name)} requires exposed state and setter fields for ${JSON.stringify(state)}`)
|
|
1034
1080
|
}
|
|
1035
1081
|
}
|
|
1036
|
-
return { callbacks: new Map([...callbacks].filter(([name]) => fields.has(name))), context: true, fields, privateStates: new Set(), states }
|
|
1082
|
+
return { callbacks: new Map([...callbacks].filter(([name]) => fields.has(name))), context: true, fields, privateStates: new Set(), stateOwner, states }
|
|
1037
1083
|
}
|
|
1038
1084
|
|
|
1039
1085
|
const resolveCustomHook = (binding, call) => {
|
|
@@ -1106,7 +1152,9 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1106
1152
|
if (hook.context) {
|
|
1107
1153
|
if (names.has(setter) && !names.has(state)) throw sourceNodeError(node.name, sourceFile, `Relative Context setter ${JSON.stringify(setter)} requires state ${JSON.stringify(state)} to be destructured`)
|
|
1108
1154
|
if (!names.has(state) && !requiredContextStates.has(state)) continue
|
|
1109
|
-
|
|
1155
|
+
const localSetter = names.has(setter) || requiredContextStates.has(state) ? setter : `__kContextState_${state}`
|
|
1156
|
+
setters.set(localSetter, state)
|
|
1157
|
+
registerState(owner, state, localSetter, "context", node, hook.stateOwner)
|
|
1110
1158
|
if (requiredContextStates.has(state)) {
|
|
1111
1159
|
const fields = customHookPrivateFields.get(node) ?? []
|
|
1112
1160
|
for (const field of [state, setter]) {
|
|
@@ -1121,13 +1169,17 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1121
1169
|
}
|
|
1122
1170
|
if (hook.privateStates.has(state)) {
|
|
1123
1171
|
setters.set(setter, state)
|
|
1172
|
+
registerState(owner, state, setter, "custom-hook", node)
|
|
1124
1173
|
const fields = customHookPrivateFields.get(node) ?? []
|
|
1125
1174
|
fields.push(state, setter)
|
|
1126
1175
|
customHookPrivateFields.set(node, fields)
|
|
1127
1176
|
continue
|
|
1128
1177
|
}
|
|
1129
1178
|
if (names.has(setter) !== names.has(state)) throw sourceNodeError(node.name, sourceFile, `Relative custom hook state ${JSON.stringify(state)} and setter ${JSON.stringify(setter)} must be destructured together`)
|
|
1130
|
-
if (names.has(setter))
|
|
1179
|
+
if (names.has(setter)) {
|
|
1180
|
+
setters.set(setter, state)
|
|
1181
|
+
registerState(owner, state, setter, "custom-hook", node)
|
|
1182
|
+
}
|
|
1131
1183
|
}
|
|
1132
1184
|
settersByFunction.set(owner, setters)
|
|
1133
1185
|
for (const name of names) {
|
|
@@ -1154,9 +1206,14 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1154
1206
|
const owner = nearestFunction(node)
|
|
1155
1207
|
if (!owner) throw sourceNodeError(node, sourceFile, "Zustand stores cannot be used outside a Kudzu component")
|
|
1156
1208
|
const setters = settersByFunction.get(owner) ?? new Map()
|
|
1157
|
-
if (selected === store.field)
|
|
1209
|
+
if (selected === store.field) {
|
|
1210
|
+
const setter = `__kStoreState_${node.name.text}`
|
|
1211
|
+
setters.set(setter, node.name.text)
|
|
1212
|
+
registerState(owner, node.name.text, setter, "store", node)
|
|
1213
|
+
}
|
|
1158
1214
|
else if (store.actions.has(selected)) {
|
|
1159
1215
|
setters.set(node.name.text, node.name.text)
|
|
1216
|
+
registerState(owner, node.name.text, node.name.text, "store-action", node)
|
|
1160
1217
|
const reducers = reducersByFunction.get(owner) ?? new Map()
|
|
1161
1218
|
reducers.set(node.name.text, { state: node.name.text, store, action: selected })
|
|
1162
1219
|
reducersByFunction.set(owner, reducers)
|
|
@@ -1183,6 +1240,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1183
1240
|
if (!owner) throw sourceNodeError(node, sourceFile, "useReducer() cannot be used outside a Kudzu component")
|
|
1184
1241
|
const setters = settersByFunction.get(owner) ?? new Map()
|
|
1185
1242
|
setters.set(dispatchElement.name.text, stateElement.name.text)
|
|
1243
|
+
registerState(owner, stateElement.name.text, dispatchElement.name.text, "reducer", node)
|
|
1186
1244
|
settersByFunction.set(owner, setters)
|
|
1187
1245
|
const reducers = reducersByFunction.get(owner) ?? new Map()
|
|
1188
1246
|
reducers.set(dispatchElement.name.text, { state: stateElement.name.text, reducer: reducer.text, import: reducerImport })
|
|
@@ -1195,6 +1253,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1195
1253
|
if (owner) {
|
|
1196
1254
|
const setters = settersByFunction.get(owner) ?? new Map()
|
|
1197
1255
|
setters.set(setterElement.name.text, stateElement.name.text)
|
|
1256
|
+
registerState(owner, stateElement.name.text, setterElement.name.text, "state", node)
|
|
1198
1257
|
settersByFunction.set(owner, setters)
|
|
1199
1258
|
const localSetters = localStateSettersByFunction.get(owner) ?? new Set()
|
|
1200
1259
|
localSetters.add(setterElement.name.text)
|
|
@@ -1205,11 +1264,28 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1205
1264
|
}
|
|
1206
1265
|
if (ts.isFunctionDeclaration(node) && node.name) {
|
|
1207
1266
|
functions.set(node.name.text, node)
|
|
1208
|
-
if (node.parent === sourceFile)
|
|
1267
|
+
if (node.parent === sourceFile) {
|
|
1268
|
+
components.set(node.name.text, { function: node, declaration: node })
|
|
1269
|
+
ensureOwner(node)
|
|
1270
|
+
}
|
|
1209
1271
|
}
|
|
1210
1272
|
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && (ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer))) {
|
|
1211
1273
|
functions.set(node.name.text, node.initializer)
|
|
1212
|
-
if (node.parent?.parent?.parent === sourceFile)
|
|
1274
|
+
if (node.parent?.parent?.parent === sourceFile) {
|
|
1275
|
+
components.set(node.name.text, { function: node.initializer, declaration: node })
|
|
1276
|
+
ensureOwner(node.initializer)
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isCallExpression(node.initializer) && ts.isIdentifier(node.initializer.expression)) {
|
|
1280
|
+
const owner = nearestFunction(node)
|
|
1281
|
+
if (owner && node.initializer.expression.text === "useRef" && node.initializer.arguments.length === 1 && node.initializer.arguments[0].kind === ts.SyntaxKind.NullKeyword) {
|
|
1282
|
+
ensureOwner(owner)
|
|
1283
|
+
componentAnalysis.registerRef(owner, { name: node.name.text, source: analysisSource(node) })
|
|
1284
|
+
}
|
|
1285
|
+
if (owner && node.initializer.expression.text === "useId" && node.initializer.arguments.length === 0) {
|
|
1286
|
+
ensureOwner(owner)
|
|
1287
|
+
componentAnalysis.registerId(owner, { name: node.name.text, source: analysisSource(node) })
|
|
1288
|
+
}
|
|
1213
1289
|
}
|
|
1214
1290
|
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isCallExpression(node.initializer) && ts.isIdentifier(node.initializer.expression) && node.initializer.expression.text === "createContext") contexts.add(node.name.text)
|
|
1215
1291
|
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && isLocalConst(node)) {
|
|
@@ -1378,6 +1454,38 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1378
1454
|
const stateBackedComponentFunctions = new WeakSet()
|
|
1379
1455
|
const stateBackedComponentRoots = []
|
|
1380
1456
|
let specializedImportIndex = 0
|
|
1457
|
+
const specialize = (call, component, label = "Keyed list", allowComponentRoot = false, ordinaryHooks = false, ordinaryStateNames = new Set(), ownership) => {
|
|
1458
|
+
const result = specializeComponentCall(call, component, sourceFile, factory, context, fail, label, allowComponentRoot, ordinaryHooks, ordinaryStateNames)
|
|
1459
|
+
const owner = nearestFunction(call)
|
|
1460
|
+
const setters = ownership?.setters ?? settersForNode(call, settersByFunction)
|
|
1461
|
+
const stateOwners = ownership?.stateOwners ?? stateOwnersForNode(call)
|
|
1462
|
+
const callbacks = functionsForNode(call)
|
|
1463
|
+
const propSignals = expression => {
|
|
1464
|
+
const signals = new Set()
|
|
1465
|
+
if (ts.isIdentifier(expression) && setters.has(expression.text)) signals.add(setters.get(expression.text))
|
|
1466
|
+
const callback = ts.isIdentifier(expression) ? callbacks.get(expression.text) : undefined
|
|
1467
|
+
for (const state of referencedStateNames((callback ?? expression).body ?? callback ?? expression, setters, callback ?? expression)) signals.add(state)
|
|
1468
|
+
return [...signals].map(name => ({ name, owner: stateOwners.get(name) ?? (owner ? `owner:${ensureOwner(owner).slot}` : "module") }))
|
|
1469
|
+
}
|
|
1470
|
+
result.analysis = componentAnalysis.registerSpecialization({
|
|
1471
|
+
kind: label,
|
|
1472
|
+
...(owner ? { owner: ensureOwner(owner).slot } : {}),
|
|
1473
|
+
...(analysisSource(call) ? { source: analysisSource(call) } : {}),
|
|
1474
|
+
props: result.props.map(prop => {
|
|
1475
|
+
const expression = result.propExpressions.get(prop.name)
|
|
1476
|
+
const signals = expression ? propSignals(expression) : []
|
|
1477
|
+
return { ...prop, ...(signals.length ? { signals } : {}) }
|
|
1478
|
+
}),
|
|
1479
|
+
states: [
|
|
1480
|
+
...result.rowStates.map(({ state, setter, source }) => ({ name: state, setter, kind: "row", ...(analysisSource(source) ? { source: analysisSource(source) } : {}) })),
|
|
1481
|
+
...result.ordinaryStates.map(({ state, setter, source }) => ({ name: state, setter, kind: "component", ...(analysisSource(source) ? { source: analysisSource(source) } : {}) }))
|
|
1482
|
+
],
|
|
1483
|
+
refs: [...result.rowRefs.map(({ name, source }) => ({ name, kind: "row", ...(analysisSource(source) ? { source: analysisSource(source) } : {}) })), ...result.ordinaryRefs.map(({ name, source }) => ({ name, kind: "component", ...(analysisSource(source) ? { source: analysisSource(source) } : {}) }))],
|
|
1484
|
+
ids: result.ordinaryIds.map(({ name, source }) => ({ name, ...(analysisSource(source) ? { source: analysisSource(source) } : {}) }))
|
|
1485
|
+
})
|
|
1486
|
+
for (const state of [...result.rowStates, ...result.ordinaryStates]) state.analysisOwner = `specialization:${result.analysis.slot}`
|
|
1487
|
+
return result
|
|
1488
|
+
}
|
|
1381
1489
|
const registerRowHooks = (call, specialization) => {
|
|
1382
1490
|
if (!specialization.rowStates.length && !specialization.rowRefs.length) return
|
|
1383
1491
|
let owner
|
|
@@ -1389,8 +1497,13 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1389
1497
|
}
|
|
1390
1498
|
if (!owner) owner = nearestFunction(call)
|
|
1391
1499
|
const setters = new Map(settersByFunction.get(owner))
|
|
1392
|
-
|
|
1500
|
+
const stateOwners = new Map(stateOwnersByFunction.get(owner))
|
|
1501
|
+
for (const state of specialization.rowStates) {
|
|
1502
|
+
setters.set(state.setter, state.state)
|
|
1503
|
+
stateOwners.set(state.state, state.analysisOwner)
|
|
1504
|
+
}
|
|
1393
1505
|
settersByFunction.set(owner, setters)
|
|
1506
|
+
stateOwnersByFunction.set(owner, stateOwners)
|
|
1394
1507
|
rowHookCalls.push(call)
|
|
1395
1508
|
usesRowState ||= specialization.rowStates.length > 0
|
|
1396
1509
|
usesRowRef ||= specialization.rowRefs.length > 0
|
|
@@ -1444,7 +1557,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1444
1557
|
fail(nestedCalls[0], "Reducer callback props require a component imported from a relative TypeScript module")
|
|
1445
1558
|
}
|
|
1446
1559
|
for (const nestedCall of nestedCalls) {
|
|
1447
|
-
const nested =
|
|
1560
|
+
const nested = specialize(nestedCall, nestedComponent, "Reducer-callback")
|
|
1448
1561
|
if (nested.effects.length) fail(nestedCall, "Reducer-callback components cannot declare effects")
|
|
1449
1562
|
nested.root = mergeSpecializedImports(nested.root, nestedComponent.getSourceFile(), nestedCall, nested.effects)
|
|
1450
1563
|
synthesizeTree(nested.root)
|
|
@@ -1483,7 +1596,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1483
1596
|
folded.parent = root.parent
|
|
1484
1597
|
return folded
|
|
1485
1598
|
}
|
|
1486
|
-
const expandSetterComponents = (root, componentSource, trail, aggregate, parentSetters) => {
|
|
1599
|
+
const expandSetterComponents = (root, componentSource, trail, aggregate, parentSetters, parentStateOwners) => {
|
|
1487
1600
|
root = foldSetterStaticConditions(root)
|
|
1488
1601
|
const replacements = new WeakMap()
|
|
1489
1602
|
let count = 0
|
|
@@ -1517,10 +1630,12 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1517
1630
|
}
|
|
1518
1631
|
const setters = new Map(parentSetters)
|
|
1519
1632
|
for (const state of aggregate.ordinaryStates) setters.set(state.setter, state.state)
|
|
1633
|
+
const stateOwners = new Map(parentStateOwners)
|
|
1634
|
+
for (const state of aggregate.ordinaryStates) stateOwners.set(state.state, state.analysisOwner)
|
|
1520
1635
|
if (jsxSetterCallbackProps(node, setters, functionsForNode(node), reducersForNode(node, reducersByFunction)).length) fail(node, "Setter callbacks cannot cross a second component boundary")
|
|
1521
|
-
const nested =
|
|
1636
|
+
const nested = specialize(node, component, "Nested setter-callback", true, true, new Set(setters.values()), { setters, stateOwners })
|
|
1522
1637
|
if (dynamic && (nested.hookDeclarations.length || nested.effects.length)) fail(node, "Hookful nested setter-callback components require an unconditional or statically truthy render path")
|
|
1523
|
-
nested.root = expandSetterComponents(nested.root, component.getSourceFile(), [...trail, component], nested, setters)
|
|
1638
|
+
nested.root = expandSetterComponents(nested.root, component.getSourceFile(), [...trail, component], nested, setters, stateOwners)
|
|
1524
1639
|
if (imported) synthesizeTree(nested.root = mergeSpecializedImports(nested.root, component.getSourceFile(), node, nested.effects))
|
|
1525
1640
|
aggregate.calculations.push(...nested.calculations)
|
|
1526
1641
|
aggregate.effects.push(...nested.effects)
|
|
@@ -1549,7 +1664,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1549
1664
|
if (identifierReferenceCount(sourceFile, name) !== calls.length) fail(component.declaration, `State-backed list component ${name} may only be referenced as JSX`)
|
|
1550
1665
|
if (stateBackedCalls.length !== calls.length) fail(component.declaration, `State-backed list component ${name} must receive its mapped prop from local state at every call`)
|
|
1551
1666
|
for (const call of stateBackedCalls) {
|
|
1552
|
-
const specialization =
|
|
1667
|
+
const specialization = specialize(call, component.function)
|
|
1553
1668
|
if (specialization.effects.length) fail(call, "State-backed list components cannot declare effects")
|
|
1554
1669
|
componentSpecializations.set(call, specialization)
|
|
1555
1670
|
stateBackedComponentRoots.push(specialization.root)
|
|
@@ -1571,7 +1686,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1571
1686
|
}
|
|
1572
1687
|
const stateBackedCalls = calls.filter(call => isStateBackedListComponentCall(call, component, settersByFunction.get(nearestFunction(call)) ?? new Map()))
|
|
1573
1688
|
for (const call of stateBackedCalls) {
|
|
1574
|
-
const specialization =
|
|
1689
|
+
const specialization = specialize(call, component)
|
|
1575
1690
|
if (specialization.effects.length) fail(call, "State-backed list components cannot declare effects")
|
|
1576
1691
|
componentSpecializations.set(call, specialization)
|
|
1577
1692
|
stateBackedComponentRoots.push(specialization.root)
|
|
@@ -1591,7 +1706,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1591
1706
|
collectReferences(component.body)
|
|
1592
1707
|
if (references.length !== 1) fail(element, `Setter-callback prop ${JSON.stringify(prop)} must be used exactly once in the component`)
|
|
1593
1708
|
}
|
|
1594
|
-
const specialization =
|
|
1709
|
+
const specialization = specialize(call, component, "Setter-callback", false, true, new Set(settersForNode(call, settersByFunction).values()))
|
|
1595
1710
|
if (specialization.hookDeclarations.length || specialization.effects.length) {
|
|
1596
1711
|
const substitutions = new Map()
|
|
1597
1712
|
const attributes = ts.isJsxElement(call) ? call.openingElement.attributes : call.attributes
|
|
@@ -1605,7 +1720,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1605
1720
|
for (const effect of specialization.effects) effect.call = substituteClone(effect.call, substitutions, factory, context)
|
|
1606
1721
|
}
|
|
1607
1722
|
}
|
|
1608
|
-
specialization.root = expandSetterComponents(specialization.root, component.getSourceFile(), [component], specialization, settersForNode(call, settersByFunction))
|
|
1723
|
+
specialization.root = expandSetterComponents(specialization.root, component.getSourceFile(), [component], specialization, settersForNode(call, settersByFunction), stateOwnersForNode(call))
|
|
1609
1724
|
if (imported) synthesizeTree(specialization.root = mergeSpecializedImports(specialization.root, component.getSourceFile(), call, specialization.effects))
|
|
1610
1725
|
if (specialization.hookDeclarations.length || specialization.effects.length) {
|
|
1611
1726
|
const owner = nearestFunction(call)
|
|
@@ -1634,6 +1749,9 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1634
1749
|
const setters = new Map(settersForNode(call, settersByFunction))
|
|
1635
1750
|
for (const state of specialization.ordinaryStates) setters.set(state.setter, state.state)
|
|
1636
1751
|
settersByFunction.set(helper, setters)
|
|
1752
|
+
const stateOwners = new Map(stateOwnersForNode(call))
|
|
1753
|
+
for (const state of specialization.ordinaryStates) stateOwners.set(state.state, state.analysisOwner)
|
|
1754
|
+
stateOwnersByFunction.set(helper, stateOwners)
|
|
1637
1755
|
usesComponentState ||= specialization.ordinaryStates.length > 0
|
|
1638
1756
|
usesComponentId ||= specialization.usesComponentId
|
|
1639
1757
|
usesComponentRef ||= specialization.ordinaryRefs.length > 0
|
|
@@ -1673,7 +1791,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1673
1791
|
if (dispatchCalls.length !== calls.length) fail(component.declaration, `Reducer-dispatch component ${name} must receive a direct local reducer dispatch at every call`)
|
|
1674
1792
|
for (const call of dispatchCalls) {
|
|
1675
1793
|
if (componentSpecializations.has(call)) fail(call, "Reducer dispatch props cannot be combined with another component specialization")
|
|
1676
|
-
const specialization =
|
|
1794
|
+
const specialization = specialize(call, component.function, "Reducer-dispatch")
|
|
1677
1795
|
registerRowHooks(call, specialization)
|
|
1678
1796
|
specialization.root = expandReducerCallbacks(specialization.root, component.function.getSourceFile(), call)
|
|
1679
1797
|
componentSpecializations.set(call, specialization)
|
|
@@ -1696,7 +1814,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1696
1814
|
const componentSource = component.getSourceFile()
|
|
1697
1815
|
for (const call of dispatchCalls) {
|
|
1698
1816
|
if (componentSpecializations.has(call)) fail(call, "Reducer dispatch props cannot be combined with another component specialization")
|
|
1699
|
-
const specialization =
|
|
1817
|
+
const specialization = specialize(call, component, "Reducer-dispatch")
|
|
1700
1818
|
registerRowHooks(call, specialization)
|
|
1701
1819
|
specialization.root = expandReducerCallbacks(specialization.root, componentSource, call)
|
|
1702
1820
|
specialization.root = mergeSpecializedImports(specialization.root, componentSource, call, specialization.effects)
|
|
@@ -1779,7 +1897,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1779
1897
|
for (const call of calls) {
|
|
1780
1898
|
const specialization = reducerComponentCalls.has(call)
|
|
1781
1899
|
? componentSpecializations.get(call)
|
|
1782
|
-
:
|
|
1900
|
+
: specialize(call, component.function, "Keyed list", true)
|
|
1783
1901
|
registerRowHooks(call, specialization)
|
|
1784
1902
|
if (specialization.effects.length && !keyedComponentCalls.has(call)) fail(call, "Effectful keyed row components may only be used directly as keyed map rows")
|
|
1785
1903
|
specialization.component = component.function
|
|
@@ -1815,7 +1933,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1815
1933
|
const chain = [...trail, component].map(entry => entry.name?.text || "anonymous").join(" -> ")
|
|
1816
1934
|
fail(node, `Keyed list component cycle: ${chain}`)
|
|
1817
1935
|
}
|
|
1818
|
-
const specialization =
|
|
1936
|
+
const specialization = specialize(node, component, "Keyed list", true)
|
|
1819
1937
|
registerRowHooks(node, specialization)
|
|
1820
1938
|
specialization.root = expandKeyedComponents(specialization.root, component.getSourceFile(), [...trail, component], specialization)
|
|
1821
1939
|
if (imported) synthesizeTree(specialization.root = mergeSpecializedImports(specialization.root, component.getSourceFile(), node, specialization.effects))
|
|
@@ -1860,7 +1978,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1860
1978
|
}
|
|
1861
1979
|
for (const { node, parts: originalParts } of rawRenderedLists) {
|
|
1862
1980
|
if (keyedListParentTag(node) === "table") throw new Error("Keyed table rows must be wrapped in <tbody>, <thead>, or <tfoot>")
|
|
1863
|
-
const specialization = componentSpecializations.get(originalParts.root) ?? { root: originalParts.root, calculations: [], effects: [], hookDeclarations: [], rowStates: [], rowRefs: [] }
|
|
1981
|
+
const specialization = componentSpecializations.get(originalParts.root) ?? { root: originalParts.root, calculations: [], effects: [], hookDeclarations: [], rowStates: [], rowRefs: [], ordinaryStates: [] }
|
|
1864
1982
|
const componentSource = specialization.componentSource ?? sourceFile
|
|
1865
1983
|
specialization.root = expandKeyedComponents(specialization.root, componentSource, specialization.component ? [specialization.component] : [], specialization)
|
|
1866
1984
|
if (specialization.imported) synthesizeTree(specialization.root = mergeSpecializedImports(specialization.root, componentSource, originalParts.root, specialization.effects))
|
|
@@ -1880,7 +1998,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1880
1998
|
callback.parent = originalParts.callback.parent
|
|
1881
1999
|
}
|
|
1882
2000
|
callback = prepareListCallback(callback, root, specialization, originalParts.item)
|
|
1883
|
-
const parts = { ...originalParts, root, callback }
|
|
2001
|
+
const parts = { ...originalParts, root, callback, analysisStateOwners: new Map([...stateOwnersForNode(originalParts.root), ...[...specialization.rowStates, ...specialization.ordinaryStates].map(state => [state.state, state.analysisOwner])]) }
|
|
1884
2002
|
for (const calculation of specialization.calculations) {
|
|
1885
2003
|
ts.setParentRecursive(calculation, false)
|
|
1886
2004
|
calculation.parent = callback
|
|
@@ -1908,6 +2026,14 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1908
2026
|
)
|
|
1909
2027
|
}
|
|
1910
2028
|
|
|
2029
|
+
let activeStateOwners
|
|
2030
|
+
const visitWithStateOwners = (node, stateOwners) => {
|
|
2031
|
+
const previous = activeStateOwners
|
|
2032
|
+
activeStateOwners = new Map([...(previous ?? []), ...stateOwners])
|
|
2033
|
+
const result = ts.visitNode(node, visitor)
|
|
2034
|
+
activeStateOwners = previous
|
|
2035
|
+
return result
|
|
2036
|
+
}
|
|
1911
2037
|
const visitor = node => {
|
|
1912
2038
|
if (ts.isVariableDeclaration(node) && ts.isObjectBindingPattern(node.name) && customHookPrivateFields.has(node)) {
|
|
1913
2039
|
const privateFields = customHookPrivateFields.get(node)
|
|
@@ -1920,7 +2046,11 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1920
2046
|
return ts.visitEachChild(factory.updateBlock(node, [...setterHookHelpers.get(node), ...node.statements]), visitor, context)
|
|
1921
2047
|
}
|
|
1922
2048
|
if (specializedDeclarations.has(node)) return node
|
|
1923
|
-
if (componentSpecializations.has(node))
|
|
2049
|
+
if (componentSpecializations.has(node)) {
|
|
2050
|
+
const specialization = componentSpecializations.get(node)
|
|
2051
|
+
const stateOwners = new Map([...stateOwnersForNode(node), ...[...specialization.rowStates, ...specialization.ordinaryStates].map(state => [state.state, state.analysisOwner])])
|
|
2052
|
+
return visitWithStateOwners(specialization.root, stateOwners)
|
|
2053
|
+
}
|
|
1924
2054
|
|
|
1925
2055
|
if (hasLinkElements && (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) && isStylesheetLink(node)) {
|
|
1926
2056
|
fail(node, "Stylesheets must be placed under src/ or declared in kudzu.config styles so Kudzu can emit them in <head>")
|
|
@@ -1990,7 +2120,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1990
2120
|
}
|
|
1991
2121
|
const invalidDependency = ordinaryDependencies.find(dependency => !ts.isIdentifier(dependency))
|
|
1992
2122
|
if (invalidDependency) effectFail(invalidDependency, "useEffect() dependencies must be direct state or runtime parameter identifiers")
|
|
1993
|
-
const
|
|
2123
|
+
const dependencyDerived = []
|
|
1994
2124
|
const dependencyStates = new Map()
|
|
1995
2125
|
const dependencySubstitutions = new Map()
|
|
1996
2126
|
const subscriptionDependencies = []
|
|
@@ -2006,7 +2136,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
2006
2136
|
const usedStates = new Set()
|
|
2007
2137
|
const expression = collectionExpression(initializer, { fail: effectFail, stateNames, selectorStates: usedStates })
|
|
2008
2138
|
if (!usedStates.size) effectFail(dependency, `useEffect() derived dependency "${dependency.text}" must read direct primitive state`)
|
|
2009
|
-
|
|
2139
|
+
dependencyDerived.push({ expression, states: usedStates, source: initializer })
|
|
2010
2140
|
for (const name of usedStates) {
|
|
2011
2141
|
subscriptionDependencies.push(factory.createIdentifier(name))
|
|
2012
2142
|
dependencyStates.set(name, factory.createIdentifier(name))
|
|
@@ -2015,12 +2145,12 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
2015
2145
|
hasDerivedDependency = true
|
|
2016
2146
|
} else {
|
|
2017
2147
|
subscriptionDependencies.push(dependency)
|
|
2018
|
-
|
|
2148
|
+
dependencyDerived.push({ expression: ["state", dependency.text], states: [dependency.text], source: dependency })
|
|
2019
2149
|
dependencyStates.set(dependency.text, dependency)
|
|
2020
2150
|
}
|
|
2021
2151
|
}
|
|
2022
2152
|
if (!hasDerivedDependency) {
|
|
2023
|
-
|
|
2153
|
+
dependencyDerived.length = 0
|
|
2024
2154
|
dependencyStates.clear()
|
|
2025
2155
|
}
|
|
2026
2156
|
if (!effectOwner) fail(node, "useEffect() cannot be used outside a Kudzu component")
|
|
@@ -2072,6 +2202,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
2072
2202
|
for (const reference of workerReferences.slice(workerStart)) Object.assign(reference, { module: handlerUrl, handler: descriptor.exportName })
|
|
2073
2203
|
usesListItem ||= Boolean(itemDependencies.length && !listEffect)
|
|
2074
2204
|
usesBehavior = true
|
|
2205
|
+
const derivedDependencies = hasDerivedDependency ? dependencyDerived.map(entry => descriptors.registerDerived("expression", entry.expression, entry.states, entry.source)) : []
|
|
2075
2206
|
return factory.updateCallExpression(node, node.expression, node.typeArguments, [
|
|
2076
2207
|
callback,
|
|
2077
2208
|
factory.createArrayLiteralExpression(hasDerivedDependency ? subscriptionDependencies : ordinaryDependencies),
|
|
@@ -2082,7 +2213,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
2082
2213
|
factory.createStringLiteral(specializedEffect ? sourceLocation(specializedEffect.source, specializedEffect.sourceFile) : sourceLocation(node, sourceFile)),
|
|
2083
2214
|
returns.cleanup ? factory.createTrue() : factory.createFalse(),
|
|
2084
2215
|
factory.createArrayLiteralExpression(itemDependencies.map(field => factory.createStringLiteral(field))),
|
|
2085
|
-
hasDerivedDependency ? jsonExpression(
|
|
2216
|
+
hasDerivedDependency ? jsonExpression(derivedDependencies.map(entry => entry.expression), factory) : factory.createArrayLiteralExpression(),
|
|
2086
2217
|
factory.createArrayLiteralExpression([...dependencyStates].map(([name, state]) => factory.createArrayLiteralExpression([factory.createStringLiteral(name), state])))
|
|
2087
2218
|
])
|
|
2088
2219
|
}
|
|
@@ -2150,12 +2281,13 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
2150
2281
|
usesBinding = true
|
|
2151
2282
|
listSource = descriptors.compileReactiveBinding(listParts.calculation, { setters: settersForNode(node, settersByFunction), importBindings })
|
|
2152
2283
|
}
|
|
2284
|
+
const selector = listParts.selector?.length ? descriptors.registerDerived("selector", listParts.selector, listParts.selectorStates, node).selector : listParts.selector ?? []
|
|
2153
2285
|
const arguments_ = [
|
|
2154
2286
|
listSource,
|
|
2155
2287
|
listParts.keyField === null ? factory.createNull() : factory.createStringLiteral(listParts.keyField),
|
|
2156
|
-
|
|
2288
|
+
visitWithStateOwners(listParts.callback, listParts.analysisStateOwners ?? new Map()),
|
|
2157
2289
|
factory.createStringLiteral(listParts.ownerField ?? ""),
|
|
2158
|
-
jsonExpression(
|
|
2290
|
+
jsonExpression(selector, factory),
|
|
2159
2291
|
listParts.indexed ? factory.createTrue() : factory.createFalse()
|
|
2160
2292
|
]
|
|
2161
2293
|
if (listParts.selectorStates?.size || listParts.static) arguments_.push(factory.createArrayLiteralExpression([...(listParts.selectorStates ?? [])].map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), factory.createIdentifier(name)]))))
|
|
@@ -2195,6 +2327,8 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
2195
2327
|
if (ts.isJsxAttribute(node) && node.initializer && ts.isJsxExpression(node.initializer) && node.initializer.expression && /^on[A-Z]/.test(node.name.text)) {
|
|
2196
2328
|
const setters = settersForNode(node, settersByFunction)
|
|
2197
2329
|
const event = descriptors.compileEvent(node.initializer.expression, {
|
|
2330
|
+
owner: fallbackOwner(node),
|
|
2331
|
+
stateOwners: activeStateOwners ?? stateOwnersForNode(node),
|
|
2198
2332
|
setters,
|
|
2199
2333
|
reducers: reducersForNode(node, reducersByFunction),
|
|
2200
2334
|
functions: functionsForNode(node),
|
|
@@ -2214,10 +2348,11 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
2214
2348
|
}
|
|
2215
2349
|
|
|
2216
2350
|
const transformed = ts.visitNode(sourceFile, visitor)
|
|
2351
|
+
descriptors.finalize()
|
|
2217
2352
|
if (!usesBehavior) return transformed
|
|
2218
2353
|
|
|
2219
2354
|
const behaviorImports = [factory.createImportSpecifier(false, factory.createIdentifier("behavior"), factory.createIdentifier("__kBehavior"))]
|
|
2220
|
-
if (
|
|
2355
|
+
if (moduleIR.handlers.some(handler => handler.kind === "module-export" && handler.role === "native")) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("nativeBehavior"), factory.createIdentifier("__kNativeBehavior")))
|
|
2221
2356
|
if (usesBinding) {
|
|
2222
2357
|
behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("binding"), factory.createIdentifier("__kBinding")))
|
|
2223
2358
|
behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("select"), factory.createIdentifier("__kSelect")))
|
|
@@ -2476,7 +2611,8 @@ function validateKeyedList(parts, sourceFile, listValues, listEventItems, listCo
|
|
|
2476
2611
|
callback.parent = nested.callback.parent
|
|
2477
2612
|
}
|
|
2478
2613
|
callback = prepareListCallback(callback, root, specialization ?? { hookDeclarations: [], effects: [] }, nested.item)
|
|
2479
|
-
const
|
|
2614
|
+
const specializedStates = [...(specialization?.rowStates ?? []), ...(specialization?.ordinaryStates ?? [])]
|
|
2615
|
+
const nestedParts = { ...nested, root, callback, state: parts.state, nested: true, analysisStateOwners: new Map([...(parts.analysisStateOwners ?? []), ...specializedStates.map(state => [state.state, state.analysisOwner])]) }
|
|
2480
2616
|
for (const calculation of specialization?.calculations ?? []) {
|
|
2481
2617
|
ts.setParentRecursive(calculation, false)
|
|
2482
2618
|
calculation.parent = callback
|
|
@@ -2704,6 +2840,13 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
|
|
|
2704
2840
|
}
|
|
2705
2841
|
const restEntries = [...props].filter(([prop]) => !acceptedProps.has(prop))
|
|
2706
2842
|
if (!rest) for (const [prop] of restEntries) fail(call, `Unknown ${label.toLowerCase()} component prop "${prop}"`)
|
|
2843
|
+
const propAnalysis = elements.map(element => ({
|
|
2844
|
+
name: (element.propertyName ?? element.name).getText(),
|
|
2845
|
+
local: element.name.getText(),
|
|
2846
|
+
provided: element.dotDotDotToken ? restEntries.length > 0 : props.has((element.propertyName ?? element.name).text),
|
|
2847
|
+
...(element.dotDotDotToken ? { rest: true } : {}),
|
|
2848
|
+
...(element.initializer ? { hasDefault: true, defaultApplied: !props.has((element.propertyName ?? element.name).text) } : {})
|
|
2849
|
+
}))
|
|
2707
2850
|
|
|
2708
2851
|
let returned
|
|
2709
2852
|
const calculations = []
|
|
@@ -2713,7 +2856,7 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
|
|
|
2713
2856
|
const rowRefs = []
|
|
2714
2857
|
const ordinaryStates = []
|
|
2715
2858
|
const ordinaryRefs = []
|
|
2716
|
-
|
|
2859
|
+
const ordinaryIds = []
|
|
2717
2860
|
if (!ts.isBlock(component.body)) {
|
|
2718
2861
|
returned = component.body
|
|
2719
2862
|
} else {
|
|
@@ -2748,8 +2891,8 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
|
|
|
2748
2891
|
synthesizeTree(initialValue)
|
|
2749
2892
|
const initializer = factory.createCallExpression(factory.createIdentifier(ordinaryHooks ? "__kComponentUseState" : "__kRowUseState"), undefined, [initialValue])
|
|
2750
2893
|
hookDeclarations.push(factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(binding, undefined, undefined, initializer)], ts.NodeFlags.Const)))
|
|
2751
|
-
if (ordinaryHooks) ordinaryStates.push({ state, setter })
|
|
2752
|
-
else rowStates.push({ state, setter })
|
|
2894
|
+
if (ordinaryHooks) ordinaryStates.push({ state, setter, source: declaration })
|
|
2895
|
+
else rowStates.push({ state, setter, source: declaration })
|
|
2753
2896
|
continue
|
|
2754
2897
|
}
|
|
2755
2898
|
if (declaration.initializer && ts.isCallExpression(declaration.initializer) && ts.isIdentifier(declaration.initializer.expression) && declaration.initializer.expression.text === "useRef") {
|
|
@@ -2761,7 +2904,7 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
|
|
|
2761
2904
|
substitutions.set(declaration.name.text, factory.createIdentifier(name))
|
|
2762
2905
|
const initializer = factory.createCallExpression(factory.createIdentifier(ordinaryHooks ? "__kComponentUseRef" : "__kRowUseRef"), declaration.initializer.typeArguments?.map(type => cloneAst(type, factory, context)), [factory.createNull()])
|
|
2763
2906
|
hookDeclarations.push(factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(factory.createIdentifier(name), undefined, undefined, initializer)], ts.NodeFlags.Const)))
|
|
2764
|
-
refs.push({ name })
|
|
2907
|
+
refs.push({ name, source: declaration })
|
|
2765
2908
|
continue
|
|
2766
2909
|
}
|
|
2767
2910
|
if (declaration.initializer && ts.isCallExpression(declaration.initializer) && ts.isIdentifier(declaration.initializer.expression) && declaration.initializer.expression.text === "useId") {
|
|
@@ -2771,7 +2914,7 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
|
|
|
2771
2914
|
substitutions.set(declaration.name.text, factory.createIdentifier(name))
|
|
2772
2915
|
const initializer = factory.createCallExpression(factory.createIdentifier("__kComponentUseId"), undefined, [])
|
|
2773
2916
|
hookDeclarations.push(factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(factory.createIdentifier(name), undefined, undefined, initializer)], ts.NodeFlags.Const)))
|
|
2774
|
-
|
|
2917
|
+
ordinaryIds.push({ name, source: declaration })
|
|
2775
2918
|
continue
|
|
2776
2919
|
}
|
|
2777
2920
|
if (!ts.isIdentifier(declaration.name) || !declaration.initializer) fail(declaration, `${label} component locals must be initialized identifiers`)
|
|
@@ -2812,7 +2955,10 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
|
|
|
2812
2955
|
rowRefs,
|
|
2813
2956
|
ordinaryStates,
|
|
2814
2957
|
ordinaryRefs,
|
|
2815
|
-
|
|
2958
|
+
ordinaryIds,
|
|
2959
|
+
propExpressions: props,
|
|
2960
|
+
props: propAnalysis,
|
|
2961
|
+
usesComponentId: ordinaryIds.length > 0
|
|
2816
2962
|
}
|
|
2817
2963
|
}
|
|
2818
2964
|
|
|
@@ -3744,9 +3890,8 @@ const workerCompiler = createWorkerCompiler({
|
|
|
3744
3890
|
})
|
|
3745
3891
|
|
|
3746
3892
|
const printEffectEntry = createEffectCodegen({ assetPath, inlineJson, relativeModulePath })
|
|
3893
|
+
const handlerLowering = createHandlerLowering({ cloneAst, synthesizeTree })
|
|
3747
3894
|
const printHandlerModule = createHandlerCodegen({
|
|
3748
|
-
cloneAst,
|
|
3749
|
-
synthesizeTree,
|
|
3750
3895
|
resolveClientImport: (entry, handlerPath) => entry.package ? entry.target : relativeModulePath(handlerPath, clientModulePath(entry.target))
|
|
3751
3896
|
})
|
|
3752
3897
|
const { normalizeReactMigrationSyntax, validateUseIdSyntax } = createReactMigrationPass({ cloneAst, jsxTagName })
|