@kudzujs/core 0.8.61 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MIGRATION_ROADMAP.md +37 -1
- package/PERFORMANCE.md +79 -1
- package/README.md +2 -2
- package/RELEASES.md +58 -0
- package/bin/kudzu.mjs +10 -1
- package/docs/next-architecture/0.9-baseline.md +1199 -0
- package/docs/next-architecture/0.9-benchmark-contracts.md +507 -0
- package/docs/next-architecture/0.9-component-property-contract.md +89 -0
- package/docs/next-architecture/0.9-compression-ledger.md +227 -0
- package/docs/next-architecture/0.9-final-proof-audit.md +176 -0
- package/docs/next-architecture/0.9-implementation-plan.md +1819 -0
- package/docs/next-architecture/0.9-resource-lifecycle.md +118 -0
- package/docs/next-architecture/0.9-semantic-compression.md +384 -0
- package/docs/next-architecture/README.md +16 -12
- package/docs/next-architecture/compiler-current-architecture.md +7 -7
- package/docs/next-architecture/large-application-ai-native-roadmap.md +6 -4
- package/docs/next-architecture/versioning.md +3 -2
- package/framework/README.md +3 -1
- package/framework/binding-runtime.js +4 -4
- package/framework/build.mjs +135 -30
- package/framework/compiler/ast-helpers.mjs +5 -0
- package/framework/compiler/browser-signal-passes.mjs +2 -7
- package/framework/compiler/collection-analysis.mjs +4 -0
- package/framework/compiler/descriptor-session.mjs +36 -12
- package/framework/compiler/effect-analysis.mjs +28 -8
- package/framework/compiler/effect-codegen.mjs +79 -36
- package/framework/compiler/effect-private-ref-pass.mjs +4 -8
- package/framework/compiler/handler-lowering.mjs +12 -7
- package/framework/compiler/ir/module-ir.mjs +26 -4
- package/framework/compiler/list-runtime-codegen.mjs +4 -2
- package/framework/compiler/optimize/command-specialization.mjs +4 -7
- package/framework/compiler/outside-click-pass.mjs +79 -0
- package/framework/compiler/react-migration-pass.mjs +27 -1
- package/framework/compiler/route-artifact-report.mjs +4 -3
- package/framework/compiler/route-build-record.mjs +12 -0
- package/framework/compiler/route-capability-planner.mjs +3 -3
- package/framework/compiler/route-ir.mjs +27 -11
- package/framework/compiler/runtime-codegen.mjs +2 -2
- package/framework/compiler/source-compiler.mjs +407 -74
- package/framework/core.d.ts +1 -0
- package/framework/core.mjs +18 -5
- package/framework/dependency-runtime.js +1 -1
- package/framework/effect-runtime.js +2 -2
- package/framework/list-runtime.js +67 -24
- package/framework/native-runtime.js +12 -9
- package/framework/runtime.js +1 -1
- package/framework/serialization.js +13 -6
- package/framework/shared-runtime.js +14 -12
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFile, realpath, stat } from "node:fs/promises"
|
|
2
2
|
import { dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path"
|
|
3
|
+
import { transform, transformSync } from "esbuild"
|
|
3
4
|
import ts from "typescript"
|
|
4
5
|
import { createBindingIndex } from "./analysis/binding-index.mjs"
|
|
5
6
|
import { createComponentAnalysisSession } from "./analysis/component-analysis.mjs"
|
|
@@ -13,6 +14,7 @@ import { analyzeEffectDependencies, validateEffectOwnedBrowserResources } from "
|
|
|
13
14
|
import { createHandlerCodegen } from "./handler-codegen.mjs"
|
|
14
15
|
import { createHandlerLowering } from "./handler-lowering.mjs"
|
|
15
16
|
import { registerSharedAction, registerSharedState } from "./ir/module-ir.mjs"
|
|
17
|
+
import { analyzeOutsideClickHook, normalizeOutsideClickHooks } from "./outside-click-pass.mjs"
|
|
16
18
|
import { createCommandSpecializer } from "./optimize/command-specialization.mjs"
|
|
17
19
|
import { applyNormalizationPasses } from "./normalization-pipeline.mjs"
|
|
18
20
|
import { assetPath, relativeModulePath, withBase } from "./path-helpers.mjs"
|
|
@@ -27,16 +29,24 @@ const { root, sourceDirectory, pagesDirectory, workDirectory, workerCompiler, mo
|
|
|
27
29
|
const buildDirectory = project.buildDirectory ?? workDirectory
|
|
28
30
|
const { ordinaryRuntimeDependencies, resolveSourceImport, runtimeModuleReference } = project.graph
|
|
29
31
|
const parseSourceFile = (file, source) => modules.read(file, source).sourceFile
|
|
32
|
+
const importFreeModules = new Map()
|
|
30
33
|
const staticAssetExtensions = new Set([".avif", ".gif", ".ico", ".jpeg", ".jpg", ".otf", ".png", ".svg", ".ttf", ".webp", ".woff", ".woff2"])
|
|
31
34
|
|
|
32
|
-
function
|
|
35
|
+
async function compileSourceAsync(file, sourceFiles, sourceIndex, staticFiles, cssModules, base) {
|
|
36
|
+
const source = sourceIndex.get(file)
|
|
37
|
+
const output = importFreeTypeScriptModule(file, source) ? (await transform(source, { loader: "ts", format: "esm", target: "es2022", sourcefile: file })).code : undefined
|
|
38
|
+
return compileSource(file, sourceFiles, sourceIndex, staticFiles, cssModules, base, output)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function compileSource(file, sourceFiles, sourceIndex, staticFiles, cssModules, base, importFreeOutput) {
|
|
33
42
|
const importedAssets = new Set()
|
|
34
43
|
const source = sourceIndex.get(file)
|
|
35
44
|
const semantic = createSemanticArtifact(relative(root, file).replaceAll(sep, "/"))
|
|
36
45
|
const handlerPath = `handlers/${relative(sourceDirectory, file).replaceAll(sep, "/").replace(/\.(?:ts|tsx)$/, ".js")}`
|
|
37
|
-
const
|
|
46
|
+
const importFreePlain = importFreeTypeScriptModule(file, source)
|
|
47
|
+
const plain = importFreePlain || plainTypeScriptModule(file, source, sourceFiles)
|
|
38
48
|
if (plain && counters) counters.plainModules = (counters.plainModules ?? 0) + 1
|
|
39
|
-
const result = ts.transpileModule(source, {
|
|
49
|
+
const result = importFreePlain ? { outputText: importFreeOutput ?? transformSync(source, { loader: "ts", format: "esm", target: "es2022", sourcefile: file }).code } : ts.transpileModule(source, {
|
|
40
50
|
fileName: file,
|
|
41
51
|
compilerOptions: {
|
|
42
52
|
target: ts.ScriptTarget.ES2022,
|
|
@@ -86,6 +96,15 @@ function plainTypeScriptModule(file, source, sourceFiles) {
|
|
|
86
96
|
return true
|
|
87
97
|
}
|
|
88
98
|
|
|
99
|
+
function importFreeTypeScriptModule(file, source) {
|
|
100
|
+
if (!file.endsWith(".ts")) return false
|
|
101
|
+
const cached = importFreeModules.get(file)
|
|
102
|
+
if (cached?.source === source) return cached.result
|
|
103
|
+
const result = !source.includes(".worker.ts") && ts.preProcessFile(source, true, true).importedFiles.length === 0
|
|
104
|
+
importFreeModules.set(file, { source, result })
|
|
105
|
+
return result
|
|
106
|
+
}
|
|
107
|
+
|
|
89
108
|
function createPlainModuleTransformer(file, sourceFiles) {
|
|
90
109
|
return context => sourceFile => context.factory.updateSourceFile(sourceFile, sourceFile.statements.map(statement => {
|
|
91
110
|
if ((!ts.isImportDeclaration(statement) && !ts.isExportDeclaration(statement)) || !runtimeModuleReference(statement) || !statement.moduleSpecifier || !ts.isStringLiteral(statement.moduleSpecifier) || !statement.moduleSpecifier.text.startsWith(".")) return statement
|
|
@@ -125,7 +144,9 @@ function reachableSourceFiles(entries, sourceFiles, sourceIndex) {
|
|
|
125
144
|
if (visited.has(file)) continue
|
|
126
145
|
visited.add(file)
|
|
127
146
|
reachable.add(file)
|
|
128
|
-
const
|
|
147
|
+
const source = sourceIndex.get(file)
|
|
148
|
+
if (importFreeTypeScriptModule(file, source)) continue
|
|
149
|
+
const sourceFile = parseSourceFile(file, source)
|
|
129
150
|
if (owner === "ordinary") for (const target of ordinaryRuntimeDependencies(file, sourceFile, sourceFiles, isStaticImport)) queue.push({ file: target, owner: target.endsWith(".worker.ts") ? "worker" : owner })
|
|
130
151
|
const visit = node => {
|
|
131
152
|
if (owner === "worker") {
|
|
@@ -305,6 +326,7 @@ function normalizeCompilerSource(sourceFile, { base, context, file, importedColl
|
|
|
305
326
|
source => normalizeReactMigrationSyntax(source, factory, context, importedCollections ?? importedSerializableCollectionNames(source, file, sourceFiles, sourceIndex)),
|
|
306
327
|
source => normalizeNavigatorCapabilityConditions(source, factory, context),
|
|
307
328
|
source => normalizeParameterizedDebounceHooks(source, factory, context),
|
|
329
|
+
source => normalizeOutsideClickHooks(source, factory, context),
|
|
308
330
|
source => normalizeEffectPrivateRefs(source, factory, context),
|
|
309
331
|
source => {
|
|
310
332
|
const result = normalizeCustomHookTimerRefs(source, factory, context)
|
|
@@ -330,7 +352,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
330
352
|
const { moduleIR } = semantic
|
|
331
353
|
return context => sourceFile => {
|
|
332
354
|
const hasLinkElements = /<link/i.test(sourceFile.text)
|
|
333
|
-
const importedStaticCollections = importedSerializableCollections(sourceFile, file, sourceFiles, sourceIndex)
|
|
355
|
+
const importedStaticCollections = importedSerializableCollections(sourceFile, file, sourceFiles, sourceIndex, true)
|
|
334
356
|
const importedCollections = new Set(importedStaticCollections.keys())
|
|
335
357
|
const normalized = normalizeCompilerSource(sourceFile, { base, context, file, importedCollections, importedStaticCollections, sourceFiles, sourceIndex })
|
|
336
358
|
sourceFile = normalized.sourceFile
|
|
@@ -393,8 +415,16 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
393
415
|
}
|
|
394
416
|
const importedCollectionTransforms = new Map()
|
|
395
417
|
const importedCalculationFunctions = new Map()
|
|
418
|
+
const validatedEffectCalculations = new WeakSet()
|
|
419
|
+
const transformImports = new Set()
|
|
420
|
+
const collectTransformImports = node => {
|
|
421
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) transformImports.add(node.expression.text)
|
|
422
|
+
if ((ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) && ts.isIdentifier(node.tagName)) transformImports.add(node.tagName.text)
|
|
423
|
+
ts.forEachChild(node, collectTransformImports)
|
|
424
|
+
}
|
|
425
|
+
collectTransformImports(sourceFile)
|
|
396
426
|
for (const [name, binding] of importBindings) {
|
|
397
|
-
if (binding.kind === "namespace") continue
|
|
427
|
+
if (binding.kind === "namespace" || !transformImports.has(name)) continue
|
|
398
428
|
try {
|
|
399
429
|
importedCollectionTransforms.set(name, resolveComponentExport(binding.target, binding.kind === "default" ? "default" : binding.imported, target => parseSourceFile(target, sourceIndex.get(target)), sourceFiles))
|
|
400
430
|
} catch {}
|
|
@@ -422,6 +452,8 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
422
452
|
const customHooks = new Map()
|
|
423
453
|
const parameterizedDebounceCalls = new WeakSet()
|
|
424
454
|
const resolvedParameterizedDebounceHooks = new Map()
|
|
455
|
+
const outsideClickCalls = new WeakMap()
|
|
456
|
+
const resolvedOutsideClickHooks = new Map()
|
|
425
457
|
const jsxLocalDeclarations = new Map()
|
|
426
458
|
const jsxLocalsByFunction = new Map()
|
|
427
459
|
const listLocalDeclarations = []
|
|
@@ -616,6 +648,15 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
616
648
|
resolvedParameterizedDebounceHooks.set(key, analysis)
|
|
617
649
|
return analysis
|
|
618
650
|
}
|
|
651
|
+
const resolveOutsideClickHook = binding => {
|
|
652
|
+
const exportName = binding.kind === "default" ? "default" : binding.imported
|
|
653
|
+
const key = `${binding.target}:${exportName}`
|
|
654
|
+
if (resolvedOutsideClickHooks.has(key)) return resolvedOutsideClickHooks.get(key)
|
|
655
|
+
const hook = resolveComponentExport(binding.target, exportName, importedSource, sourceFiles)
|
|
656
|
+
const analysis = analyzeOutsideClickHook(hook)
|
|
657
|
+
resolvedOutsideClickHooks.set(key, analysis)
|
|
658
|
+
return analysis
|
|
659
|
+
}
|
|
619
660
|
|
|
620
661
|
const collect = node => {
|
|
621
662
|
if (ts.isVariableDeclaration(node) && node.initializer && ts.isCallExpression(node.initializer)) {
|
|
@@ -629,7 +670,8 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
629
670
|
const delay = node.initializer.arguments[1] && unwrapExpression(node.initializer.arguments[1])
|
|
630
671
|
const setters = owner ? settersByFunction.get(owner) ?? new Map() : new Map()
|
|
631
672
|
if (!owner) throw sourceNodeError(node, sourceFile, "Parameterized debounce hooks cannot be used outside a Kudzu component")
|
|
632
|
-
|
|
673
|
+
const stateInitializer = ts.isIdentifier(value) ? directStateInitializer(owner, value.text) : undefined
|
|
674
|
+
if (node.initializer.arguments.length !== 2 || !ts.isIdentifier(value) || !new Set(setters.values()).has(value.text) || !stateInitializer || !isPrimitiveDefaultLiteral(stateInitializer)) throw sourceNodeError(node.initializer, sourceFile, "Parameterized debounce hooks require one direct primitive state argument")
|
|
633
675
|
if (!ts.isNumericLiteral(delay)) throw sourceNodeError(node.initializer.arguments[1] ?? node.initializer, sourceFile, "Parameterized debounce hook delays must be numeric literals")
|
|
634
676
|
const syntheticSetter = `__kSetDebounced_${Math.max(0, node.pos)}`
|
|
635
677
|
setters.set(syntheticSetter, node.name.text)
|
|
@@ -657,6 +699,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
657
699
|
}
|
|
658
700
|
}
|
|
659
701
|
const contextSubstitutions = new Map()
|
|
702
|
+
const contextSharedStates = new Map()
|
|
660
703
|
for (const [setter, state] of hook.states) {
|
|
661
704
|
if (hook.context) {
|
|
662
705
|
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`)
|
|
@@ -676,6 +719,9 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
676
719
|
const localSetter = localName(setter)
|
|
677
720
|
setters.set(localSetter, localState)
|
|
678
721
|
registerState(owner, localState, localSetter, "context", node, { owner: hook.stateOwner, state: hook.stateSymbols.get(state) })
|
|
722
|
+
const sharedState = registerSharedState(moduleIR, { identity: hook.stateOwner.symbol.id, field: state })
|
|
723
|
+
contextSharedStates.set(state, sharedState)
|
|
724
|
+
stateOwnersByFunction.get(owner).set(localState, { kind: "shared-state", sharedState: sharedState.slot })
|
|
679
725
|
if (requiredContextStates.has(state)) {
|
|
680
726
|
for (const [field, local] of [[state, localState], [setter, localSetter]]) {
|
|
681
727
|
if (names.has(field) || privateFields.some(entry => (typeof entry === "string" ? entry : entry.property) === field)) continue
|
|
@@ -710,7 +756,16 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
710
756
|
if (hook.context) {
|
|
711
757
|
const reducers = reducersByFunction.get(owner) ?? new Map()
|
|
712
758
|
const states = new Map([...hook.states].map(([setter, state]) => [contextSubstitutions.get(setter)?.text ?? setter, contextSubstitutions.get(state)?.text ?? state]))
|
|
713
|
-
|
|
759
|
+
const referenced = referencedStateNames(hook.callbacks.get(name).body, hook.states, hook.callbacks.get(name))
|
|
760
|
+
const anchor = [...hook.states.values()].find(state => referenced.has(state))
|
|
761
|
+
const sharedState = contextSharedStates.get(anchor)
|
|
762
|
+
if (!sharedState) {
|
|
763
|
+
reducers.set(name, { sourceKind: "Context", directImplementation: callback, states })
|
|
764
|
+
reducersByFunction.set(owner, reducers)
|
|
765
|
+
continue
|
|
766
|
+
}
|
|
767
|
+
const action = registerSharedAction(moduleIR, { state: sharedState.slot, name })
|
|
768
|
+
reducers.set(name, { state: contextSubstitutions.get(anchor)?.text ?? anchor, sourceKind: "Context", sharedAction: { ...action, directImplementation: callback, states } })
|
|
714
769
|
reducersByFunction.set(owner, reducers)
|
|
715
770
|
}
|
|
716
771
|
}
|
|
@@ -788,6 +843,17 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
788
843
|
}
|
|
789
844
|
}
|
|
790
845
|
}
|
|
846
|
+
if (ts.isCallExpression(node) && ts.isExpressionStatement(node.parent) && ts.isIdentifier(node.expression) && /^use[A-Z]/.test(node.expression.text) && importBindings.has(node.expression.text) && importBindings.get(node.expression.text).kind !== "namespace") {
|
|
847
|
+
const hook = resolveOutsideClickHook(importBindings.get(node.expression.text))
|
|
848
|
+
if (hook) {
|
|
849
|
+
const owner = nearestFunction(node)
|
|
850
|
+
const ref = node.arguments[0] && unwrapExpression(node.arguments[0])
|
|
851
|
+
const callback = directSetterLiteralCallback(node.arguments[1], owner ? settersByFunction.get(owner) ?? new Map() : new Map())
|
|
852
|
+
if (!owner || node.arguments.length !== 2 || !ts.isIdentifier(ref) || !componentHasDirectObjectRef(owner, ref.text)) throw sourceNodeError(node, sourceFile, "Outside-click hooks require one direct component DOM ref as their first argument")
|
|
853
|
+
if (!callback) throw sourceNodeError(node.arguments[1] ?? node, sourceFile, "Outside-click hooks require one inline direct literal setter callback")
|
|
854
|
+
outsideClickCalls.set(node, callback)
|
|
855
|
+
}
|
|
856
|
+
}
|
|
791
857
|
if (ts.isFunctionDeclaration(node) && node.name) {
|
|
792
858
|
functions.set(node.name.text, node)
|
|
793
859
|
if (node.parent === sourceFile) {
|
|
@@ -936,6 +1002,78 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
936
1002
|
if (!returns.length || returns.some(returned => !returned || !ts.isObjectLiteralExpression(returned))) fail(call.expression, "Reactive imported calculations must return a plain object")
|
|
937
1003
|
const fieldExists = returns.every(returned => returned.properties.some(property => ts.isSpreadAssignment(property) || !ts.isComputedPropertyName(property.name) && property.name.text === field))
|
|
938
1004
|
if (!fieldExists) fail(call.parent, `Reactive imported calculation does not return field ${JSON.stringify(field)}`)
|
|
1005
|
+
return { calculation, returns }
|
|
1006
|
+
}
|
|
1007
|
+
const validateSelectedEffectCalculation = (call, field) => {
|
|
1008
|
+
const { calculation, returns } = validateImportedCalculation(call, field)
|
|
1009
|
+
const calculationSource = calculation.getSourceFile()
|
|
1010
|
+
const reject = (target, message) => { throw sourceNodeError(target, calculationSource, message) }
|
|
1011
|
+
if (calculation.asteriskToken || calculation.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) reject(calculation, "Imported calculations used by useEffect() must be synchronous; move async work into the owned effect")
|
|
1012
|
+
if (calculation.parameters.some(parameter => !ts.isIdentifier(parameter.name) || parameter.dotDotDotToken || parameter.initializer || parameter.questionToken)) reject(calculation, "Imported calculations used by useEffect() require ordinary identifier parameters without defaults or rest")
|
|
1013
|
+
if (call.arguments.some(argument => !ts.isIdentifier(unwrapExpression(argument)))) fail(call, "useEffect() selected calculation arguments must be direct primitive state identifiers; aliases, property paths, and composed arguments are not supported")
|
|
1014
|
+
const shapes = returns.map(returned => returned.properties.map(property => {
|
|
1015
|
+
if (!ts.isPropertyAssignment(property) && !ts.isShorthandPropertyAssignment(property) || ts.isComputedPropertyName(property.name) || ["__proto__", "constructor", "prototype"].includes(property.name.text)) reject(property, "Imported calculations used by useEffect() must return direct safe plain-object fields without spreads, methods, or computed names")
|
|
1016
|
+
return property.name.text
|
|
1017
|
+
}).sort())
|
|
1018
|
+
const expected = JSON.stringify(shapes[0])
|
|
1019
|
+
if (shapes.some(shape => JSON.stringify(shape) !== expected)) reject(calculation, `Imported calculations used by useEffect() must return the same direct plain-object fields on every path; expected ${expected}`)
|
|
1020
|
+
if (validatedEffectCalculations.has(calculation)) return
|
|
1021
|
+
const staticImports = importedSerializableCollections(calculationSource, calculationSource.fileName, sourceFiles, sourceIndex)
|
|
1022
|
+
const imported = new Map()
|
|
1023
|
+
for (const statement of calculationSource.statements) if (ts.isImportDeclaration(statement) && statement.importClause && !statement.importClause.isTypeOnly && ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
1024
|
+
const names = []
|
|
1025
|
+
if (statement.importClause.name) names.push(statement.importClause.name.text)
|
|
1026
|
+
const bindings = statement.importClause.namedBindings
|
|
1027
|
+
if (bindings && ts.isNamespaceImport(bindings)) names.push(bindings.name.text)
|
|
1028
|
+
if (bindings && ts.isNamedImports(bindings)) names.push(...bindings.elements.filter(entry => !entry.isTypeOnly).map(entry => entry.name.text))
|
|
1029
|
+
for (const name of names) imported.set(name, statement.moduleSpecifier.text)
|
|
1030
|
+
}
|
|
1031
|
+
for (const [name, target] of imported) if (!target.startsWith(".") && referencesIdentifier(calculation.body, name)) reject(calculation.body, `Imported calculations used by useEffect() cannot reference package import ${JSON.stringify(name)} from ${JSON.stringify(target)}`)
|
|
1032
|
+
const localNames = new Set(calculation.parameters.flatMap(parameter => bindingNames(parameter.name)))
|
|
1033
|
+
if (ts.isBlock(calculation.body)) for (const statement of calculation.body.statements) {
|
|
1034
|
+
if (ts.isVariableStatement(statement)) for (const declaration of statement.declarationList.declarations) for (const name of bindingNames(declaration.name)) localNames.add(name)
|
|
1035
|
+
if (ts.isFunctionDeclaration(statement) && statement.name) localNames.add(statement.name.text)
|
|
1036
|
+
}
|
|
1037
|
+
const localDeclarations = new Map()
|
|
1038
|
+
if (ts.isBlock(calculation.body)) for (const statement of calculation.body.statements) if (ts.isVariableStatement(statement)) for (const declaration of statement.declarationList.declarations) if (ts.isIdentifier(declaration.name) && declaration.initializer) localDeclarations.set(declaration.name.text, declaration.initializer)
|
|
1039
|
+
const visiting = []
|
|
1040
|
+
const visited = new Set()
|
|
1041
|
+
const visitLocal = name => {
|
|
1042
|
+
if (visited.has(name)) return
|
|
1043
|
+
const cycle = visiting.indexOf(name)
|
|
1044
|
+
if (cycle >= 0) reject(localDeclarations.get(name), `Imported calculation derived-local cycle: ${[...visiting.slice(cycle), name].join(" -> ")}`)
|
|
1045
|
+
visiting.push(name)
|
|
1046
|
+
const initializer = localDeclarations.get(name)
|
|
1047
|
+
if (initializer) for (const candidate of localDeclarations.keys()) if (referencesIdentifier(initializer, candidate)) visitLocal(candidate)
|
|
1048
|
+
visiting.pop()
|
|
1049
|
+
visited.add(name)
|
|
1050
|
+
}
|
|
1051
|
+
for (const name of localDeclarations.keys()) visitLocal(name)
|
|
1052
|
+
const visit = node => {
|
|
1053
|
+
if (ts.isTypeNode(node)) return
|
|
1054
|
+
if (ts.isBinaryExpression(node) && assignmentOperators.has(node.operatorToken.kind) || ts.isPostfixUnaryExpression(node) || ts.isPrefixUnaryExpression(node) && [ts.SyntaxKind.PlusPlusToken, ts.SyntaxKind.MinusMinusToken].includes(node.operator) || ts.isDeleteExpression(node)) reject(node, "Imported calculations used by useEffect() must be pure; assignments, updates, delete, and mutation are not supported")
|
|
1055
|
+
if (ts.isAwaitExpression(node) || ts.isYieldExpression(node) || ts.isNewExpression(node)) reject(node, "Imported calculations used by useEffect() must be deterministic and synchronous; await, yield, and construction are not supported")
|
|
1056
|
+
if (ts.isCallExpression(node)) {
|
|
1057
|
+
if (ts.isIdentifier(node.expression) && ["Boolean", "Number", "String"].includes(node.expression.text)) {
|
|
1058
|
+
// Primitive conversions are deterministic.
|
|
1059
|
+
} else if (ts.isPropertyAccessExpression(node.expression)) {
|
|
1060
|
+
const method = node.expression.name.text
|
|
1061
|
+
const receiver = unwrapExpression(node.expression.expression)
|
|
1062
|
+
if (mutatingListMethods.has(method)) reject(node, `Imported calculations used by useEffect() must be pure; mutating method ${JSON.stringify(method)} is not supported`)
|
|
1063
|
+
const math = ts.isIdentifier(receiver) && receiver.text === "Math"
|
|
1064
|
+
const find = method === "find" && node.arguments.length === 1 && ts.isArrowFunction(node.arguments[0]) && !ts.isBlock(node.arguments[0].body) && !node.arguments[0].modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)
|
|
1065
|
+
if (!find && !(math && pureMathMethods.has(method)) && !pureListMethods.has(method)) reject(node, `Imported calculations used by useEffect() must be deterministic; call ${JSON.stringify(node.expression.getText(calculationSource))} is not supported`)
|
|
1066
|
+
} else reject(node, "Imported calculations used by useEffect() cannot call arbitrary functions")
|
|
1067
|
+
}
|
|
1068
|
+
if (ts.isIdentifier(node) && isReferenceIdentifier(node)) {
|
|
1069
|
+
const target = imported.get(node.text)
|
|
1070
|
+
if (target && !target.startsWith(".")) reject(node, `Imported calculations used by useEffect() cannot reference package import ${JSON.stringify(node.text)} from ${JSON.stringify(target)}`)
|
|
1071
|
+
if (target?.startsWith(".") && !staticImports.has(node.text)) reject(node, `Imported calculations used by useEffect() may capture only relative exported JSON-safe const arrays; capture ${JSON.stringify(node.text)} is opaque or nonserializable`)
|
|
1072
|
+
}
|
|
1073
|
+
ts.forEachChild(node, visit)
|
|
1074
|
+
}
|
|
1075
|
+
visit(calculation.body)
|
|
1076
|
+
validatedEffectCalculations.add(calculation)
|
|
939
1077
|
}
|
|
940
1078
|
const validateReactiveJsxExpression = (expression, allowedNames) => {
|
|
941
1079
|
const value = unwrapExpression(expression)
|
|
@@ -1009,6 +1147,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1009
1147
|
}
|
|
1010
1148
|
const componentSpecializations = new WeakMap()
|
|
1011
1149
|
const specializedEffectStateOwners = new WeakMap()
|
|
1150
|
+
const calculationDerivedByOwner = new WeakMap()
|
|
1012
1151
|
const setterHookHelpers = new WeakMap()
|
|
1013
1152
|
const expandedRowSpecializations = new WeakMap()
|
|
1014
1153
|
const nestedRowSpecializations = new Map()
|
|
@@ -1039,7 +1178,9 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1039
1178
|
props: result.props.map(prop => {
|
|
1040
1179
|
const expression = result.propExpressions.get(prop.name)
|
|
1041
1180
|
const signals = expression ? propSignals(expression) : []
|
|
1042
|
-
|
|
1181
|
+
const uses = result.propertyUses.get(prop.local) ?? []
|
|
1182
|
+
const properties = signals.length === 1 ? uses.map(use => ({ signal: signals[0], path: use.path, consumers: use.consumers, equality: "object-is" })) : []
|
|
1183
|
+
return { ...prop, ...(signals.length ? { signals } : {}), ...(properties.length ? { properties } : {}) }
|
|
1043
1184
|
}),
|
|
1044
1185
|
states: [
|
|
1045
1186
|
...result.rowStates.map(({ state, setter, source }) => ({ name: state, setter, kind: "row", ...(analysisSite(source, "hook") ? { site: analysisSite(source, "hook") } : {}), ...(analysisSource(source) ? { source: analysisSource(source) } : {}) })),
|
|
@@ -1117,6 +1258,44 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1117
1258
|
merged.parent = root.parent
|
|
1118
1259
|
return merged
|
|
1119
1260
|
}
|
|
1261
|
+
const materializeComponentHelper = (call, specialization, prefix, prepend) => {
|
|
1262
|
+
const owner = nearestFunction(call)
|
|
1263
|
+
const name = `${prefix}${Math.max(0, call.pos)}`
|
|
1264
|
+
const effectStatements = specialization.effects.map(entry => {
|
|
1265
|
+
const effectCall = factory.updateCallExpression(entry.call, factory.createIdentifier("__kComponentUseEffect"), entry.call.typeArguments, entry.call.arguments)
|
|
1266
|
+
synthesizeTree(effectCall)
|
|
1267
|
+
ts.setOriginalNode(effectCall, entry.source)
|
|
1268
|
+
specializedEffectStateOwners.set(effectCall, { owner: { kind: "specialization", slot: specialization.analysis.slot }, references: specialization.propStateOwners })
|
|
1269
|
+
return factory.createExpressionStatement(effectCall)
|
|
1270
|
+
})
|
|
1271
|
+
const rendered = prepend ? factory.createNull() : specialization.root
|
|
1272
|
+
const helper = factory.createFunctionDeclaration(undefined, undefined, name, undefined, [], undefined, factory.createBlock([...specialization.hookDeclarations, ...effectStatements, factory.createReturnStatement(rendered)], true))
|
|
1273
|
+
ts.setParentRecursive(helper, false)
|
|
1274
|
+
helper.parent = owner.body
|
|
1275
|
+
const helpers = setterHookHelpers.get(owner.body) ?? []
|
|
1276
|
+
helpers.push(helper)
|
|
1277
|
+
setterHookHelpers.set(owner.body, helpers)
|
|
1278
|
+
const setters = new Map(settersForNode(call, settersByFunction))
|
|
1279
|
+
for (const state of specialization.ordinaryStates) setters.set(state.setter, state.state)
|
|
1280
|
+
settersByFunction.set(helper, setters)
|
|
1281
|
+
const stateOwners = new Map([...stateOwnersForNode(call), ...specialization.propStateOwners])
|
|
1282
|
+
for (const state of specialization.ordinaryStates) stateOwners.set(state.state, state.analysisReference)
|
|
1283
|
+
stateOwnersByFunction.set(helper, stateOwners)
|
|
1284
|
+
usesComponentState ||= specialization.ordinaryStates.length > 0
|
|
1285
|
+
usesComponentId ||= specialization.usesComponentId
|
|
1286
|
+
usesComponentRef ||= specialization.ordinaryRefs.length > 0
|
|
1287
|
+
usesComponentEffects ||= specialization.effects.length > 0
|
|
1288
|
+
const invocation = factory.createJsxSelfClosingElement(factory.createIdentifier(name), undefined, factory.createJsxAttributes([]))
|
|
1289
|
+
specialization.root = prepend ? prependJsxChild(specialization.root, invocation, factory) : invocation
|
|
1290
|
+
ts.setParentRecursive(specialization.root, false)
|
|
1291
|
+
specialization.root.parent = call.parent
|
|
1292
|
+
}
|
|
1293
|
+
const attachStateBackedEffects = (call, specialization, componentSource, imported) => {
|
|
1294
|
+
if (imported) synthesizeTree(specialization.root = mergeSpecializedImports(specialization.root, componentSource, call, specialization.effects))
|
|
1295
|
+
if (!specialization.effects.length) return
|
|
1296
|
+
if (specialization.hookDeclarations.length) fail(call, "State-backed object property components cannot declare state, refs, or IDs")
|
|
1297
|
+
materializeComponentHelper(call, specialization, "KPropertyEffect", true)
|
|
1298
|
+
}
|
|
1120
1299
|
const expandReducerCallbacks = (root, componentSource, call, ownership) => {
|
|
1121
1300
|
const componentImports = clientImportBindings(componentSource, componentSource.fileName, sourceFiles)
|
|
1122
1301
|
const replacements = new WeakMap()
|
|
@@ -1300,8 +1479,10 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1300
1479
|
if (identifierReferenceCount(sourceFile, name) !== calls.length) fail(component.declaration, `State-backed list component ${name} may only be referenced as JSX`)
|
|
1301
1480
|
if (stateBackedCalls.length !== calls.length) fail(component.declaration, `State-backed list component ${name} must receive its mapped prop from local state at every call`)
|
|
1302
1481
|
for (const call of stateBackedCalls) {
|
|
1303
|
-
const
|
|
1304
|
-
|
|
1482
|
+
const objectProperty = isStateBackedListComponentCall(call, component.function, settersByFunction.get(nearestFunction(call)) ?? new Map(), true)
|
|
1483
|
+
const specialization = specialize(call, component.function, objectProperty ? "Object-property component" : "Keyed list")
|
|
1484
|
+
if (specialization.effects.length && !objectProperty) fail(call, "State-backed list components cannot declare effects")
|
|
1485
|
+
if (objectProperty) attachStateBackedEffects(call, specialization, component.function.getSourceFile(), false)
|
|
1305
1486
|
componentSpecializations.set(call, specialization)
|
|
1306
1487
|
stateBackedComponentRoots.push(specialization.root)
|
|
1307
1488
|
}
|
|
@@ -1322,8 +1503,10 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1322
1503
|
}
|
|
1323
1504
|
const stateBackedCalls = calls.filter(call => isStateBackedListComponentCall(call, component, settersByFunction.get(nearestFunction(call)) ?? new Map()))
|
|
1324
1505
|
for (const call of stateBackedCalls) {
|
|
1325
|
-
const
|
|
1326
|
-
|
|
1506
|
+
const objectProperty = isStateBackedListComponentCall(call, component, settersByFunction.get(nearestFunction(call)) ?? new Map(), true)
|
|
1507
|
+
const specialization = specialize(call, component, objectProperty ? "Object-property component" : "Keyed list")
|
|
1508
|
+
if (specialization.effects.length && !objectProperty) fail(call, "State-backed list components cannot declare effects")
|
|
1509
|
+
if (objectProperty) attachStateBackedEffects(call, specialization, component.getSourceFile(), true)
|
|
1327
1510
|
componentSpecializations.set(call, specialization)
|
|
1328
1511
|
stateBackedComponentRoots.push(specialization.root)
|
|
1329
1512
|
}
|
|
@@ -1340,7 +1523,8 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1340
1523
|
const setterAttribute = attributes.properties.find(entry => ts.isJsxAttribute(entry) && entry.name.text === prop)
|
|
1341
1524
|
const stateValue = stateAttribute?.initializer && ts.isJsxExpression(stateAttribute.initializer) ? unwrapExpression(stateAttribute.initializer.expression) : undefined
|
|
1342
1525
|
const setterValue = setterAttribute?.initializer && ts.isJsxExpression(setterAttribute.initializer) ? unwrapExpression(setterAttribute.initializer.expression) : undefined
|
|
1343
|
-
|
|
1526
|
+
const stateInitializer = ts.isIdentifier(stateValue) ? directStateInitializer(nearestFunction(call), stateValue.text) : undefined
|
|
1527
|
+
if (!ts.isIdentifier(stateValue) || !ts.isIdentifier(setterValue) || setters.get(setterValue.text) !== stateValue.text || !stateInitializer || !ts.isArrayLiteralExpression(stateInitializer)) fail(setterAttribute ?? call, `Setter-callback effect prop ${JSON.stringify(prop)} must target the same direct array state passed through ${JSON.stringify(effect.stateProp)}`)
|
|
1344
1528
|
}
|
|
1345
1529
|
const specialization = specialize(call, component, "Setter-callback", true, true, new Set(settersForNode(call, settersByFunction).values()))
|
|
1346
1530
|
if (specialization.hookDeclarations.length || specialization.effects.length) {
|
|
@@ -1358,44 +1542,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1358
1542
|
}
|
|
1359
1543
|
specialization.root = expandSetterComponents(specialization.root, component.getSourceFile(), [component], specialization, settersForNode(call, settersByFunction), stateOwnersForNode(call))
|
|
1360
1544
|
if (imported) synthesizeTree(specialization.root = mergeSpecializedImports(specialization.root, component.getSourceFile(), call, specialization.effects))
|
|
1361
|
-
if (specialization.hookDeclarations.length || specialization.effects.length)
|
|
1362
|
-
const owner = nearestFunction(call)
|
|
1363
|
-
const name = `KSetterComponent${Math.max(0, call.pos)}`
|
|
1364
|
-
const effectStatements = specialization.effects.map(entry => {
|
|
1365
|
-
const effectCall = factory.updateCallExpression(entry.call, factory.createIdentifier("__kComponentUseEffect"), entry.call.typeArguments, entry.call.arguments)
|
|
1366
|
-
synthesizeTree(effectCall)
|
|
1367
|
-
ts.setOriginalNode(effectCall, entry.source)
|
|
1368
|
-
specializedEffectStateOwners.set(effectCall, { owner: { kind: "specialization", slot: specialization.analysis.slot }, references: specialization.propStateOwners })
|
|
1369
|
-
return factory.createExpressionStatement(effectCall)
|
|
1370
|
-
})
|
|
1371
|
-
const helper = factory.createFunctionDeclaration(
|
|
1372
|
-
undefined,
|
|
1373
|
-
undefined,
|
|
1374
|
-
name,
|
|
1375
|
-
undefined,
|
|
1376
|
-
[],
|
|
1377
|
-
undefined,
|
|
1378
|
-
factory.createBlock([...specialization.hookDeclarations, ...effectStatements, factory.createReturnStatement(specialization.root)], true)
|
|
1379
|
-
)
|
|
1380
|
-
ts.setParentRecursive(helper, false)
|
|
1381
|
-
helper.parent = owner.body
|
|
1382
|
-
const helpers = setterHookHelpers.get(owner.body) ?? []
|
|
1383
|
-
helpers.push(helper)
|
|
1384
|
-
setterHookHelpers.set(owner.body, helpers)
|
|
1385
|
-
const setters = new Map(settersForNode(call, settersByFunction))
|
|
1386
|
-
for (const state of specialization.ordinaryStates) setters.set(state.setter, state.state)
|
|
1387
|
-
settersByFunction.set(helper, setters)
|
|
1388
|
-
const stateOwners = new Map([...stateOwnersForNode(call), ...specialization.propStateOwners])
|
|
1389
|
-
for (const state of specialization.ordinaryStates) stateOwners.set(state.state, state.analysisReference)
|
|
1390
|
-
stateOwnersByFunction.set(helper, stateOwners)
|
|
1391
|
-
usesComponentState ||= specialization.ordinaryStates.length > 0
|
|
1392
|
-
usesComponentId ||= specialization.usesComponentId
|
|
1393
|
-
usesComponentRef ||= specialization.ordinaryRefs.length > 0
|
|
1394
|
-
usesComponentEffects ||= specialization.effects.length > 0
|
|
1395
|
-
specialization.root = factory.createJsxSelfClosingElement(factory.createIdentifier(name), undefined, factory.createJsxAttributes([]))
|
|
1396
|
-
ts.setParentRecursive(specialization.root, false)
|
|
1397
|
-
specialization.root.parent = call.parent
|
|
1398
|
-
}
|
|
1545
|
+
if (specialization.hookDeclarations.length || specialization.effects.length) materializeComponentHelper(call, specialization, "KSetterComponent", false)
|
|
1399
1546
|
componentSpecializations.set(call, specialization)
|
|
1400
1547
|
}
|
|
1401
1548
|
for (const [name, component] of components) {
|
|
@@ -1702,7 +1849,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1702
1849
|
: collectionSymbol !== undefined ? { kind: "symbol", symbol: collectionSymbol } : { kind: "static" }
|
|
1703
1850
|
if (listParts.calculation) {
|
|
1704
1851
|
usesBinding = true
|
|
1705
|
-
const compiled = descriptors.compileReactiveBinding(listParts.calculation, { setters: settersForNode(node, settersByFunction), importBindings, keyedBlock: blockSlot })
|
|
1852
|
+
const compiled = descriptors.compileReactiveBinding(listParts.calculation, { setters: settersForNode(node, settersByFunction), importBindings, keyedBlock: blockSlot, derived: calculationBinding(node, nearestFunction(node)) })
|
|
1706
1853
|
listSource = compiled.node
|
|
1707
1854
|
collection = { kind: "binding", binding: compiled.binding }
|
|
1708
1855
|
}
|
|
@@ -1747,6 +1894,8 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1747
1894
|
return factory.updateJsxExpression(node, factory.createCallExpression(factory.createIdentifier("__kList"), undefined, arguments_))
|
|
1748
1895
|
}
|
|
1749
1896
|
const visitor = node => {
|
|
1897
|
+
const outsideClick = ts.isCallExpression(node) && outsideClickCalls.get(node)
|
|
1898
|
+
if (outsideClick) return factory.updateCallExpression(node, node.expression, node.typeArguments, [node.arguments[0], factory.createIdentifier(outsideClick.setter), cloneAst(outsideClick.value, factory, context)])
|
|
1750
1899
|
if (ts.isJsxAttribute(node) && contextProviderPrivateSetters.has(node)) {
|
|
1751
1900
|
const expression = unwrapExpression(node.initializer.expression)
|
|
1752
1901
|
const value = factory.updateObjectLiteralExpression(expression, [...expression.properties, ...contextProviderPrivateSetters.get(node).map(name => factory.createShorthandPropertyAssignment(name))])
|
|
@@ -1825,6 +1974,26 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1825
1974
|
if (callback.parameters.length) effectFail(callback, "useEffect() callback cannot declare parameters")
|
|
1826
1975
|
if (!ts.isArrayLiteralExpression(dependencies)) effectFail(dependencies, "useEffect() dependencies must be a literal array")
|
|
1827
1976
|
const setters = settersForNode(node, settersByFunction)
|
|
1977
|
+
const resolveCalculation = dependency => {
|
|
1978
|
+
if (specializedEffect) return undefined
|
|
1979
|
+
const value = unwrapExpression(dependency)
|
|
1980
|
+
const result = ts.isIdentifier(value) ? value : ts.isPropertyAccessExpression(value) || ts.isElementAccessExpression(value) ? unwrapExpression(value.expression) : undefined
|
|
1981
|
+
if (!result || !ts.isIdentifier(result)) return undefined
|
|
1982
|
+
const entries = jsxLocalDeclarations.get(effectOwner)?.get(result.text)
|
|
1983
|
+
const initializer = entries?.length === 1 && entries[0].node.parent?.parent?.parent === effectOwner?.body ? unwrapExpression(entries[0].initializer) : undefined
|
|
1984
|
+
if (!initializer || !ts.isCallExpression(initializer) || !ts.isIdentifier(initializer.expression) || !importBindings.has(initializer.expression.text)) return undefined
|
|
1985
|
+
if (ts.isIdentifier(value)) effectFail(value, `useEffect() cannot depend on the whole imported calculation result ${JSON.stringify(value.text)}; select one JSON-safe primitive field such as ${value.text}.id`)
|
|
1986
|
+
if (ts.isElementAccessExpression(value)) effectFail(value, `useEffect() imported calculation dependencies require one direct static result field such as ${result.text}.id; computed result properties are not supported`)
|
|
1987
|
+
if (!ts.isPropertyAccessExpression(value) || ["__proto__", "constructor", "prototype"].includes(value.name.text)) effectFail(value, "useEffect() imported calculation dependency field must not be __proto__, prototype, or constructor")
|
|
1988
|
+
validateSelectedEffectCalculation(initializer, value.name.text)
|
|
1989
|
+
const expanded = unwrapExpression(resolveReactiveJsxExpression(dependency, effectOwner, setters))
|
|
1990
|
+
if (!ts.isPropertyAccessExpression(expanded) || expanded.name.text !== value.name.text) return undefined
|
|
1991
|
+
const call = unwrapExpression(expanded.expression)
|
|
1992
|
+
if (!ts.isCallExpression(call) || !ts.isIdentifier(call.expression) || !importBindings.has(call.expression.text)) return undefined
|
|
1993
|
+
const states = referencedStateNames(call, setters, call, bindingIndex)
|
|
1994
|
+
if (states.size < 2 || call.arguments.some(argument => !ts.isIdentifier(unwrapExpression(argument)) || !states.has(unwrapExpression(argument).text))) effectFail(call, "useEffect() selected calculations require at least two direct primitive state arguments")
|
|
1995
|
+
return { name: value.expression.text, field: value.name.text, call, states, source: dependency }
|
|
1996
|
+
}
|
|
1828
1997
|
const dependencyAnalysis = analyzeEffectDependencies({
|
|
1829
1998
|
dependencies,
|
|
1830
1999
|
node,
|
|
@@ -1834,9 +2003,12 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1834
2003
|
localDeclarations: jsxLocalDeclarations.get(nearestFunction(node)),
|
|
1835
2004
|
factory,
|
|
1836
2005
|
fail: effectFail,
|
|
1837
|
-
bindingIndex
|
|
2006
|
+
bindingIndex,
|
|
2007
|
+
resolveCalculation
|
|
1838
2008
|
})
|
|
1839
2009
|
const { dependencyItem, itemDependencies, ordinaryDependencies, entries: dependencyEntries, dependencyStates, substitutions: dependencySubstitutions, subscriptions: subscriptionDependencies, hasDerived: hasDerivedDependency } = dependencyAnalysis
|
|
2010
|
+
const calculationEntries = dependencyEntries.filter(entry => entry.kind === "calculation")
|
|
2011
|
+
if (calculationEntries.length && (calculationEntries.length !== 1 || dependencyEntries.length !== 1)) effectFail(dependencies, "useEffect() selected calculation fields must be the only dependency")
|
|
1840
2012
|
if (!effectOwner) fail(node, "useEffect() cannot be used outside a Kudzu component")
|
|
1841
2013
|
if (!ts.isBlock(callback.body)) effectFail(callback, "useEffect() callback must use a block body")
|
|
1842
2014
|
const cleanupSubstitutions = new Map()
|
|
@@ -1860,6 +2032,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1860
2032
|
if (invalidCleanup) effectFail(invalidCleanup, "useEffect() cleanup functions cannot declare parameters or be generators")
|
|
1861
2033
|
if (returns.cleanup && callback.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) effectFail(callback, "useEffect() async callbacks cannot return cleanup functions")
|
|
1862
2034
|
validateEffectOwnedBrowserResources(callback, returns, effectFail, bindingIndex)
|
|
2035
|
+
const calculationEvaluators = dependencyEntries.map(entry => entry.kind === "calculation" ? descriptors.compileDerivedEvaluator(entry.call, { setters, importBindings }) : undefined)
|
|
1863
2036
|
const callbackSource = specializedEffect?.sourceFile ?? sourceFile
|
|
1864
2037
|
const callbackFile = callbackSource.fileName
|
|
1865
2038
|
let compiledCallback = dependencySubstitutions.size ? substituteClone(callback, dependencySubstitutions, factory, context) : callback
|
|
@@ -1889,7 +2062,16 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1889
2062
|
})
|
|
1890
2063
|
usesListItem ||= Boolean(itemDependencies.length && !listEffect)
|
|
1891
2064
|
usesBehavior = true
|
|
1892
|
-
const derivedDependencies = hasDerivedDependency ? dependencyEntries.map(entry => entry.kind === "derived"
|
|
2065
|
+
const derivedDependencies = hasDerivedDependency ? dependencyEntries.map((entry, index) => entry.kind === "derived"
|
|
2066
|
+
? descriptors.registerDerived("expression", entry.expression, entry.states, entry.source)
|
|
2067
|
+
: entry.kind === "calculation" ? descriptors.registerDerived("calculation", { binding: calculationEvaluators[index].binding, fields: [entry.field] }, entry.states, entry.source) : undefined) : []
|
|
2068
|
+
if (calculationEntries.length) {
|
|
2069
|
+
const calculations = calculationDerivedByOwner.get(effectOwner) ?? new Map()
|
|
2070
|
+
dependencyEntries.forEach((entry, index) => {
|
|
2071
|
+
if (entry.kind === "calculation") calculations.set(entry.name, derivedDependencies[index])
|
|
2072
|
+
})
|
|
2073
|
+
calculationDerivedByOwner.set(effectOwner, calculations)
|
|
2074
|
+
}
|
|
1893
2075
|
const effectSource = specializedEffect?.source ?? node
|
|
1894
2076
|
const lexicalOwner = nearestFunction(effectSource)
|
|
1895
2077
|
const effectStateOwners = new Map([...stateOwnersForNode(callbackArgument), ...stateOwnersForNode(node), ...(specializedEffectStateOwners.get(node)?.references ?? []), ...(activeStateOwners ?? [])])
|
|
@@ -1898,7 +2080,9 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1898
2080
|
const dependencyStateNames = [...dependencyStates.keys()]
|
|
1899
2081
|
const effect = descriptors.registerEffect(descriptor, {
|
|
1900
2082
|
cleanup: returns.cleanup,
|
|
1901
|
-
dependencies: hasDerivedDependency ? dependencyEntries.map((entry, index) => entry.kind === "derived"
|
|
2083
|
+
dependencies: hasDerivedDependency ? dependencyEntries.map((entry, index) => entry.kind === "derived"
|
|
2084
|
+
? { kind: "derived", derived: derivedDependencies[index].slot, sources: [...entry.states].map(signalFor) }
|
|
2085
|
+
: entry.kind === "calculation" ? { kind: "derived", derived: derivedDependencies[index].slot, sources: [...entry.states].map(signalFor), field: entry.field, evaluator: calculationEvaluators[index].binding } : { kind: "signal", signal: signalFor(entry.name) }) : ordinaryDependencies.map(dependency => ({ kind: "signal", signal: signalFor(dependency.text) })),
|
|
1902
2086
|
subscriptions: subscriptionNames.map(signalFor),
|
|
1903
2087
|
dependencySignals: dependencyStateNames.map(signalFor),
|
|
1904
2088
|
itemDependencies,
|
|
@@ -1912,7 +2096,12 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1912
2096
|
...(analysisSite(effectSource, "hook") ? { site: analysisSite(effectSource, "hook") } : {}),
|
|
1913
2097
|
...(analysisSource(effectSource) ? { source: analysisSource(effectSource) } : {})
|
|
1914
2098
|
})
|
|
1915
|
-
const
|
|
2099
|
+
const hasExpressionDependency = dependencyEntries.some(entry => entry.kind === "derived")
|
|
2100
|
+
const dependencyExpressions = hasExpressionDependency ? effect.dependencies.map((dependency, index) => dependency.kind === "derived" ? moduleIR.derived[dependency.derived].expression : ["state", dependencyEntries[index]?.name ?? ordinaryDependencies[index].text]) : []
|
|
2101
|
+
const dependencyEvaluators = calculationEntries.length ? calculationEvaluators.map((evaluator, index) => evaluator ? factory.createObjectLiteralExpression([
|
|
2102
|
+
factory.createSpreadAssignment(evaluator.descriptor),
|
|
2103
|
+
factory.createPropertyAssignment("field", factory.createStringLiteral(dependencyEntries[index].field))
|
|
2104
|
+
]) : factory.createNull()) : []
|
|
1916
2105
|
const buildCallback = [...packageBindings].some(([name]) => referenceIdentifiers(callback, name).length)
|
|
1917
2106
|
? factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), factory.createBlock([], false))
|
|
1918
2107
|
: callback
|
|
@@ -1926,8 +2115,9 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1926
2115
|
factory.createStringLiteral(specializedEffect ? sourceLocation(specializedEffect.source, specializedEffect.sourceFile) : sourceLocation(node, sourceFile)),
|
|
1927
2116
|
effect.cleanup ? factory.createTrue() : factory.createFalse(),
|
|
1928
2117
|
factory.createArrayLiteralExpression(effect.itemDependencies.map(field => factory.createStringLiteral(field))),
|
|
1929
|
-
|
|
1930
|
-
factory.createArrayLiteralExpression(dependencyStateNames.map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), factory.createIdentifier(name)])))
|
|
2118
|
+
dependencyExpressions.length ? jsonExpression(dependencyExpressions, factory) : factory.createArrayLiteralExpression(),
|
|
2119
|
+
factory.createArrayLiteralExpression(dependencyStateNames.map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), factory.createIdentifier(name)]))),
|
|
2120
|
+
factory.createArrayLiteralExpression(dependencyEvaluators)
|
|
1931
2121
|
])
|
|
1932
2122
|
}
|
|
1933
2123
|
|
|
@@ -2004,7 +2194,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
2004
2194
|
if ((usedStates.size || captures.size) && !ts.isIdentifier(expression) && !containsJsx(expression)) {
|
|
2005
2195
|
usesBehavior = true
|
|
2006
2196
|
usesBinding = true
|
|
2007
|
-
return factory.updateJsxExpression(node, descriptors.compileReactiveBinding(expression, { setters, importBindings }).node)
|
|
2197
|
+
return factory.updateJsxExpression(node, descriptors.compileReactiveBinding(expression, { setters, importBindings, derived: calculationBinding(node.expression, nearestFunction(node)) }).node)
|
|
2008
2198
|
}
|
|
2009
2199
|
}
|
|
2010
2200
|
|
|
@@ -2017,7 +2207,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
2017
2207
|
if ((usedStates.size || captures.size) && !ts.isIdentifier(expression)) {
|
|
2018
2208
|
usesBehavior = true
|
|
2019
2209
|
usesBinding = true
|
|
2020
|
-
const compiled = descriptors.compileReactiveBinding(expression, { setters, importBindings })
|
|
2210
|
+
const compiled = descriptors.compileReactiveBinding(expression, { setters, importBindings, derived: calculationBinding(sourceExpression, nearestFunction(node)) })
|
|
2021
2211
|
return factory.updateJsxAttribute(node, node.name, factory.createJsxExpression(undefined, compiled.node))
|
|
2022
2212
|
}
|
|
2023
2213
|
}
|
|
@@ -2046,6 +2236,27 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
2046
2236
|
return ts.visitEachChild(node, visitor, context)
|
|
2047
2237
|
}
|
|
2048
2238
|
|
|
2239
|
+
function calculationBinding(expression, owner) {
|
|
2240
|
+
const calculations = calculationDerivedByOwner.get(owner)
|
|
2241
|
+
if (!calculations?.size) return undefined
|
|
2242
|
+
const fields = new Map()
|
|
2243
|
+
const visit = node => {
|
|
2244
|
+
const value = unwrapExpression(node)
|
|
2245
|
+
if (ts.isPropertyAccessExpression(value) && ts.isIdentifier(value.expression) && calculations.has(value.expression.text)) {
|
|
2246
|
+
const derived = calculations.get(value.expression.text)
|
|
2247
|
+
const entry = fields.get(derived.slot) ?? { derived, fields: new Set() }
|
|
2248
|
+
entry.fields.add(value.name.text)
|
|
2249
|
+
fields.set(derived.slot, entry)
|
|
2250
|
+
}
|
|
2251
|
+
ts.forEachChild(node, visit)
|
|
2252
|
+
}
|
|
2253
|
+
visit(expression)
|
|
2254
|
+
if (fields.size !== 1) return undefined
|
|
2255
|
+
const [{ derived, fields: selected }] = fields.values()
|
|
2256
|
+
for (const field of selected) if (!derived.calculation.fields.includes(field)) derived.calculation.fields.push(field)
|
|
2257
|
+
return { derived: derived.slot, fields: [...selected] }
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2049
2260
|
const transformed = ts.visitNode(sourceFile, visitor)
|
|
2050
2261
|
descriptors.finalize()
|
|
2051
2262
|
if (!usesBehavior) return transformed
|
|
@@ -2181,7 +2392,7 @@ function jsonExpression(value, factory) {
|
|
|
2181
2392
|
return factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("JSON"), "parse"), undefined, [factory.createStringLiteral(JSON.stringify(value))])
|
|
2182
2393
|
}
|
|
2183
2394
|
|
|
2184
|
-
function isStateBackedListComponentCall(call, component, setters) {
|
|
2395
|
+
function isStateBackedListComponentCall(call, component, setters, propertyOnly = false) {
|
|
2185
2396
|
if (component.parameters.length !== 1 || !ts.isObjectBindingPattern(component.parameters[0].name)) return false
|
|
2186
2397
|
const attributes = ts.isJsxElement(call) ? call.openingElement.attributes : call.attributes
|
|
2187
2398
|
const stateNames = new Set(setters.values())
|
|
@@ -2201,7 +2412,9 @@ function isStateBackedListComponentCall(call, component, setters) {
|
|
|
2201
2412
|
let found = false
|
|
2202
2413
|
const visit = node => {
|
|
2203
2414
|
if (found || node !== returned && isFunctionLike(node)) return
|
|
2204
|
-
|
|
2415
|
+
const collection = ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "map" ? unwrapExpression(node.expression.expression) : undefined
|
|
2416
|
+
const root = collection && ts.isPropertyAccessExpression(collection) ? unwrapExpression(collection.expression) : collection
|
|
2417
|
+
if (root && ts.isIdentifier(root) && mappedProps.has(root.text) && (!propertyOnly || ts.isPropertyAccessExpression(collection))) {
|
|
2205
2418
|
found = true
|
|
2206
2419
|
return
|
|
2207
2420
|
}
|
|
@@ -2211,6 +2424,75 @@ function isStateBackedListComponentCall(call, component, setters) {
|
|
|
2211
2424
|
return found
|
|
2212
2425
|
}
|
|
2213
2426
|
|
|
2427
|
+
function referencesComponentProperty(expression, locals) {
|
|
2428
|
+
let found = false
|
|
2429
|
+
const visit = node => {
|
|
2430
|
+
if (found) return
|
|
2431
|
+
if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(unwrapExpression(node.expression)) && locals.has(unwrapExpression(node.expression).text)) {
|
|
2432
|
+
found = true
|
|
2433
|
+
return
|
|
2434
|
+
}
|
|
2435
|
+
ts.forEachChild(node, visit)
|
|
2436
|
+
}
|
|
2437
|
+
visit(expression)
|
|
2438
|
+
return found
|
|
2439
|
+
}
|
|
2440
|
+
|
|
2441
|
+
function componentPropertyMutation(component, locals) {
|
|
2442
|
+
let invalid
|
|
2443
|
+
const root = expression => {
|
|
2444
|
+
let value = unwrapExpression(expression)
|
|
2445
|
+
while (ts.isPropertyAccessExpression(value) || ts.isElementAccessExpression(value)) value = unwrapExpression(value.expression)
|
|
2446
|
+
return ts.isIdentifier(value) ? value.text : undefined
|
|
2447
|
+
}
|
|
2448
|
+
const visit = node => {
|
|
2449
|
+
if (invalid) return
|
|
2450
|
+
if (ts.isBinaryExpression(node) && node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && node.operatorToken.kind <= ts.SyntaxKind.LastAssignment && locals.has(root(node.left))) invalid = node
|
|
2451
|
+
else if ((ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node)) && locals.has(root(node.operand))) invalid = node
|
|
2452
|
+
else if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && mutatingListMethods.has(node.expression.name.text) && locals.has(root(node.expression.expression))) invalid = node
|
|
2453
|
+
if (!invalid) ts.forEachChild(node, visit)
|
|
2454
|
+
}
|
|
2455
|
+
visit(component)
|
|
2456
|
+
return invalid
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
function prependJsxChild(root, child, factory) {
|
|
2460
|
+
if (ts.isJsxElement(root)) return factory.updateJsxElement(root, root.openingElement, [child, ...root.children], root.closingElement)
|
|
2461
|
+
const opening = factory.createJsxOpeningElement(root.tagName, root.typeArguments, root.attributes)
|
|
2462
|
+
const closing = factory.createJsxClosingElement(root.tagName)
|
|
2463
|
+
return factory.createJsxElement(opening, [child], closing)
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
function componentPropertyUses(component, returned, effectCalls) {
|
|
2467
|
+
const locals = new Set(component.parameters[0].name.elements.filter(element => !element.dotDotDotToken && ts.isIdentifier(element.name)).map(element => element.name.text))
|
|
2468
|
+
const uses = new Map()
|
|
2469
|
+
const add = (local, field, consumer) => {
|
|
2470
|
+
if (["__proto__", "constructor", "prototype"].includes(field)) return
|
|
2471
|
+
const fields = uses.get(local) ?? new Map()
|
|
2472
|
+
const consumers = fields.get(field) ?? new Set()
|
|
2473
|
+
consumers.add(consumer)
|
|
2474
|
+
fields.set(field, consumers)
|
|
2475
|
+
uses.set(local, fields)
|
|
2476
|
+
}
|
|
2477
|
+
for (const call of effectCalls) {
|
|
2478
|
+
const dependencies = call.arguments[1]
|
|
2479
|
+
if (!ts.isArrayLiteralExpression(dependencies)) continue
|
|
2480
|
+
for (const dependency of dependencies.elements) {
|
|
2481
|
+
const value = unwrapExpression(dependency)
|
|
2482
|
+
if (ts.isPropertyAccessExpression(value) && ts.isIdentifier(unwrapExpression(value.expression)) && locals.has(unwrapExpression(value.expression).text)) add(unwrapExpression(value.expression).text, value.name.text, "effect")
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
const visit = node => {
|
|
2486
|
+
if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(unwrapExpression(node.expression)) && locals.has(unwrapExpression(node.expression).text)) {
|
|
2487
|
+
const map = ts.isPropertyAccessExpression(node.parent) && node.parent.expression === node && node.parent.name.text === "map" && ts.isCallExpression(node.parent.parent)
|
|
2488
|
+
add(unwrapExpression(node.expression).text, node.name.text, map ? "list" : "binding")
|
|
2489
|
+
}
|
|
2490
|
+
ts.forEachChild(node, visit)
|
|
2491
|
+
}
|
|
2492
|
+
visit(returned)
|
|
2493
|
+
return new Map([...uses].map(([local, fields]) => [local, [...fields].map(([field, consumers]) => ({ path: [field], consumers: ["binding", "effect", "list"].filter(consumer => consumers.has(consumer)) }))]))
|
|
2494
|
+
}
|
|
2495
|
+
|
|
2214
2496
|
function jsxCallHasDirectStateProp(call, setters) {
|
|
2215
2497
|
const attributes = ts.isJsxElement(call) ? call.openingElement.attributes : call.attributes
|
|
2216
2498
|
const stateNames = new Set(setters.values())
|
|
@@ -2238,12 +2520,30 @@ function componentHasDirectPropStateInitializer(component) {
|
|
|
2238
2520
|
return component.body.statements.some(statement => ts.isVariableStatement(statement) && statement.declarationList.declarations.some(declaration => declaration.initializer && ts.isCallExpression(declaration.initializer) && ts.isIdentifier(declaration.initializer.expression) && declaration.initializer.expression.text === "useState" && declaration.initializer.arguments.length === 1 && ts.isIdentifier(declaration.initializer.arguments[0]) && props.has(declaration.initializer.arguments[0].text)))
|
|
2239
2521
|
}
|
|
2240
2522
|
|
|
2241
|
-
function
|
|
2242
|
-
|
|
2523
|
+
function componentHasDirectObjectRef(component, ref) {
|
|
2524
|
+
if (!component || !ts.isBlock(component.body)) return false
|
|
2525
|
+
const declaration = component.body.statements.some(statement => ts.isVariableStatement(statement) && statement.declarationList.declarations.some(entry => ts.isIdentifier(entry.name) && entry.name.text === ref && entry.initializer && ts.isCallExpression(entry.initializer) && ts.isIdentifier(entry.initializer.expression) && entry.initializer.expression.text === "useRef" && entry.initializer.arguments.length === 1 && entry.initializer.arguments[0].kind === ts.SyntaxKind.NullKeyword))
|
|
2526
|
+
let attachments = 0
|
|
2527
|
+
let intrinsic = 0
|
|
2528
|
+
const visit = node => {
|
|
2529
|
+
if (ts.isJsxAttribute(node) && node.name.text === "ref" && node.initializer && ts.isJsxExpression(node.initializer) && ts.isIdentifier(node.initializer.expression) && node.initializer.expression.text === ref) {
|
|
2530
|
+
attachments++
|
|
2531
|
+
const element = node.parent?.parent
|
|
2532
|
+
const tag = ts.isJsxOpeningElement(element) || ts.isJsxSelfClosingElement(element) ? element.tagName : undefined
|
|
2533
|
+
if (ts.isIdentifier(tag) && tag.text[0] === tag.text[0].toLowerCase()) intrinsic++
|
|
2534
|
+
}
|
|
2535
|
+
ts.forEachChild(node, visit)
|
|
2536
|
+
}
|
|
2537
|
+
visit(component.body)
|
|
2538
|
+
return declaration && attachments === 1 && intrinsic === 1
|
|
2243
2539
|
}
|
|
2244
2540
|
|
|
2245
|
-
function
|
|
2246
|
-
|
|
2541
|
+
function directSetterLiteralCallback(node, setters) {
|
|
2542
|
+
node = node && unwrapExpression(node)
|
|
2543
|
+
if ((!ts.isArrowFunction(node) && !ts.isFunctionExpression(node)) || node.parameters.length || node.asteriskToken || node.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) return undefined
|
|
2544
|
+
const expression = ts.isBlock(node.body) ? node.body.statements.length === 1 && ts.isExpressionStatement(node.body.statements[0]) ? node.body.statements[0].expression : undefined : node.body
|
|
2545
|
+
if (!expression || !ts.isCallExpression(expression) || !ts.isIdentifier(expression.expression) || !setters.has(expression.expression.text) || expression.arguments.length !== 1 || !isPrimitiveDefaultLiteral(unwrapExpression(expression.arguments[0]))) return undefined
|
|
2546
|
+
return { setter: expression.expression.text, value: unwrapExpression(expression.arguments[0]) }
|
|
2247
2547
|
}
|
|
2248
2548
|
|
|
2249
2549
|
function directSetterPropEffect(component, setterProp) {
|
|
@@ -2597,6 +2897,7 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
|
|
|
2597
2897
|
}
|
|
2598
2898
|
const substitutions = new Map()
|
|
2599
2899
|
const acceptedProps = new Set()
|
|
2900
|
+
const propLocals = new Set()
|
|
2600
2901
|
let rest
|
|
2601
2902
|
const elements = component.parameters[0].name.elements
|
|
2602
2903
|
for (const [index, element] of elements.entries()) {
|
|
@@ -2609,10 +2910,15 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
|
|
|
2609
2910
|
if (element.initializer && !isSerializableStateLiteral(element.initializer)) fail(element.initializer, `${label} component prop defaults must be directly serializable primitive, plain-object, or array literals`)
|
|
2610
2911
|
const prop = (element.propertyName ?? element.name).text
|
|
2611
2912
|
acceptedProps.add(prop)
|
|
2913
|
+
propLocals.add(element.name.text)
|
|
2612
2914
|
substitutions.set(element.name.text, props.has(prop) ? props.get(prop) : element.initializer ?? factory.createIdentifier("undefined"))
|
|
2613
2915
|
}
|
|
2614
2916
|
const restEntries = [...props].filter(([prop]) => !acceptedProps.has(prop))
|
|
2615
2917
|
if (!rest) for (const [prop] of restEntries) fail(call, `Unknown ${label.toLowerCase()} component prop "${prop}"`)
|
|
2918
|
+
if (label === "Object-property component") {
|
|
2919
|
+
const mutation = componentPropertyMutation(component, propLocals)
|
|
2920
|
+
if (mutation) fail(mutation, "Object-property component props must remain immutable")
|
|
2921
|
+
}
|
|
2616
2922
|
const propAnalysis = elements.map(element => ({
|
|
2617
2923
|
name: (element.propertyName ?? element.name).getText(),
|
|
2618
2924
|
local: element.name.getText(),
|
|
@@ -2650,7 +2956,7 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
|
|
|
2650
2956
|
const substitutedProp = propReceiver ? substitutions.get(propReceiver.text) : undefined
|
|
2651
2957
|
const substitutedState = substitutedProp && ts.isIdentifier(unwrapExpression(substitutedProp)) ? unwrapExpression(substitutedProp).text : undefined
|
|
2652
2958
|
const directProp = ts.isIdentifier(initialArgument)
|
|
2653
|
-
const parentInitializer = substitutedState ? directStateInitializer(call, substitutedState) : undefined
|
|
2959
|
+
const parentInitializer = substitutedState ? directStateInitializer(nearestFunction(call), substitutedState) : undefined
|
|
2654
2960
|
const propInitializer = ordinaryHooks && substitutedState && ordinaryStateNames.has(substitutedState) && parentInitializer && (isPrimitiveDefaultLiteral(parentInitializer) || directProp && (ts.isObjectLiteralExpression(parentInitializer) || ts.isArrayLiteralExpression(parentInitializer)))
|
|
2655
2961
|
const rowItemProp = propReceiver && elements.find(element => !element.dotDotDotToken && ts.isIdentifier(element.name) && element.name.text === propReceiver.text)
|
|
2656
2962
|
const rowItemInitializer = !ordinaryHooks && directProp && substitutedState && rowItemProp && directProps.has((rowItemProp.propertyName ?? rowItemProp.name).text)
|
|
@@ -2702,6 +3008,7 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
|
|
|
2702
3008
|
continue
|
|
2703
3009
|
}
|
|
2704
3010
|
if (!ts.isIdentifier(declaration.name) || !declaration.initializer) fail(declaration, `${label} component locals must be initialized identifiers`)
|
|
3011
|
+
if (label === "Object-property component" && referencesComponentProperty(declaration.initializer, propLocals)) fail(declaration.initializer, "Object-property component property aliases are not supported; read the static prop.field path directly at each consumer")
|
|
2705
3012
|
const calculation = substituteClone(declaration.initializer, substitutions, factory, context)
|
|
2706
3013
|
calculations.push({ name: declaration.name.text, expression: calculation })
|
|
2707
3014
|
substitutions.set(declaration.name.text, calculation)
|
|
@@ -2740,6 +3047,7 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
|
|
|
2740
3047
|
ordinaryStates,
|
|
2741
3048
|
ordinaryRefs,
|
|
2742
3049
|
ordinaryIds,
|
|
3050
|
+
propertyUses: componentPropertyUses(component, returned, effectCalls),
|
|
2743
3051
|
propExpressions: props,
|
|
2744
3052
|
props: propAnalysis,
|
|
2745
3053
|
usesComponentId: ordinaryIds.length > 0
|
|
@@ -2754,8 +3062,7 @@ function isSerializableStateLiteral(node) {
|
|
|
2754
3062
|
return value.properties.every(property => ts.isPropertyAssignment(property) && !ts.isComputedPropertyName(property.name) && property.name.text !== "__proto__" && isSerializableStateLiteral(property.initializer))
|
|
2755
3063
|
}
|
|
2756
3064
|
|
|
2757
|
-
function directStateInitializer(
|
|
2758
|
-
const owner = nearestFunction(call)
|
|
3065
|
+
function directStateInitializer(owner, name) {
|
|
2759
3066
|
if (!owner || !ts.isBlock(owner.body)) return
|
|
2760
3067
|
for (const statement of owner.body.statements) {
|
|
2761
3068
|
if (!ts.isVariableStatement(statement)) continue
|
|
@@ -3086,10 +3393,34 @@ function importedSerializableCollectionNames(sourceFile, file, sourceFiles, sour
|
|
|
3086
3393
|
return new Set(importedSerializableCollections(sourceFile, file, sourceFiles, sourceIndex).keys())
|
|
3087
3394
|
}
|
|
3088
3395
|
|
|
3089
|
-
function importedSerializableCollections(sourceFile, file, sourceFiles, sourceIndex) {
|
|
3396
|
+
function importedSerializableCollections(sourceFile, file, sourceFiles, sourceIndex, onlyMapped = false) {
|
|
3090
3397
|
const collections = new Map()
|
|
3398
|
+
const mapped = new Set()
|
|
3399
|
+
const memoNames = new Set(["useMemo"])
|
|
3400
|
+
for (const statement of sourceFile.statements) if (ts.isImportDeclaration(statement) && ["react", "@kudzujs/core"].includes(statement.moduleSpecifier.text) && statement.importClause?.namedBindings && ts.isNamedImports(statement.importClause.namedBindings)) {
|
|
3401
|
+
for (const entry of statement.importClause.namedBindings.elements) if ((entry.propertyName ?? entry.name).text === "useMemo") memoNames.add(entry.name.text)
|
|
3402
|
+
}
|
|
3403
|
+
const collectRoot = node => {
|
|
3404
|
+
if (ts.isPropertyAccessExpression(node) && ["filter", "flatMap", "slice", "toSorted", "map"].includes(node.name.text)) {
|
|
3405
|
+
let expression = node.expression
|
|
3406
|
+
while (ts.isCallExpression(expression) || ts.isPropertyAccessExpression(expression)) expression = expression.expression
|
|
3407
|
+
if (ts.isIdentifier(expression)) mapped.add(expression.text)
|
|
3408
|
+
}
|
|
3409
|
+
ts.forEachChild(node, collectRoot)
|
|
3410
|
+
}
|
|
3411
|
+
const visit = node => {
|
|
3412
|
+
if (ts.isPropertyAccessExpression(node) && node.name.text === "map") {
|
|
3413
|
+
collectRoot(node)
|
|
3414
|
+
}
|
|
3415
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && memoNames.has(node.expression.text) && node.arguments[0]) collectRoot(node.arguments[0])
|
|
3416
|
+
ts.forEachChild(node, visit)
|
|
3417
|
+
}
|
|
3418
|
+
if (onlyMapped) {
|
|
3419
|
+
visit(sourceFile)
|
|
3420
|
+
if (!mapped.size) return collections
|
|
3421
|
+
}
|
|
3091
3422
|
for (const [name, binding] of clientImportBindings(sourceFile, file, sourceFiles)) {
|
|
3092
|
-
if (binding.kind !== "named") continue
|
|
3423
|
+
if (binding.kind !== "named" || onlyMapped && !mapped.has(name)) continue
|
|
3093
3424
|
const imported = parseSourceFile(binding.target, sourceIndex.get(binding.target))
|
|
3094
3425
|
for (const statement of imported.statements) {
|
|
3095
3426
|
if (!ts.isVariableStatement(statement) || !(statement.declarationList.flags & ts.NodeFlags.Const) || !statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword)) continue
|
|
@@ -3227,7 +3558,9 @@ function orderSourceStyles(entryFiles, sourceFiles, sourceIndex, staticFiles) {
|
|
|
3227
3558
|
const visit = file => {
|
|
3228
3559
|
if (seenSources.has(file)) return
|
|
3229
3560
|
seenSources.add(file)
|
|
3230
|
-
const
|
|
3561
|
+
const source = sourceIndex.get(file)
|
|
3562
|
+
if (importFreeTypeScriptModule(file, source)) return
|
|
3563
|
+
const sourceFile = parseSourceFile(file, source)
|
|
3231
3564
|
for (const statement of sourceFile.statements) {
|
|
3232
3565
|
if ((!ts.isImportDeclaration(statement) && !ts.isExportDeclaration(statement)) || !runtimeModuleReference(statement) || !statement.moduleSpecifier || !ts.isStringLiteral(statement.moduleSpecifier) || !statement.moduleSpecifier.text.startsWith(".")) continue
|
|
3233
3566
|
const specifier = statement.moduleSpecifier.text
|
|
@@ -3338,7 +3671,7 @@ const printHandlerModule = createHandlerCodegen({
|
|
|
3338
3671
|
const { normalizeReactMigrationSyntax, validateUseIdSyntax } = createReactMigrationPass({ cloneAst, jsxTagName })
|
|
3339
3672
|
const normalizeReactRouterSyntax = createRouterPass({ withBase })
|
|
3340
3673
|
|
|
3341
|
-
return { collectClientModules, compileClientModule, compiledPath, compileSource, layoutExportError, orderSourceStyles, reachableSourceFiles, safeStaticFiles }
|
|
3674
|
+
return { collectClientModules, compileClientModule, compiledPath, compileSource, compileSourceAsync, layoutExportError, orderSourceStyles, reachableSourceFiles, safeStaticFiles }
|
|
3342
3675
|
}
|
|
3343
3676
|
|
|
3344
3677
|
const currentCompiler = sourceIndex => createSourceCompiler(createProjectSession(process.cwd(), { sourceIndex }))
|