@kudzujs/core 0.8.22 → 0.8.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/GOAL_B.md +5 -3
- package/MIGRATION_ROADMAP.md +14 -0
- package/PERFORMANCE.md +134 -1
- package/README.md +2 -2
- package/RELEASES.md +71 -2
- package/docs/next-architecture/README.md +4 -4
- package/docs/next-architecture/compiler-current-architecture.md +13 -14
- package/docs/next-architecture/goal-a-compiler-foundation.md +5 -5
- package/docs/next-architecture/goal-b-optimization-benchmarks.md +18 -6
- package/docs/next-architecture/versioning.md +5 -3
- package/framework/README.md +2 -2
- package/framework/build.mjs +29 -3017
- package/framework/compiler/normalization-pipeline.mjs +3 -2
- package/framework/compiler/path-helpers.mjs +18 -0
- package/framework/compiler/source-compiler.mjs +2969 -0
- package/framework/compiler/source-graph.mjs +29 -0
- package/framework/compiler/worker-compiler.mjs +9 -2
- package/framework/dev-server.mjs +1 -8
- package/framework/list-runtime.js +2 -1
- package/package.json +3 -1
|
@@ -0,0 +1,2969 @@
|
|
|
1
|
+
import { readFile, realpath, stat } from "node:fs/promises"
|
|
2
|
+
import { dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path"
|
|
3
|
+
import ts from "typescript"
|
|
4
|
+
import { createComponentAnalysisSession } from "./analysis/component-analysis.mjs"
|
|
5
|
+
import { normalizeEffectAnimationFrameRefs } from "./animation-frame-pass.mjs"
|
|
6
|
+
import { bindingNames, containsJsx, effectReturns, functionVarDeclaresName, importDeclarationNames, isFunctionLike, isLocalConst, isReferenceIdentifier, isShadowedByParameter, isShadowedIdentifier, isUnshadowedGlobal, nearestFunction, referenceIdentifiers, referencesIdentifier, sourceLocation, sourceNodeError, statementDeclaresName, unwrapExpression } from "./ast-helpers.mjs"
|
|
7
|
+
import { normalizeMediaQueryExternalStores, normalizeNavigatorCapabilityConditions } from "./browser-signal-passes.mjs"
|
|
8
|
+
import { analyzeCollectionPipeline, collectionExpression, collectionParameters, isArrayFromCall, mutatingCollectionMethods as mutatingListMethods, pureCollectionMathMethods as pureMathMethods, pureCollectionMethods as pureListMethods } from "./collection-analysis.mjs"
|
|
9
|
+
import { normalizeCustomHookTimerRefs } from "./custom-hook-timer-pass.mjs"
|
|
10
|
+
import { captureNames, createDescriptorSession, createSemanticArtifact, nativeCaptureNames, referencedReducerDispatches, referencedStateNames } from "./descriptor-session.mjs"
|
|
11
|
+
import { analyzeEffectDependencies, validateEffectOwnedBrowserResources } from "./effect-analysis.mjs"
|
|
12
|
+
import { createHandlerCodegen } from "./handler-codegen.mjs"
|
|
13
|
+
import { createHandlerLowering } from "./handler-lowering.mjs"
|
|
14
|
+
import { createCommandSpecializer } from "./optimize/command-specialization.mjs"
|
|
15
|
+
import { applyNormalizationPasses } from "./normalization-pipeline.mjs"
|
|
16
|
+
import { assetPath, relativeModulePath, withBase } from "./path-helpers.mjs"
|
|
17
|
+
import { createReactMigrationPass, reactMemoExpression } from "./react-migration-pass.mjs"
|
|
18
|
+
import { normalizeRenderControlFlow } from "./render-control-pass.mjs"
|
|
19
|
+
import { createRouterPass } from "./router-pass.mjs"
|
|
20
|
+
import { parseSourceFile, resolveSourceImport, runtimeModuleReference } from "./source-graph.mjs"
|
|
21
|
+
import { createWorkerCompiler } from "./worker-compiler.mjs"
|
|
22
|
+
import { createZustandPass } from "./zustand-pass.mjs"
|
|
23
|
+
|
|
24
|
+
const root = process.cwd()
|
|
25
|
+
const sourceDirectory = join(root, "src")
|
|
26
|
+
const pagesDirectory = join(sourceDirectory, "pages")
|
|
27
|
+
const workDirectory = join(root, ".kudzu")
|
|
28
|
+
const staticAssetExtensions = new Set([".avif", ".gif", ".ico", ".jpeg", ".jpg", ".otf", ".png", ".svg", ".ttf", ".webp", ".woff", ".woff2"])
|
|
29
|
+
|
|
30
|
+
export function compileSource(file, sourceFiles, sourceIndex, staticFiles, cssModules, base) {
|
|
31
|
+
const importedAssets = new Set()
|
|
32
|
+
const source = sourceIndex.get(file)
|
|
33
|
+
const semantic = createSemanticArtifact(relative(root, file).replaceAll(sep, "/"))
|
|
34
|
+
const handlerPath = `handlers/${relative(sourceDirectory, file).replaceAll(sep, "/").replace(/\.(?:ts|tsx)$/, ".js")}`
|
|
35
|
+
const result = ts.transpileModule(source, {
|
|
36
|
+
fileName: file,
|
|
37
|
+
compilerOptions: {
|
|
38
|
+
target: ts.ScriptTarget.ES2022,
|
|
39
|
+
module: ts.ModuleKind.ESNext,
|
|
40
|
+
jsx: ts.JsxEmit.ReactJSX,
|
|
41
|
+
jsxImportSource: "@kudzujs/core"
|
|
42
|
+
},
|
|
43
|
+
transformers: { before: [createKudzuTransformer({ semantic, handlerUrl: assetPath(base, `assets/${handlerPath}`), file, sourceFiles, sourceIndex, staticFiles, importedAssets, cssModules, base })] },
|
|
44
|
+
reportDiagnostics: true
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const errors = result.diagnostics?.filter(diagnostic => diagnostic.category === ts.DiagnosticCategory.Error) ?? []
|
|
48
|
+
if (errors.length) {
|
|
49
|
+
throw new Error(errors.map(error => ts.flattenDiagnosticMessageText(error.messageText, "\n")).join("\n"))
|
|
50
|
+
}
|
|
51
|
+
const packageReference = emittedPackageReference(result.outputText, file, new Set(["react", "react-router-dom"]))
|
|
52
|
+
if (packageReference) throw new Error(`${relative(root, file)} Runtime ${packageReference} module references are not supported`)
|
|
53
|
+
|
|
54
|
+
const output = compiledPath(file)
|
|
55
|
+
const { componentAnalysis, moduleIR } = semantic
|
|
56
|
+
const sourceResult = { file: relative(root, file).replaceAll(sep, "/"), componentAnalysis, moduleIR, buildModule: { path: relative(root, output).replaceAll(sep, "/"), code: result.outputText }, importedAssets: [...importedAssets].map(file => relative(root, file).replaceAll(sep, "/")).sort() }
|
|
57
|
+
const moduleHandlers = moduleIR.handlers.filter(handler => handler.kind === "module-export")
|
|
58
|
+
if (!moduleHandlers.length && !moduleIR.bindings.length) {
|
|
59
|
+
normalizeModulePaths(moduleIR)
|
|
60
|
+
return sourceResult
|
|
61
|
+
}
|
|
62
|
+
const moduleSource = printHandlerModule({ moduleIR, handlerPath })
|
|
63
|
+
const moduleResult = ts.transpileModule(moduleSource, {
|
|
64
|
+
compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext },
|
|
65
|
+
reportDiagnostics: true
|
|
66
|
+
})
|
|
67
|
+
const moduleErrors = moduleResult.diagnostics?.filter(diagnostic => diagnostic.category === ts.DiagnosticCategory.Error) ?? []
|
|
68
|
+
if (moduleErrors.length) throw new Error(moduleErrors.map(error => ts.flattenDiagnosticMessageText(error.messageText, "\n")).join("\n"))
|
|
69
|
+
normalizeModulePaths(moduleIR)
|
|
70
|
+
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) }
|
|
71
|
+
return sourceResult
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function normalizeModulePaths(moduleIR) {
|
|
75
|
+
const normalize = target => isAbsolute(target) ? relative(root, target).replaceAll(sep, "/") : target
|
|
76
|
+
moduleIR.imports = moduleIR.imports.map(entry => entry.package ? entry : { ...entry, target: normalize(entry.target) })
|
|
77
|
+
moduleIR.clientModules = moduleIR.clientModules.map(normalize)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function emittedPackageReference(source, file, packages) {
|
|
81
|
+
const sourceFile = ts.createSourceFile(file, source, ts.ScriptTarget.ES2022, true, ts.ScriptKind.JS)
|
|
82
|
+
let found
|
|
83
|
+
const visit = node => {
|
|
84
|
+
if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier) && packages.has(node.moduleSpecifier.text)) found = node.moduleSpecifier.text
|
|
85
|
+
if (ts.isCallExpression(node) && (node.expression.kind === ts.SyntaxKind.ImportKeyword || ts.isIdentifier(node.expression) && node.expression.text === "require") && ts.isStringLiteral(node.arguments[0]) && packages.has(node.arguments[0].text)) found = node.arguments[0].text
|
|
86
|
+
if (!found) ts.forEachChild(node, visit)
|
|
87
|
+
}
|
|
88
|
+
visit(sourceFile)
|
|
89
|
+
return found
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function reachableSourceFiles(entries, sourceFiles, sourceIndex) {
|
|
93
|
+
const reachable = new Set()
|
|
94
|
+
const queue = [...entries]
|
|
95
|
+
while (queue.length) {
|
|
96
|
+
const file = queue.pop()
|
|
97
|
+
if (reachable.has(file)) continue
|
|
98
|
+
reachable.add(file)
|
|
99
|
+
const sourceFile = parseSourceFile(file, sourceIndex.get(file))
|
|
100
|
+
const visit = node => {
|
|
101
|
+
const specifier = (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && runtimeModuleReference(node) && node.moduleSpecifier
|
|
102
|
+
if (specifier && ts.isStringLiteral(specifier) && specifier.text.startsWith(".") && !isStaticImport(specifier.text)) {
|
|
103
|
+
try { queue.push(resolveSourceImport(file, specifier.text, sourceFiles)) } catch {}
|
|
104
|
+
}
|
|
105
|
+
const worker = workerCompiler.candidate(node, sourceFile)
|
|
106
|
+
if (worker && ts.isStringLiteral(worker.url.arguments[0]) && worker.url.arguments[0].text.endsWith(".worker.ts")) {
|
|
107
|
+
try { queue.push(resolveSourceImport(file, worker.url.arguments[0].text, sourceFiles)) } catch {}
|
|
108
|
+
}
|
|
109
|
+
ts.forEachChild(node, visit)
|
|
110
|
+
}
|
|
111
|
+
visit(sourceFile)
|
|
112
|
+
}
|
|
113
|
+
return [...reachable].sort()
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function normalizeClsxSyntax(sourceFile, factory, context) {
|
|
117
|
+
const names = new Set()
|
|
118
|
+
for (const statement of sourceFile.statements) {
|
|
119
|
+
if (!ts.isImportDeclaration(statement) || statement.importClause?.isTypeOnly || !ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text !== "clsx") continue
|
|
120
|
+
if (statement.importClause?.name) names.add(statement.importClause.name.text)
|
|
121
|
+
const bindings = statement.importClause?.namedBindings
|
|
122
|
+
if (bindings && ts.isNamedImports(bindings)) for (const entry of bindings.elements) if (!entry.isTypeOnly && (entry.propertyName ?? entry.name).text === "clsx") names.add(entry.name.text)
|
|
123
|
+
}
|
|
124
|
+
if (!names.size) return sourceFile
|
|
125
|
+
|
|
126
|
+
const lower = node => {
|
|
127
|
+
node = unwrapExpression(node)
|
|
128
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) || ts.isNumericLiteral(node)) return node
|
|
129
|
+
if (node.kind === ts.SyntaxKind.FalseKeyword || node.kind === ts.SyntaxKind.NullKeyword) return factory.createStringLiteral("")
|
|
130
|
+
if (ts.isConditionalExpression(node)) return factory.updateConditionalExpression(node, node.condition, node.questionToken, lower(node.whenTrue), node.colonToken, lower(node.whenFalse))
|
|
131
|
+
if (ts.isArrayLiteralExpression(node)) return combine(node.elements.map(lower))
|
|
132
|
+
if (ts.isObjectLiteralExpression(node)) return combine(node.properties.map(property => {
|
|
133
|
+
if (!ts.isPropertyAssignment(property) || property.name && ts.isComputedPropertyName(property.name)) throw sourceNodeError(property, sourceFile, "clsx() object arguments require ordinary key/value properties")
|
|
134
|
+
const name = property.name
|
|
135
|
+
const value = name && (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) ? name.text : undefined
|
|
136
|
+
if (value === undefined) throw sourceNodeError(property, sourceFile, "clsx() object keys must be identifiers or literals")
|
|
137
|
+
return factory.createConditionalExpression(property.initializer, undefined, factory.createStringLiteral(value), undefined, factory.createStringLiteral(""))
|
|
138
|
+
}))
|
|
139
|
+
throw sourceNodeError(node, sourceFile, "clsx() arguments must be string/number literals, literal arrays, literal objects, or conditionals")
|
|
140
|
+
}
|
|
141
|
+
const combine = entries => entries.length ? entries.reduce((result, entry) => factory.createBinaryExpression(factory.createBinaryExpression(result, factory.createToken(ts.SyntaxKind.PlusToken), factory.createStringLiteral(" ")), factory.createToken(ts.SyntaxKind.PlusToken), entry)) : factory.createStringLiteral("")
|
|
142
|
+
|
|
143
|
+
const visitor = node => {
|
|
144
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && names.has(node.expression.text) && !isShadowedIdentifier(node.expression, sourceFile)) return combine(node.arguments.map(lower))
|
|
145
|
+
if (ts.isIdentifier(node) && names.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, sourceFile) && !(ts.isCallExpression(node.parent) && node.parent.expression === node)) throw sourceNodeError(node, sourceFile, "clsx imports may only be called directly")
|
|
146
|
+
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text === "clsx") {
|
|
147
|
+
const clause = node.importClause
|
|
148
|
+
if (!clause || clause.isTypeOnly) return node
|
|
149
|
+
let bindings = clause.namedBindings
|
|
150
|
+
if (bindings && ts.isNamedImports(bindings)) {
|
|
151
|
+
const elements = bindings.elements.filter(entry => entry.isTypeOnly || (entry.propertyName ?? entry.name).text !== "clsx")
|
|
152
|
+
bindings = elements.length ? factory.updateNamedImports(bindings, elements) : undefined
|
|
153
|
+
}
|
|
154
|
+
const defaultName = clause.name && names.has(clause.name.text) ? undefined : clause.name
|
|
155
|
+
if (!defaultName && !bindings) return undefined
|
|
156
|
+
return factory.updateImportDeclaration(node, node.modifiers, factory.updateImportClause(clause, clause.isTypeOnly, defaultName, bindings), node.moduleSpecifier, node.attributes)
|
|
157
|
+
}
|
|
158
|
+
return ts.visitEachChild(node, visitor, context)
|
|
159
|
+
}
|
|
160
|
+
return ts.visitNode(sourceFile, visitor)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function normalizeLazyStateInitializers(sourceFile, factory, context, file, sourceFiles, sourceIndex) {
|
|
164
|
+
const bindings = new Set()
|
|
165
|
+
for (const statement of sourceFile.statements) {
|
|
166
|
+
if (!ts.isImportDeclaration(statement) || statement.importClause?.isTypeOnly || !ts.isStringLiteral(statement.moduleSpecifier) || !["react", "@kudzujs/core"].includes(statement.moduleSpecifier.text)) continue
|
|
167
|
+
const named = statement.importClause?.namedBindings
|
|
168
|
+
if (named && ts.isNamedImports(named)) for (const entry of named.elements) {
|
|
169
|
+
const imported = (entry.propertyName ?? entry.name).text
|
|
170
|
+
if (!entry.isTypeOnly && ["useReducer", "useState"].includes(imported) && entry.name.text === imported) bindings.add(imported)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (!bindings.size) return sourceFile
|
|
174
|
+
const imports = clientImportBindings(sourceFile, file, sourceFiles)
|
|
175
|
+
const visitor = node => {
|
|
176
|
+
if (bindings.has("useReducer") && ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "useReducer" && !isShadowedIdentifier(node.expression, sourceFile) && node.arguments.length === 3) {
|
|
177
|
+
const initialArg = node.arguments[1]
|
|
178
|
+
const initializer = node.arguments[2]
|
|
179
|
+
let declaration
|
|
180
|
+
if (ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer)) declaration = initializer
|
|
181
|
+
else if (ts.isIdentifier(initializer)) {
|
|
182
|
+
declaration = localComponentDeclaration(sourceFile, initializer.text)
|
|
183
|
+
const binding = imports.get(initializer.text)
|
|
184
|
+
if (!declaration && binding && binding.kind !== "namespace") {
|
|
185
|
+
try {
|
|
186
|
+
declaration = resolveComponentExport(binding.target, binding.kind === "default" ? "default" : binding.imported, target => parseSourceFile(target, sourceIndex.get(target)), sourceFiles)
|
|
187
|
+
} catch {}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (!declaration || declaration.parameters.length !== 1 || !ts.isIdentifier(declaration.parameters[0].name) || declaration.parameters[0].initializer || declaration.parameters[0].dotDotDotToken || declaration.asteriskToken || declaration.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword) || ts.isFunctionExpression(initializer) && initializer.name) throw sourceNodeError(initializer, sourceFile, "Lazy useReducer() requires one inline, same-file, or relative-imported synchronous one-parameter initializer")
|
|
191
|
+
if (!isSerializableStateLiteral(initialArg)) throw sourceNodeError(initialArg, sourceFile, "Lazy useReducer() initial argument must be directly serializable")
|
|
192
|
+
const expression = reactMemoExpression(declaration)
|
|
193
|
+
const lowered = expression && substituteClone(expression, new Map([[declaration.parameters[0].name.text, initialArg]]), factory, context)
|
|
194
|
+
if (!lowered || !isSerializableStateLiteral(lowered)) throw sourceNodeError(initializer, sourceFile, "Lazy useReducer() initializer must directly return a serializable primitive, plain-object, or array literal derived only from its initial argument")
|
|
195
|
+
return factory.updateCallExpression(node, node.expression, node.typeArguments, [node.arguments[0], synthesizeSerializableStateLiteral(lowered, factory)])
|
|
196
|
+
}
|
|
197
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && bindings.has(node.expression.text) && !isShadowedIdentifier(node.expression, sourceFile) && node.arguments[0] && (ts.isArrowFunction(node.arguments[0]) || ts.isFunctionExpression(node.arguments[0]))) {
|
|
198
|
+
const initializer = node.arguments[0]
|
|
199
|
+
if (node.arguments.length !== 1 || initializer.parameters.length || initializer.asteriskToken || initializer.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword) || ts.isFunctionExpression(initializer) && initializer.name) throw sourceNodeError(initializer, sourceFile, "Lazy useState() requires one anonymous synchronous zero-parameter initializer")
|
|
200
|
+
const expression = ts.isBlock(initializer.body)
|
|
201
|
+
? initializer.body.statements.length === 1 && ts.isReturnStatement(initializer.body.statements[0]) ? initializer.body.statements[0].expression : undefined
|
|
202
|
+
: initializer.body
|
|
203
|
+
if (!expression || !isSerializableStateLiteral(expression)) throw sourceNodeError(initializer.body, sourceFile, "Lazy useState() initializer must return one directly serializable primitive, plain-object, or array literal")
|
|
204
|
+
return factory.updateCallExpression(node, node.expression, node.typeArguments, [cloneAst(expression, factory, context)])
|
|
205
|
+
}
|
|
206
|
+
return ts.visitEachChild(node, visitor, context)
|
|
207
|
+
}
|
|
208
|
+
return ts.visitNode(sourceFile, visitor)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function normalizeCompilerSource(sourceFile, { base, context, file, importedCollections, importedStaticCollections, sourceFiles, sourceIndex }) {
|
|
212
|
+
const factory = context.factory
|
|
213
|
+
let customHookTimerStates = new Set()
|
|
214
|
+
sourceFile = applyNormalizationPasses(sourceFile, [
|
|
215
|
+
...(importedStaticCollections ? [source => normalizeImportedStaticCollections(source, importedStaticCollections, factory, context)] : []),
|
|
216
|
+
source => normalizeReactRouterSyntax(source, factory, context, base),
|
|
217
|
+
source => normalizeClsxSyntax(source, factory, context),
|
|
218
|
+
source => normalizeMediaQueryExternalStores(source, factory, context),
|
|
219
|
+
source => normalizeReactMigrationSyntax(source, factory, context, importedCollections ?? importedSerializableCollectionNames(source, file, sourceFiles, sourceIndex)),
|
|
220
|
+
source => normalizeNavigatorCapabilityConditions(source, factory, context),
|
|
221
|
+
source => normalizeEffectAnimationFrameRefs(source, factory, context),
|
|
222
|
+
source => {
|
|
223
|
+
const result = normalizeCustomHookTimerRefs(source, factory, context)
|
|
224
|
+
customHookTimerStates = result.timerStates
|
|
225
|
+
return result.sourceFile
|
|
226
|
+
},
|
|
227
|
+
source => {
|
|
228
|
+
validateUseIdSyntax(source)
|
|
229
|
+
return source
|
|
230
|
+
},
|
|
231
|
+
source => normalizeLazyStateInitializers(source, factory, context, file, sourceFiles, sourceIndex),
|
|
232
|
+
source => normalizeZustandMigrationSyntax(source, factory, context),
|
|
233
|
+
source => normalizeRenderControlFlow(source, factory, context),
|
|
234
|
+
source => {
|
|
235
|
+
workerCompiler.rejectOrdinaryImports(source, file, sourceFiles)
|
|
236
|
+
return source
|
|
237
|
+
}
|
|
238
|
+
])
|
|
239
|
+
return { sourceFile, customHookTimerStates }
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourceIndex, staticFiles, importedAssets, cssModules, base }) {
|
|
243
|
+
const { moduleIR } = semantic
|
|
244
|
+
return context => sourceFile => {
|
|
245
|
+
const hasLinkElements = /<link/i.test(sourceFile.text)
|
|
246
|
+
const importedStaticCollections = importedSerializableCollections(sourceFile, file, sourceFiles, sourceIndex)
|
|
247
|
+
const importedCollections = new Set(importedStaticCollections.keys())
|
|
248
|
+
const normalized = normalizeCompilerSource(sourceFile, { base, context, file, importedCollections, importedStaticCollections, sourceFiles, sourceIndex })
|
|
249
|
+
sourceFile = normalized.sourceFile
|
|
250
|
+
const { customHookTimerStates } = normalized
|
|
251
|
+
const factory = context.factory
|
|
252
|
+
const sourceName = source => relative(root, source.fileName).replaceAll(sep, "/")
|
|
253
|
+
const componentAnalysis = createComponentAnalysisSession(semantic.componentAnalysis)
|
|
254
|
+
const descriptors = createDescriptorSession({
|
|
255
|
+
semantic,
|
|
256
|
+
handlerUrl,
|
|
257
|
+
factory,
|
|
258
|
+
context,
|
|
259
|
+
compileEventCommand,
|
|
260
|
+
handlerLowering,
|
|
261
|
+
isPrimitiveLiteral: isPrimitiveDefaultLiteral,
|
|
262
|
+
sourceName,
|
|
263
|
+
rejectWorkerConstructions: expression => workerCompiler.rejectConstructions(expression, expression.getSourceFile(), "Relative TypeScript Worker construction is only supported directly inside an inline useEffect() callback")
|
|
264
|
+
})
|
|
265
|
+
const importBindings = clientImportBindings(sourceFile, file, sourceFiles)
|
|
266
|
+
const packageBindings = packageImportBindings(sourceFile)
|
|
267
|
+
for (const [name] of packageBindings) {
|
|
268
|
+
const references = referenceIdentifiers(sourceFile, name)
|
|
269
|
+
const invalid = references.find(reference => !insideJsxEventHandler(reference, sourceFile))
|
|
270
|
+
if (invalid) throw sourceNodeError(invalid, sourceFile, `Package import ${JSON.stringify(name)} may only be referenced directly inside JSX event handlers`)
|
|
271
|
+
}
|
|
272
|
+
const hasUseEffectImport = sourceFile.statements.some(statement => ts.isImportDeclaration(statement) && ["@kudzujs/core", "react"].includes(statement.moduleSpecifier.text) && statement.importClause?.namedBindings && ts.isNamedImports(statement.importClause.namedBindings) && statement.importClause.namedBindings.elements.some(entry => !entry.propertyName && entry.name.text === "useEffect"))
|
|
273
|
+
const importedSourceCache = new Map()
|
|
274
|
+
const importedSource = target => {
|
|
275
|
+
let result = importedSourceCache.get(target)
|
|
276
|
+
if (!result) {
|
|
277
|
+
result = normalizeCompilerSource(parseSourceFile(target, sourceIndex.get(target)), { base, context, file: target, sourceFiles, sourceIndex })
|
|
278
|
+
importedSourceCache.set(target, result)
|
|
279
|
+
}
|
|
280
|
+
return result.sourceFile
|
|
281
|
+
}
|
|
282
|
+
const importedCollectionTransforms = new Map()
|
|
283
|
+
const importedCalculationFunctions = new Map()
|
|
284
|
+
for (const [name, binding] of importBindings) {
|
|
285
|
+
if (binding.kind === "namespace") continue
|
|
286
|
+
try {
|
|
287
|
+
importedCollectionTransforms.set(name, resolveComponentExport(binding.target, binding.kind === "default" ? "default" : binding.imported, target => parseSourceFile(target, sourceIndex.get(target)), sourceFiles))
|
|
288
|
+
} catch {}
|
|
289
|
+
}
|
|
290
|
+
const settersByFunction = new Map()
|
|
291
|
+
const stateOwnersByFunction = new Map()
|
|
292
|
+
const localStateSettersByFunction = new Map()
|
|
293
|
+
const reducersByFunction = new Map()
|
|
294
|
+
const zustandStores = new Map()
|
|
295
|
+
const resolvedZustandStore = entry => {
|
|
296
|
+
const exportName = entry.kind === "default" ? "default" : entry.imported
|
|
297
|
+
const key = `${entry.target}:${exportName}`
|
|
298
|
+
if (zustandStores.has(key)) return zustandStores.get(key)
|
|
299
|
+
const targetSource = parseSourceFile(entry.target, sourceIndex.get(entry.target))
|
|
300
|
+
const store = analyzeZustandStores(targetSource).get(exportName)
|
|
301
|
+
zustandStores.set(key, store)
|
|
302
|
+
return store
|
|
303
|
+
}
|
|
304
|
+
const functions = new Map()
|
|
305
|
+
const customHookFunctionsByOwner = new Map()
|
|
306
|
+
const customHookPrivateFields = new WeakMap()
|
|
307
|
+
const components = new Map()
|
|
308
|
+
const contexts = new Set()
|
|
309
|
+
const customHooks = new Map()
|
|
310
|
+
const jsxLocalDeclarations = new Map()
|
|
311
|
+
const jsxLocalsByFunction = new Map()
|
|
312
|
+
const listLocalDeclarations = []
|
|
313
|
+
const listLocalUses = []
|
|
314
|
+
const analysisSource = node => {
|
|
315
|
+
const original = ts.getOriginalNode(node)
|
|
316
|
+
return original.pos >= 0 && original.end >= 0 ? { file: sourceName(original.getSourceFile()), start: original.getStart(), end: original.end } : undefined
|
|
317
|
+
}
|
|
318
|
+
const analyzedProps = owner => {
|
|
319
|
+
if (owner.parameters.length !== 1 || !ts.isObjectBindingPattern(owner.parameters[0].name)) return []
|
|
320
|
+
return owner.parameters[0].name.elements.map(element => ({
|
|
321
|
+
name: (element.propertyName ?? element.name).getText(),
|
|
322
|
+
local: element.name.getText(),
|
|
323
|
+
...(element.dotDotDotToken ? { rest: true } : {}),
|
|
324
|
+
...(element.initializer ? { hasDefault: true } : {})
|
|
325
|
+
}))
|
|
326
|
+
}
|
|
327
|
+
const ownerName = owner => owner.name?.text ?? (ts.isVariableDeclaration(owner.parent) && ts.isIdentifier(owner.parent.name) ? owner.parent.name.text : "anonymous")
|
|
328
|
+
const ensureOwner = (owner, kind = "component") => componentAnalysis.registerOwner(owner, { kind, name: ownerName(owner), props: analyzedProps(owner), source: analysisSource(owner) })
|
|
329
|
+
const registerState = (owner, state, setter, kind, node, externalOwner) => {
|
|
330
|
+
const ownerRecord = ensureOwner(owner)
|
|
331
|
+
const stateOwner = externalOwner ?? `owner:${ownerRecord.slot}`
|
|
332
|
+
const stateOwners = stateOwnersByFunction.get(owner) ?? new Map()
|
|
333
|
+
stateOwners.set(state, stateOwner)
|
|
334
|
+
stateOwnersByFunction.set(owner, stateOwners)
|
|
335
|
+
return componentAnalysis.registerState(owner, { name: state, setter, kind, ...(externalOwner ? { owner: externalOwner } : {}), source: analysisSource(node) })
|
|
336
|
+
}
|
|
337
|
+
const stateOwnersForNode = node => {
|
|
338
|
+
for (let current = node.parent; current; current = current.parent) {
|
|
339
|
+
if (isFunctionLike(current) && stateOwnersByFunction.has(current)) return stateOwnersByFunction.get(current)
|
|
340
|
+
}
|
|
341
|
+
return new Map()
|
|
342
|
+
}
|
|
343
|
+
const fallbackOwner = node => {
|
|
344
|
+
for (let current = node.parent; current; current = current.parent) {
|
|
345
|
+
const owner = isFunctionLike(current) ? componentAnalysis.owner(current) : undefined
|
|
346
|
+
if (owner) return `owner:${owner.slot}`
|
|
347
|
+
}
|
|
348
|
+
return "module"
|
|
349
|
+
}
|
|
350
|
+
let usesBehavior = false
|
|
351
|
+
let usesBinding = false
|
|
352
|
+
let usesConditional = false
|
|
353
|
+
let usesList = false
|
|
354
|
+
let usesListEffects = false
|
|
355
|
+
let usesListItem = false
|
|
356
|
+
let usesRowState = false
|
|
357
|
+
let usesRowRef = false
|
|
358
|
+
let usesComponentState = false
|
|
359
|
+
let usesComponentId = false
|
|
360
|
+
let usesComponentRef = false
|
|
361
|
+
let usesComponentEffects = false
|
|
362
|
+
|
|
363
|
+
const resolveContextHook = (returned, hookSource) => {
|
|
364
|
+
if (!hasFrameworkImport(hookSource, "useContext")) throw sourceNodeError(returned.expression, hookSource, "Relative Context hooks must call useContext imported from react or @kudzujs/core")
|
|
365
|
+
if (returned.arguments.length !== 1 || !ts.isIdentifier(returned.arguments[0])) throw sourceNodeError(returned, hookSource, "Relative Context hooks must directly return useContext(ContextIdentifier)")
|
|
366
|
+
const contextName = returned.arguments[0].text
|
|
367
|
+
let providerSource = hookSource
|
|
368
|
+
let providerContextName = contextName
|
|
369
|
+
const hookImports = clientImportBindings(hookSource, hookSource.fileName, sourceFiles)
|
|
370
|
+
if (hookImports.has(contextName)) {
|
|
371
|
+
const binding = hookImports.get(contextName)
|
|
372
|
+
if (binding.kind === "namespace" || binding.kind === "default") throw sourceNodeError(returned.arguments[0], hookSource, "Relative Context hooks require a named Context import")
|
|
373
|
+
providerSource = importedSource(binding.target)
|
|
374
|
+
providerContextName = binding.imported
|
|
375
|
+
}
|
|
376
|
+
const hasContext = hasFrameworkImport(providerSource, "createContext") && providerSource.statements.some(statement => ts.isVariableStatement(statement) && statement.declarationList.declarations.some(declaration => ts.isIdentifier(declaration.name) && declaration.name.text === providerContextName && declaration.initializer && ts.isCallExpression(declaration.initializer) && ts.isIdentifier(declaration.initializer.expression) && declaration.initializer.expression.text === "createContext"))
|
|
377
|
+
if (!hasContext) throw sourceNodeError(returned.arguments[0], hookSource, "Relative Context hooks require a local or named relative createContext() declaration")
|
|
378
|
+
|
|
379
|
+
const providers = []
|
|
380
|
+
const findProviders = node => {
|
|
381
|
+
if (ts.isJsxAttribute(node) && node.name.text === "value") {
|
|
382
|
+
const element = node.parent?.parent
|
|
383
|
+
const tag = ts.isJsxOpeningElement(element) || ts.isJsxSelfClosingElement(element) ? element.tagName : undefined
|
|
384
|
+
if (ts.isPropertyAccessExpression(tag) && tag.name.text === "Provider" && ts.isIdentifier(tag.expression) && tag.expression.text === providerContextName) providers.push(node)
|
|
385
|
+
}
|
|
386
|
+
ts.forEachChild(node, findProviders)
|
|
387
|
+
}
|
|
388
|
+
findProviders(providerSource)
|
|
389
|
+
if (providers.length !== 1) throw sourceNodeError(returned.arguments[0], hookSource, "Relative Context hooks require exactly one Provider value in the Context module")
|
|
390
|
+
const provider = providers[0]
|
|
391
|
+
const value = provider.initializer && ts.isJsxExpression(provider.initializer) && provider.initializer.expression ? unwrapExpression(provider.initializer.expression) : undefined
|
|
392
|
+
if (!value || !ts.isObjectLiteralExpression(value)) throw sourceNodeError(provider, providerSource, "Context Provider value must be one direct object literal")
|
|
393
|
+
const owner = nearestFunction(provider)
|
|
394
|
+
if (!owner) throw sourceNodeError(provider, providerSource, "Context Provider value must be returned by a component")
|
|
395
|
+
const stateOwner = `external:${sourceName(providerSource)}:${owner.getStart(providerSource)}`
|
|
396
|
+
|
|
397
|
+
const states = new Map()
|
|
398
|
+
const callbacks = new Map()
|
|
399
|
+
const hasUseState = hasFrameworkImport(providerSource, "useState")
|
|
400
|
+
const collectProviderBindings = node => {
|
|
401
|
+
if (ts.isVariableDeclaration(node) && nearestFunction(node) === owner) {
|
|
402
|
+
if (hasUseState && ts.isArrayBindingPattern(node.name) && node.initializer && ts.isCallExpression(node.initializer) && ts.isIdentifier(node.initializer.expression) && node.initializer.expression.text === "useState") {
|
|
403
|
+
const [state, setter] = node.name.elements
|
|
404
|
+
if (node.name.elements.length === 2 && state && setter && ts.isBindingElement(state) && ts.isBindingElement(setter) && ts.isIdentifier(state.name) && ts.isIdentifier(setter.name)) states.set(setter.name.text, state.name.text)
|
|
405
|
+
}
|
|
406
|
+
if (ts.isIdentifier(node.name) && node.initializer && (ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer))) callbacks.set(node.name.text, node.initializer)
|
|
407
|
+
}
|
|
408
|
+
ts.forEachChild(node, collectProviderBindings)
|
|
409
|
+
}
|
|
410
|
+
collectProviderBindings(owner.body)
|
|
411
|
+
|
|
412
|
+
const fields = new Set()
|
|
413
|
+
const stateFields = new Set([...states].flat())
|
|
414
|
+
for (const property of value.properties) {
|
|
415
|
+
if (!ts.isShorthandPropertyAssignment(property)) throw sourceNodeError(property, providerSource, "Context Provider values must use direct shorthand state, setter, or action fields")
|
|
416
|
+
const name = property.name.text
|
|
417
|
+
if (!stateFields.has(name) && !callbacks.has(name)) throw sourceNodeError(property, providerSource, `Context Provider field ${JSON.stringify(name)} must be a direct provider-owned state, setter, or action`)
|
|
418
|
+
fields.add(name)
|
|
419
|
+
}
|
|
420
|
+
for (const [setter, state] of states) {
|
|
421
|
+
if (fields.has(setter) !== fields.has(state)) throw sourceNodeError(value, providerSource, `Context Provider state ${JSON.stringify(state)} and setter ${JSON.stringify(setter)} must be exposed together`)
|
|
422
|
+
}
|
|
423
|
+
for (const [name, callback] of callbacks) {
|
|
424
|
+
if (!fields.has(name)) continue
|
|
425
|
+
if (callback.asteriskToken || callback.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) throw sourceNodeError(callback, providerSource, `Context action ${JSON.stringify(name)} must be synchronous`)
|
|
426
|
+
const capture = nativeCaptureNames(callback, states).values().next().value
|
|
427
|
+
if (capture) throw sourceNodeError(callback, providerSource, `Context action ${JSON.stringify(name)} cannot capture private binding ${JSON.stringify(capture)}`)
|
|
428
|
+
for (const state of referencedStateNames(callback.body, states, callback)) {
|
|
429
|
+
const setter = [...states].find(([, candidate]) => candidate === state)?.[0]
|
|
430
|
+
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)}`)
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return { callbacks: new Map([...callbacks].filter(([name]) => fields.has(name))), context: true, fields, privateStates: new Set(), stateOwner, states }
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const resolveCustomHook = (binding, call) => {
|
|
437
|
+
const exportName = binding.kind === "default" ? "default" : binding.imported
|
|
438
|
+
const key = `${binding.target}:${exportName}`
|
|
439
|
+
if (customHooks.has(key)) return customHooks.get(key)
|
|
440
|
+
const hook = resolveComponentExport(binding.target, exportName, importedSource, sourceFiles)
|
|
441
|
+
const hookSource = hook.getSourceFile()
|
|
442
|
+
if (hook.parameters.length || hook.asteriskToken || hook.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword) || !ts.isBlock(hook.body)) throw sourceNodeError(hook, hookSource, "Relative custom hooks must be synchronous zero-argument functions with a block body")
|
|
443
|
+
const returns = hook.body.statements.filter(ts.isReturnStatement)
|
|
444
|
+
const returned = returns.length === 1 && returns[0] === hook.body.statements.at(-1) && returns[0].expression ? unwrapExpression(returns[0].expression) : undefined
|
|
445
|
+
if (returned && ts.isCallExpression(returned) && ts.isIdentifier(returned.expression) && returned.expression.text === "useContext") {
|
|
446
|
+
const analysis = resolveContextHook(returned, hookSource)
|
|
447
|
+
customHooks.set(key, analysis)
|
|
448
|
+
return analysis
|
|
449
|
+
}
|
|
450
|
+
if (!returned || !ts.isObjectLiteralExpression(returned)) throw sourceNodeError(hook.body, hookSource, "Relative custom hooks must end with one direct object return or direct useContext(ContextIdentifier)")
|
|
451
|
+
|
|
452
|
+
const states = new Map()
|
|
453
|
+
const callbacks = new Map()
|
|
454
|
+
for (const statement of hook.body.statements) {
|
|
455
|
+
if (!ts.isVariableStatement(statement) || !(statement.declarationList.flags & ts.NodeFlags.Const)) continue
|
|
456
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
457
|
+
if (ts.isArrayBindingPattern(declaration.name) && declaration.initializer && ts.isCallExpression(declaration.initializer) && ts.isIdentifier(declaration.initializer.expression) && declaration.initializer.expression.text === "useState") {
|
|
458
|
+
const [state, setter] = declaration.name.elements
|
|
459
|
+
if (declaration.name.elements.length === 2 && state && setter && ts.isBindingElement(state) && ts.isBindingElement(setter) && ts.isIdentifier(state.name) && ts.isIdentifier(setter.name)) states.set(setter.name.text, state.name.text)
|
|
460
|
+
}
|
|
461
|
+
if (ts.isIdentifier(declaration.name) && declaration.initializer && (ts.isArrowFunction(declaration.initializer) || ts.isFunctionExpression(declaration.initializer))) callbacks.set(declaration.name.text, declaration.initializer)
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
const fields = new Set()
|
|
465
|
+
for (const property of returned.properties) {
|
|
466
|
+
if (!ts.isShorthandPropertyAssignment(property)) throw sourceNodeError(property, hookSource, "Relative custom hooks must return direct shorthand bindings")
|
|
467
|
+
fields.add(property.name.text)
|
|
468
|
+
}
|
|
469
|
+
for (const [name, callback] of callbacks) {
|
|
470
|
+
const capture = nativeCaptureNames(callback, states).values().next().value
|
|
471
|
+
if (capture) throw sourceNodeError(callback, hookSource, `Relative custom hook callback ${JSON.stringify(name)} cannot capture private binding ${JSON.stringify(capture)}`)
|
|
472
|
+
}
|
|
473
|
+
const privateStates = new Set([...states.values()].filter(state => importedSourceCache.get(hookSource.fileName)?.customHookTimerStates.has(state)))
|
|
474
|
+
const analysis = { callbacks, fields, privateStates, states }
|
|
475
|
+
customHooks.set(key, analysis)
|
|
476
|
+
return analysis
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const collect = node => {
|
|
480
|
+
if (ts.isVariableDeclaration(node) && node.initializer && ts.isCallExpression(node.initializer)) {
|
|
481
|
+
const callName = ts.isIdentifier(node.initializer.expression) ? node.initializer.expression.text : ""
|
|
482
|
+
if (callName && /^use[A-Z]/.test(callName) && importBindings.has(callName) && importBindings.get(callName).kind !== "namespace" && !resolvedZustandStore(importBindings.get(callName))) {
|
|
483
|
+
if (!isLocalConst(node) || !ts.isObjectBindingPattern(node.name) || node.initializer.arguments.length) throw sourceNodeError(node, sourceFile, "Relative custom hooks must initialize one top-level const object destructuring with no arguments")
|
|
484
|
+
const hook = resolveCustomHook(importBindings.get(callName), node.initializer)
|
|
485
|
+
const names = new Set()
|
|
486
|
+
for (const element of node.name.elements) {
|
|
487
|
+
if (element.dotDotDotToken || element.propertyName || element.initializer || !ts.isIdentifier(element.name)) throw sourceNodeError(element, sourceFile, "Relative custom hook results must use direct identifier shorthand without aliases, defaults, or rest")
|
|
488
|
+
const name = element.name.text
|
|
489
|
+
if (!hook.fields.has(name)) throw sourceNodeError(element, sourceFile, `Relative custom hook does not directly return ${JSON.stringify(name)}`)
|
|
490
|
+
names.add(name)
|
|
491
|
+
}
|
|
492
|
+
const owner = nearestFunction(node)
|
|
493
|
+
if (!owner) throw sourceNodeError(node, sourceFile, "Relative custom hooks cannot be used outside a Kudzu component")
|
|
494
|
+
const setters = settersByFunction.get(owner) ?? new Map()
|
|
495
|
+
const requiredContextStates = new Set()
|
|
496
|
+
if (hook.context) {
|
|
497
|
+
for (const name of names) {
|
|
498
|
+
const callback = hook.callbacks.get(name)
|
|
499
|
+
if (callback) for (const state of referencedStateNames(callback.body, hook.states, callback)) requiredContextStates.add(state)
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
for (const [setter, state] of hook.states) {
|
|
503
|
+
if (hook.context) {
|
|
504
|
+
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`)
|
|
505
|
+
if (!names.has(state) && !requiredContextStates.has(state)) continue
|
|
506
|
+
const localSetter = names.has(setter) || requiredContextStates.has(state) ? setter : `__kContextState_${state}`
|
|
507
|
+
setters.set(localSetter, state)
|
|
508
|
+
registerState(owner, state, localSetter, "context", node, hook.stateOwner)
|
|
509
|
+
if (requiredContextStates.has(state)) {
|
|
510
|
+
const fields = customHookPrivateFields.get(node) ?? []
|
|
511
|
+
for (const field of [state, setter]) {
|
|
512
|
+
if (names.has(field) || fields.includes(field)) continue
|
|
513
|
+
const conflict = owner.parameters.some(parameter => bindingNames(parameter.name).includes(field)) || owner.body.statements.some(statement => statement !== node.parent.parent && statementDeclaresName(statement, field))
|
|
514
|
+
if (conflict) throw sourceNodeError(node.name, sourceFile, `Context action state field ${JSON.stringify(field)} conflicts with a consumer binding`)
|
|
515
|
+
fields.push(field)
|
|
516
|
+
}
|
|
517
|
+
customHookPrivateFields.set(node, fields)
|
|
518
|
+
}
|
|
519
|
+
continue
|
|
520
|
+
}
|
|
521
|
+
if (hook.privateStates.has(state)) {
|
|
522
|
+
setters.set(setter, state)
|
|
523
|
+
registerState(owner, state, setter, "custom-hook", node)
|
|
524
|
+
const fields = customHookPrivateFields.get(node) ?? []
|
|
525
|
+
fields.push(state, setter)
|
|
526
|
+
customHookPrivateFields.set(node, fields)
|
|
527
|
+
continue
|
|
528
|
+
}
|
|
529
|
+
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`)
|
|
530
|
+
if (names.has(setter)) {
|
|
531
|
+
setters.set(setter, state)
|
|
532
|
+
registerState(owner, state, setter, "custom-hook", node)
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
settersByFunction.set(owner, setters)
|
|
536
|
+
for (const name of names) {
|
|
537
|
+
if (hook.callbacks.has(name)) {
|
|
538
|
+
const callbacks = customHookFunctionsByOwner.get(owner) ?? new Map()
|
|
539
|
+
callbacks.set(name, hook.callbacks.get(name))
|
|
540
|
+
customHookFunctionsByOwner.set(owner, callbacks)
|
|
541
|
+
if (hook.context) {
|
|
542
|
+
const reducers = reducersByFunction.get(owner) ?? new Map()
|
|
543
|
+
reducers.set(name, { contextAction: hook.callbacks.get(name), states: hook.states })
|
|
544
|
+
reducersByFunction.set(owner, reducers)
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
else if (![...hook.states].some(([setter, state]) => name === setter || name === state)) throw sourceNodeError(node.name, sourceFile, `Relative custom hook result ${JSON.stringify(name)} must be a direct useState value, setter, or callback`)
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
if (ts.isIdentifier(node.name) && callName && importBindings.has(callName) && importBindings.get(callName).kind !== "namespace") {
|
|
551
|
+
const storeImport = importBindings.get(callName)
|
|
552
|
+
const store = resolvedZustandStore(storeImport)
|
|
553
|
+
if (store) {
|
|
554
|
+
const selector = node.initializer.arguments[0]
|
|
555
|
+
if (node.initializer.arguments.length !== 1 || !selector || !ts.isArrowFunction(selector) || selector.parameters.length !== 1 || !ts.isIdentifier(selector.parameters[0].name) || !ts.isPropertyAccessExpression(unwrapExpression(selector.body)) || !ts.isIdentifier(unwrapExpression(selector.body).expression) || unwrapExpression(selector.body).expression.text !== selector.parameters[0].name.text) throw sourceNodeError(node.initializer, sourceFile, "Zustand selectors must be direct arrows such as state => state.quantities")
|
|
556
|
+
const selected = unwrapExpression(selector.body).name.text
|
|
557
|
+
const owner = nearestFunction(node)
|
|
558
|
+
if (!owner) throw sourceNodeError(node, sourceFile, "Zustand stores cannot be used outside a Kudzu component")
|
|
559
|
+
const setters = settersByFunction.get(owner) ?? new Map()
|
|
560
|
+
if (selected === store.field) {
|
|
561
|
+
const setter = `__kStoreState_${node.name.text}`
|
|
562
|
+
setters.set(setter, node.name.text)
|
|
563
|
+
registerState(owner, node.name.text, setter, "store", node)
|
|
564
|
+
}
|
|
565
|
+
else if (store.actions.has(selected)) {
|
|
566
|
+
setters.set(node.name.text, node.name.text)
|
|
567
|
+
registerState(owner, node.name.text, node.name.text, "store-action", node)
|
|
568
|
+
const reducers = reducersByFunction.get(owner) ?? new Map()
|
|
569
|
+
reducers.set(node.name.text, { state: node.name.text, store, action: selected })
|
|
570
|
+
reducersByFunction.set(owner, reducers)
|
|
571
|
+
} else throw sourceNodeError(unwrapExpression(selector.body).name, sourceFile, `Zustand store ${JSON.stringify(store.name)} has no supported property ${JSON.stringify(selected)}`)
|
|
572
|
+
settersByFunction.set(owner, setters)
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
if (callName === "useReducer") {
|
|
576
|
+
if (!ts.isArrayBindingPattern(node.name)) throw sourceNodeError(node.name, sourceFile, "useReducer() must use [state, dispatch] identifier destructuring")
|
|
577
|
+
const [stateElement, dispatchElement] = node.name.elements
|
|
578
|
+
if (node.name.elements.length !== 2 || !stateElement || !dispatchElement || !ts.isBindingElement(stateElement) || !ts.isBindingElement(dispatchElement) || !ts.isIdentifier(stateElement.name) || !ts.isIdentifier(dispatchElement.name)) throw sourceNodeError(node.name, sourceFile, "useReducer() must use [state, dispatch] identifier destructuring")
|
|
579
|
+
if (node.initializer.arguments.length !== 2) throw sourceNodeError(node.initializer, sourceFile, "useReducer() requires exactly a reducer and initial value")
|
|
580
|
+
const reducer = node.initializer.arguments[0]
|
|
581
|
+
if (!ts.isIdentifier(reducer) || !importBindings.has(reducer.text) || importBindings.get(reducer.text).kind === "namespace") throw sourceNodeError(reducer, sourceFile, "useReducer() reducers must be default or named imports from relative TypeScript modules")
|
|
582
|
+
const reducerImport = importBindings.get(reducer.text)
|
|
583
|
+
let reducerDeclaration
|
|
584
|
+
try {
|
|
585
|
+
reducerDeclaration = resolveComponentExport(reducerImport.target, reducerImport.kind === "default" ? "default" : reducerImport.imported, importedSource, sourceFiles)
|
|
586
|
+
} catch {
|
|
587
|
+
throw sourceNodeError(reducer, sourceFile, "useReducer() imports must resolve to a statically analyzable reducer function")
|
|
588
|
+
}
|
|
589
|
+
if (reducerDeclaration.parameters.length !== 2 || reducerDeclaration.asteriskToken || reducerDeclaration.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) throw sourceNodeError(reducer, sourceFile, "useReducer() reducers must be synchronous functions with exactly state and action parameters")
|
|
590
|
+
const owner = nearestFunction(node)
|
|
591
|
+
if (!owner) throw sourceNodeError(node, sourceFile, "useReducer() cannot be used outside a Kudzu component")
|
|
592
|
+
const setters = settersByFunction.get(owner) ?? new Map()
|
|
593
|
+
setters.set(dispatchElement.name.text, stateElement.name.text)
|
|
594
|
+
registerState(owner, stateElement.name.text, dispatchElement.name.text, "reducer", node)
|
|
595
|
+
settersByFunction.set(owner, setters)
|
|
596
|
+
const reducers = reducersByFunction.get(owner) ?? new Map()
|
|
597
|
+
reducers.set(dispatchElement.name.text, { state: stateElement.name.text, reducer: reducer.text, import: reducerImport })
|
|
598
|
+
reducersByFunction.set(owner, reducers)
|
|
599
|
+
}
|
|
600
|
+
if (ts.isArrayBindingPattern(node.name)) {
|
|
601
|
+
const [stateElement, setterElement] = node.name.elements
|
|
602
|
+
if (callName === "useState" && stateElement && setterElement && ts.isBindingElement(stateElement) && ts.isBindingElement(setterElement) && ts.isIdentifier(stateElement.name) && ts.isIdentifier(setterElement.name)) {
|
|
603
|
+
const owner = nearestFunction(node)
|
|
604
|
+
if (owner) {
|
|
605
|
+
const setters = settersByFunction.get(owner) ?? new Map()
|
|
606
|
+
setters.set(setterElement.name.text, stateElement.name.text)
|
|
607
|
+
registerState(owner, stateElement.name.text, setterElement.name.text, "state", node)
|
|
608
|
+
settersByFunction.set(owner, setters)
|
|
609
|
+
const localSetters = localStateSettersByFunction.get(owner) ?? new Set()
|
|
610
|
+
localSetters.add(setterElement.name.text)
|
|
611
|
+
localStateSettersByFunction.set(owner, localSetters)
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
if (ts.isFunctionDeclaration(node) && node.name) {
|
|
617
|
+
functions.set(node.name.text, node)
|
|
618
|
+
if (node.parent === sourceFile) {
|
|
619
|
+
components.set(node.name.text, { function: node, declaration: node })
|
|
620
|
+
ensureOwner(node)
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && (ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer))) {
|
|
624
|
+
functions.set(node.name.text, node.initializer)
|
|
625
|
+
if (node.parent?.parent?.parent === sourceFile) {
|
|
626
|
+
components.set(node.name.text, { function: node.initializer, declaration: node })
|
|
627
|
+
ensureOwner(node.initializer)
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isCallExpression(node.initializer) && ts.isIdentifier(node.initializer.expression)) {
|
|
631
|
+
const owner = nearestFunction(node)
|
|
632
|
+
if (owner && node.initializer.expression.text === "useRef" && node.initializer.arguments.length === 1 && node.initializer.arguments[0].kind === ts.SyntaxKind.NullKeyword) {
|
|
633
|
+
ensureOwner(owner)
|
|
634
|
+
componentAnalysis.registerRef(owner, { name: node.name.text, source: analysisSource(node) })
|
|
635
|
+
}
|
|
636
|
+
if (owner && node.initializer.expression.text === "useId" && node.initializer.arguments.length === 0) {
|
|
637
|
+
ensureOwner(owner)
|
|
638
|
+
componentAnalysis.registerId(owner, { name: node.name.text, source: analysisSource(node) })
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
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)
|
|
642
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && isLocalConst(node)) {
|
|
643
|
+
const owner = nearestFunction(node)
|
|
644
|
+
const declarations = jsxLocalDeclarations.get(owner) ?? new Map()
|
|
645
|
+
const entries = declarations.get(node.name.text) ?? []
|
|
646
|
+
entries.push({ node, initializer: node.initializer })
|
|
647
|
+
declarations.set(node.name.text, entries)
|
|
648
|
+
jsxLocalDeclarations.set(owner, declarations)
|
|
649
|
+
}
|
|
650
|
+
ts.forEachChild(node, collect)
|
|
651
|
+
}
|
|
652
|
+
collect(sourceFile)
|
|
653
|
+
const functionsForNode = node => {
|
|
654
|
+
const callbacks = customHookFunctionsByOwner.get(nearestFunction(node))
|
|
655
|
+
return callbacks ? new Map([...functions, ...callbacks]) : functions
|
|
656
|
+
}
|
|
657
|
+
for (const [owner, declarations] of jsxLocalDeclarations) {
|
|
658
|
+
const names = new Set()
|
|
659
|
+
let changed = true
|
|
660
|
+
while (changed) {
|
|
661
|
+
changed = false
|
|
662
|
+
for (const [name, entries] of declarations) {
|
|
663
|
+
if (!names.has(name) && entries.some(({ initializer }) => isJsxLocalValue(initializer, names))) {
|
|
664
|
+
names.add(name)
|
|
665
|
+
changed = true
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
for (const name of names) {
|
|
670
|
+
const entries = declarations.get(name)
|
|
671
|
+
if (entries.length > 1) {
|
|
672
|
+
const position = sourceFile.getLineAndCharacterOfPosition(entries[1].node.getStart(sourceFile))
|
|
673
|
+
throw new Error(`${sourceFile.fileName}:${position.line + 1}:${position.character + 1} Block-scoped JSX local "${name}" must not shadow another local`)
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
jsxLocalsByFunction.set(owner, names)
|
|
677
|
+
}
|
|
678
|
+
for (const [owner, declarations] of jsxLocalDeclarations) {
|
|
679
|
+
const setters = settersByFunction.get(owner) ?? new Map()
|
|
680
|
+
for (const [name, entries] of declarations) {
|
|
681
|
+
for (const declaration of entries) {
|
|
682
|
+
const parts = keyedListParts(declaration.initializer, setters, declarations, (target, message) => { throw sourceNodeError(target, sourceFile, message) }, new Set(), importedCollections, factory, context, importedCollectionTransforms)
|
|
683
|
+
if (!parts) continue
|
|
684
|
+
const uses = []
|
|
685
|
+
const collectUses = node => {
|
|
686
|
+
if (ts.isJsxExpression(node) && node.initializer === undefined && ts.isIdentifier(node.expression) && node.expression.text === name && nearestFunction(node) === owner) uses.push(node)
|
|
687
|
+
ts.forEachChild(node, collectUses)
|
|
688
|
+
}
|
|
689
|
+
collectUses(owner.body)
|
|
690
|
+
const references = identifierReferenceCount(owner.body, name)
|
|
691
|
+
const position = sourceFile.getLineAndCharacterOfPosition(declaration.node.getStart(sourceFile))
|
|
692
|
+
if (uses.length > 1) throw new Error(`${sourceFile.fileName}:${position.line + 1}:${position.character + 1} Keyed list local "${name}" must be rendered exactly once`)
|
|
693
|
+
if (references !== uses.length) throw new Error(`${sourceFile.fileName}:${position.line + 1}:${position.character + 1} Keyed list local "${name}" may only be used as a JSX child`)
|
|
694
|
+
listLocalDeclarations.push(declaration.node)
|
|
695
|
+
if (uses.length) listLocalUses.push({ node: uses[0], parts })
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
const fail = (node, message) => {
|
|
700
|
+
throw sourceNodeError(node, sourceFile, message)
|
|
701
|
+
}
|
|
702
|
+
const validateImportedCalculation = (call, field) => {
|
|
703
|
+
const name = call.expression.text
|
|
704
|
+
let calculation = importedCalculationFunctions.get(name)
|
|
705
|
+
if (!calculation) {
|
|
706
|
+
const binding = importBindings.get(name)
|
|
707
|
+
try {
|
|
708
|
+
calculation = resolveComponentExport(binding.target, binding.kind === "default" ? "default" : binding.imported, importedSource, sourceFiles)
|
|
709
|
+
} catch {
|
|
710
|
+
fail(call.expression, "Reactive imported calculations must resolve to a directly exported relative TypeScript function")
|
|
711
|
+
}
|
|
712
|
+
importedCalculationFunctions.set(name, calculation)
|
|
713
|
+
}
|
|
714
|
+
if (calculation.asteriskToken || calculation.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) fail(call.expression, "Reactive imported calculations must be synchronous functions")
|
|
715
|
+
if (calculation.parameters.length !== call.arguments.length) fail(call, "Reactive imported calculations require one direct argument for each declared parameter")
|
|
716
|
+
const returns = ts.isBlock(calculation.body) ? [] : [unwrapExpression(calculation.body)]
|
|
717
|
+
const collectReturns = node => {
|
|
718
|
+
if (node !== calculation.body && isFunctionLike(node)) return
|
|
719
|
+
if (ts.isReturnStatement(node)) returns.push(node.expression ? unwrapExpression(node.expression) : null)
|
|
720
|
+
ts.forEachChild(node, collectReturns)
|
|
721
|
+
}
|
|
722
|
+
if (ts.isBlock(calculation.body)) collectReturns(calculation.body)
|
|
723
|
+
if (ts.isBlock(calculation.body) && !ts.isReturnStatement(calculation.body.statements.at(-1))) fail(call.expression, "Reactive imported calculations must end with an unconditional return")
|
|
724
|
+
if (!returns.length || returns.some(returned => !returned || !ts.isObjectLiteralExpression(returned))) fail(call.expression, "Reactive imported calculations must return a plain object")
|
|
725
|
+
const fieldExists = returns.every(returned => returned.properties.some(property => ts.isSpreadAssignment(property) || !ts.isComputedPropertyName(property.name) && property.name.text === field))
|
|
726
|
+
if (!fieldExists) fail(call.parent, `Reactive imported calculation does not return field ${JSON.stringify(field)}`)
|
|
727
|
+
}
|
|
728
|
+
const validateReactiveJsxExpression = (expression, allowedNames) => {
|
|
729
|
+
const value = unwrapExpression(expression)
|
|
730
|
+
const formatAccess = ts.isCallExpression(value) && !value.questionDotToken && ts.isPropertyAccessExpression(value.expression) && !value.expression.questionDotToken && value.expression.name.text === "format" ? value.expression : undefined
|
|
731
|
+
const formatter = formatAccess && unwrapExpression(formatAccess.expression)
|
|
732
|
+
const constructor = formatter && ts.isNewExpression(formatter) && ts.isPropertyAccessExpression(formatter.expression) && formatter.expression.name.text === "NumberFormat" && ts.isIdentifier(formatter.expression.expression) && formatter.expression.expression.text === "Intl" ? formatter : undefined
|
|
733
|
+
if (!constructor) {
|
|
734
|
+
const validate = node => {
|
|
735
|
+
const current = unwrapExpression(node)
|
|
736
|
+
if (ts.isPropertyAccessExpression(current) && ts.isCallExpression(unwrapExpression(current.expression))) {
|
|
737
|
+
const call = unwrapExpression(current.expression)
|
|
738
|
+
if (ts.isIdentifier(call.expression) && importBindings.has(call.expression.text) && importBindings.get(call.expression.text).kind !== "namespace") {
|
|
739
|
+
validateImportedCalculation(call, current.name.text)
|
|
740
|
+
for (const argument of call.arguments) collectionExpression(argument, { fail: (target, message) => fail(target, message.replace("Rendered collection", "Reactive imported calculation")), stateNames: allowedNames })
|
|
741
|
+
return factory.createNumericLiteral(0)
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
return ts.visitEachChild(current, validate, context)
|
|
745
|
+
}
|
|
746
|
+
const normalized = ts.visitNode(value, validate)
|
|
747
|
+
collectionExpression(normalized, { fail: (node, message) => fail(node, message.replace("Rendered collection", "Reactive JSX local")), stateNames: allowedNames })
|
|
748
|
+
return
|
|
749
|
+
}
|
|
750
|
+
const intl = constructor.expression.expression
|
|
751
|
+
if (!isUnshadowedGlobal(intl, sourceFile)) fail(intl, "Reactive JSX Intl.NumberFormat requires the unshadowed global Intl object")
|
|
752
|
+
if (constructor.arguments?.length !== 1 || !ts.isStringLiteral(constructor.arguments[0])) fail(constructor, "Reactive JSX Intl.NumberFormat requires exactly one static string locale")
|
|
753
|
+
const rounded = value.arguments.length === 1 ? unwrapExpression(value.arguments[0]) : undefined
|
|
754
|
+
const roundAccess = rounded && ts.isCallExpression(rounded) && !rounded.questionDotToken && rounded.arguments.length === 1 && ts.isPropertyAccessExpression(rounded.expression) && !rounded.expression.questionDotToken && rounded.expression.name.text === "round" && ts.isIdentifier(rounded.expression.expression) && rounded.expression.expression.text === "Math" ? rounded.expression : undefined
|
|
755
|
+
if (!roundAccess) fail(value, "Reactive JSX Intl.NumberFormat format() requires exactly Math.round(expression)")
|
|
756
|
+
if (!isUnshadowedGlobal(roundAccess.expression, sourceFile)) fail(roundAccess.expression, "Reactive JSX Intl.NumberFormat requires the unshadowed global Math object")
|
|
757
|
+
collectionExpression(rounded.arguments[0], { fail: (node, message) => fail(node, message.replace("Rendered collection", "Reactive JSX local")), stateNames: allowedNames })
|
|
758
|
+
}
|
|
759
|
+
const resolveReactiveJsxExpression = (expression, owner, setters) => {
|
|
760
|
+
const declarations = jsxLocalDeclarations.get(owner)
|
|
761
|
+
if (!declarations) return expression
|
|
762
|
+
const substitutions = new Map()
|
|
763
|
+
const resolving = []
|
|
764
|
+
const resolve = (name, reference) => {
|
|
765
|
+
if (substitutions.has(name)) return
|
|
766
|
+
const entries = declarations.get(name)
|
|
767
|
+
if (!entries?.length) return
|
|
768
|
+
if (jsxLocalsByFunction.get(owner)?.has(name)) return
|
|
769
|
+
if (entries.length !== 1 || entries[0].node.parent?.parent?.parent !== owner?.body) return
|
|
770
|
+
const cycle = resolving.indexOf(name)
|
|
771
|
+
if (cycle >= 0) fail(reference, `Reactive JSX local cycle: ${[...resolving.slice(cycle), name].join(" -> ")}`)
|
|
772
|
+
resolving.push(name)
|
|
773
|
+
const initializer = entries[0].initializer
|
|
774
|
+
const visit = node => {
|
|
775
|
+
if (ts.isIdentifier(node) && isReferenceIdentifier(node) && !isShadowedByParameter(node, initializer) && declarations.has(node.text)) resolve(node.text, node)
|
|
776
|
+
ts.forEachChild(node, visit)
|
|
777
|
+
}
|
|
778
|
+
visit(initializer)
|
|
779
|
+
substitutions.set(name, substituteClone(initializer, substitutions, factory, context))
|
|
780
|
+
resolving.pop()
|
|
781
|
+
}
|
|
782
|
+
const visit = node => {
|
|
783
|
+
if (ts.isIdentifier(node) && isReferenceIdentifier(node) && !isShadowedByParameter(node, expression) && declarations.has(node.text)) resolve(node.text, node)
|
|
784
|
+
ts.forEachChild(node, visit)
|
|
785
|
+
}
|
|
786
|
+
visit(expression)
|
|
787
|
+
if (!substitutions.size) return expression
|
|
788
|
+
const expanded = substituteClone(expression, substitutions, factory, context)
|
|
789
|
+
ts.setParentRecursive(expanded, false)
|
|
790
|
+
expanded.parent = expression.parent
|
|
791
|
+
const usedStates = referencedStateNames(expanded, setters)
|
|
792
|
+
if (!usedStates.size) return expression
|
|
793
|
+
const captures = captureNames(expanded, expanded, setters)
|
|
794
|
+
const allowedNames = new Set([...setters.values(), ...captures])
|
|
795
|
+
validateReactiveJsxExpression(expanded, allowedNames)
|
|
796
|
+
return expanded
|
|
797
|
+
}
|
|
798
|
+
const componentSpecializations = new WeakMap()
|
|
799
|
+
const setterHookHelpers = new WeakMap()
|
|
800
|
+
const expandedRowSpecializations = new WeakMap()
|
|
801
|
+
const nestedRowSpecializations = new Map()
|
|
802
|
+
const reducerComponentCalls = new WeakSet()
|
|
803
|
+
const rowHookCalls = []
|
|
804
|
+
const specializedDeclarations = new WeakSet()
|
|
805
|
+
const stateBackedComponentFunctions = new WeakSet()
|
|
806
|
+
const stateBackedComponentRoots = []
|
|
807
|
+
let specializedImportIndex = 0
|
|
808
|
+
const specialize = (call, component, label = "Keyed list", allowComponentRoot = false, ordinaryHooks = false, ordinaryStateNames = new Set(), ownership) => {
|
|
809
|
+
const result = specializeComponentCall(call, component, sourceFile, factory, context, fail, label, allowComponentRoot, ordinaryHooks, ordinaryStateNames)
|
|
810
|
+
const owner = nearestFunction(call)
|
|
811
|
+
const setters = ownership?.setters ?? settersForNode(call, settersByFunction)
|
|
812
|
+
const stateOwners = ownership?.stateOwners ?? stateOwnersForNode(call)
|
|
813
|
+
const callbacks = functionsForNode(call)
|
|
814
|
+
const propSignals = expression => {
|
|
815
|
+
const signals = new Set()
|
|
816
|
+
if (ts.isIdentifier(expression) && setters.has(expression.text)) signals.add(setters.get(expression.text))
|
|
817
|
+
const callback = ts.isIdentifier(expression) ? callbacks.get(expression.text) : undefined
|
|
818
|
+
for (const state of referencedStateNames((callback ?? expression).body ?? callback ?? expression, setters, callback ?? expression)) signals.add(state)
|
|
819
|
+
return [...signals].map(name => ({ name, owner: stateOwners.get(name) ?? (owner ? `owner:${ensureOwner(owner).slot}` : "module") }))
|
|
820
|
+
}
|
|
821
|
+
result.analysis = componentAnalysis.registerSpecialization({
|
|
822
|
+
kind: label,
|
|
823
|
+
...(owner ? { owner: ensureOwner(owner).slot } : {}),
|
|
824
|
+
...(analysisSource(call) ? { source: analysisSource(call) } : {}),
|
|
825
|
+
props: result.props.map(prop => {
|
|
826
|
+
const expression = result.propExpressions.get(prop.name)
|
|
827
|
+
const signals = expression ? propSignals(expression) : []
|
|
828
|
+
return { ...prop, ...(signals.length ? { signals } : {}) }
|
|
829
|
+
}),
|
|
830
|
+
states: [
|
|
831
|
+
...result.rowStates.map(({ state, setter, source }) => ({ name: state, setter, kind: "row", ...(analysisSource(source) ? { source: analysisSource(source) } : {}) })),
|
|
832
|
+
...result.ordinaryStates.map(({ state, setter, source }) => ({ name: state, setter, kind: "component", ...(analysisSource(source) ? { source: analysisSource(source) } : {}) }))
|
|
833
|
+
],
|
|
834
|
+
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) } : {}) }))],
|
|
835
|
+
ids: result.ordinaryIds.map(({ name, source }) => ({ name, ...(analysisSource(source) ? { source: analysisSource(source) } : {}) }))
|
|
836
|
+
})
|
|
837
|
+
for (const state of [...result.rowStates, ...result.ordinaryStates]) state.analysisOwner = `specialization:${result.analysis.slot}`
|
|
838
|
+
for (const ref of [...result.rowRefs, ...result.ordinaryRefs]) ref.analysisOwner = `specialization:${result.analysis.slot}`
|
|
839
|
+
return result
|
|
840
|
+
}
|
|
841
|
+
const registerRowHooks = (call, specialization) => {
|
|
842
|
+
if (!specialization.rowStates.length && !specialization.rowRefs.length) return
|
|
843
|
+
let owner
|
|
844
|
+
for (let current = call.parent; current; current = current.parent) {
|
|
845
|
+
if (isFunctionLike(current) && settersByFunction.has(current)) {
|
|
846
|
+
owner = current
|
|
847
|
+
break
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
if (!owner) owner = nearestFunction(call)
|
|
851
|
+
const setters = new Map(settersByFunction.get(owner))
|
|
852
|
+
const stateOwners = new Map(stateOwnersByFunction.get(owner))
|
|
853
|
+
for (const state of specialization.rowStates) {
|
|
854
|
+
setters.set(state.setter, state.state)
|
|
855
|
+
stateOwners.set(state.state, state.analysisOwner)
|
|
856
|
+
}
|
|
857
|
+
settersByFunction.set(owner, setters)
|
|
858
|
+
stateOwnersByFunction.set(owner, stateOwners)
|
|
859
|
+
rowHookCalls.push(call)
|
|
860
|
+
usesRowState ||= specialization.rowStates.length > 0
|
|
861
|
+
usesRowRef ||= specialization.rowRefs.length > 0
|
|
862
|
+
}
|
|
863
|
+
const mergeSpecializedImports = (root, componentSource, call, effects = []) => {
|
|
864
|
+
const componentImports = clientImportBindings(componentSource, componentSource.fileName, sourceFiles)
|
|
865
|
+
for (const name of runtimeImportNames(componentSource, false)) if (referenceIdentifiers(root, name).length) fail(call, "Imported specialized component handlers may only use relative TypeScript runtime imports")
|
|
866
|
+
const substitutions = new Map()
|
|
867
|
+
for (const statement of componentSource.statements) {
|
|
868
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier) || !isStaticImport(statement.moduleSpecifier.text)) continue
|
|
869
|
+
const entry = staticImportEntry(statement, componentSource, componentSource.fileName, staticFiles, importedAssets, cssModules, base, factory)
|
|
870
|
+
if (!entry?.name) continue
|
|
871
|
+
if (referenceIdentifiers(root, entry.name).length) substitutions.set(entry.name, entry.value)
|
|
872
|
+
for (const effect of effects) {
|
|
873
|
+
if (effect.source.getSourceFile() !== componentSource) continue
|
|
874
|
+
if (!referenceIdentifiers(effect.call, entry.name).length) continue
|
|
875
|
+
ts.setParentRecursive(effect.call, false)
|
|
876
|
+
effect.call = substituteClone(effect.call, new Map([[entry.name, entry.value]]), factory, context)
|
|
877
|
+
synthesizeTree(effect.call)
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
for (const [name, entry] of componentImports) {
|
|
881
|
+
const references = referenceIdentifiers(root, name)
|
|
882
|
+
if (!references.length) continue
|
|
883
|
+
if (references.some(reference => !insideJsxEventHandler(reference, root))) fail(call, `Imported specialized component runtime import "${name}" may only be used inside event handlers`)
|
|
884
|
+
let local
|
|
885
|
+
do local = `__kDispatchImport${specializedImportIndex++}`
|
|
886
|
+
while (importBindings.has(local))
|
|
887
|
+
substitutions.set(name, factory.createIdentifier(local))
|
|
888
|
+
importBindings.set(local, { ...entry, local })
|
|
889
|
+
}
|
|
890
|
+
if (!substitutions.size) return root
|
|
891
|
+
const merged = substituteClone(root, substitutions, factory, context)
|
|
892
|
+
ts.setParentRecursive(merged, false)
|
|
893
|
+
merged.parent = root.parent
|
|
894
|
+
return merged
|
|
895
|
+
}
|
|
896
|
+
const expandReducerCallbacks = (root, componentSource, call) => {
|
|
897
|
+
const componentImports = clientImportBindings(componentSource, componentSource.fileName, sourceFiles)
|
|
898
|
+
const replacements = new WeakMap()
|
|
899
|
+
let count = 0
|
|
900
|
+
for (const [name, entry] of componentImports) {
|
|
901
|
+
if (entry.kind === "namespace") continue
|
|
902
|
+
const nestedCalls = jsxTagUses(root, name).filter(nestedCall => jsxCallHasReducerCallbackProp(nestedCall, reducersForNode(nestedCall, reducersByFunction)))
|
|
903
|
+
if (!nestedCalls.length) continue
|
|
904
|
+
const imported = entry.kind === "default" ? "default" : entry.imported
|
|
905
|
+
let nestedComponent
|
|
906
|
+
try {
|
|
907
|
+
nestedComponent = resolveComponentExport(entry.target, imported, importedSource, sourceFiles)
|
|
908
|
+
} catch {
|
|
909
|
+
fail(nestedCalls[0], "Reducer callback props require a component imported from a relative TypeScript module")
|
|
910
|
+
}
|
|
911
|
+
for (const nestedCall of nestedCalls) {
|
|
912
|
+
const nested = specialize(nestedCall, nestedComponent, "Reducer-callback")
|
|
913
|
+
if (nested.effects.length) fail(nestedCall, "Reducer-callback components cannot declare effects")
|
|
914
|
+
nested.root = mergeSpecializedImports(nested.root, nestedComponent.getSourceFile(), nestedCall, nested.effects)
|
|
915
|
+
synthesizeTree(nested.root)
|
|
916
|
+
replacements.set(nestedCall, nested.root)
|
|
917
|
+
count++
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
if (!count) return root
|
|
921
|
+
const expanded = replaceSpecializedCalls(root, replacements, context)
|
|
922
|
+
ts.setParentRecursive(expanded, false)
|
|
923
|
+
expanded.parent = root.parent
|
|
924
|
+
return expanded
|
|
925
|
+
}
|
|
926
|
+
const staticConditionValue = expression => {
|
|
927
|
+
const value = unwrapExpression(expression)
|
|
928
|
+
if (value.kind === ts.SyntaxKind.TrueKeyword) return true
|
|
929
|
+
if (value.kind === ts.SyntaxKind.FalseKeyword || value.kind === ts.SyntaxKind.NullKeyword || ts.isIdentifier(value) && value.text === "undefined") return false
|
|
930
|
+
if (ts.isStringLiteral(value) || ts.isNoSubstitutionTemplateLiteral(value)) return Boolean(value.text)
|
|
931
|
+
if (ts.isNumericLiteral(value)) return Number(value.text) !== 0
|
|
932
|
+
return undefined
|
|
933
|
+
}
|
|
934
|
+
const foldSetterStaticConditions = root => {
|
|
935
|
+
const visit = node => {
|
|
936
|
+
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
|
|
937
|
+
const condition = staticConditionValue(node.left)
|
|
938
|
+
if (condition !== undefined) return condition ? ts.visitNode(node.right, visit) : node.left
|
|
939
|
+
}
|
|
940
|
+
if (ts.isConditionalExpression(node)) {
|
|
941
|
+
const condition = staticConditionValue(node.condition)
|
|
942
|
+
if (condition !== undefined) return ts.visitNode(condition ? node.whenTrue : node.whenFalse, visit)
|
|
943
|
+
}
|
|
944
|
+
return ts.visitEachChild(node, visit, context)
|
|
945
|
+
}
|
|
946
|
+
const folded = ts.visitNode(root, visit)
|
|
947
|
+
ts.setParentRecursive(folded, false)
|
|
948
|
+
folded.parent = root.parent
|
|
949
|
+
return folded
|
|
950
|
+
}
|
|
951
|
+
const expandSetterComponents = (root, componentSource, trail, aggregate, parentSetters, parentStateOwners) => {
|
|
952
|
+
root = foldSetterStaticConditions(root)
|
|
953
|
+
const replacements = new WeakMap()
|
|
954
|
+
let count = 0
|
|
955
|
+
const visit = (node, dynamic = false) => {
|
|
956
|
+
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
|
|
957
|
+
visit(node.left, dynamic)
|
|
958
|
+
visit(node.right, true)
|
|
959
|
+
return
|
|
960
|
+
}
|
|
961
|
+
if (ts.isConditionalExpression(node)) {
|
|
962
|
+
visit(node.condition, dynamic)
|
|
963
|
+
visit(node.whenTrue, true)
|
|
964
|
+
visit(node.whenFalse, true)
|
|
965
|
+
return
|
|
966
|
+
}
|
|
967
|
+
const tag = jsxTagName(node)
|
|
968
|
+
if (tag && (!ts.isIdentifier(tag) || tag.text[0] !== tag.text[0].toLowerCase())) {
|
|
969
|
+
if (!ts.isIdentifier(tag)) fail(node, "Nested setter-callback components must use identifier JSX tags")
|
|
970
|
+
const name = tag.text
|
|
971
|
+
let component = localComponentDeclaration(componentSource, name)
|
|
972
|
+
let imported = false
|
|
973
|
+
if (!component) {
|
|
974
|
+
const binding = clientImportBindings(componentSource, componentSource.fileName, sourceFiles).get(name)
|
|
975
|
+
if (!binding || binding.kind === "namespace") fail(node, `Nested setter-callback component ${name} must be declared locally or imported from a relative TypeScript module`)
|
|
976
|
+
component = resolveComponentExport(binding.target, binding.kind === "default" ? "default" : binding.imported, importedSource, sourceFiles)
|
|
977
|
+
imported = true
|
|
978
|
+
}
|
|
979
|
+
if (trail.includes(component)) {
|
|
980
|
+
const chain = [...trail, component].map(entry => entry.name?.text || "anonymous").join(" -> ")
|
|
981
|
+
fail(node, `Nested setter-callback component cycle: ${chain}`)
|
|
982
|
+
}
|
|
983
|
+
const setters = new Map(parentSetters)
|
|
984
|
+
for (const state of aggregate.ordinaryStates) setters.set(state.setter, state.state)
|
|
985
|
+
const stateOwners = new Map(parentStateOwners)
|
|
986
|
+
for (const state of aggregate.ordinaryStates) stateOwners.set(state.state, state.analysisOwner)
|
|
987
|
+
if (jsxSetterCallbackProps(node, setters, functionsForNode(node), reducersForNode(node, reducersByFunction)).length) fail(node, "Setter callbacks cannot cross a second component boundary")
|
|
988
|
+
const nested = specialize(node, component, "Nested setter-callback", true, true, new Set(setters.values()), { setters, stateOwners })
|
|
989
|
+
if (dynamic && (nested.hookDeclarations.length || nested.effects.length)) fail(node, "Hookful nested setter-callback components require an unconditional or statically truthy render path")
|
|
990
|
+
nested.root = expandSetterComponents(nested.root, component.getSourceFile(), [...trail, component], nested, setters, stateOwners)
|
|
991
|
+
if (imported) synthesizeTree(nested.root = mergeSpecializedImports(nested.root, component.getSourceFile(), node, nested.effects))
|
|
992
|
+
aggregate.calculations.push(...nested.calculations)
|
|
993
|
+
aggregate.effects.push(...nested.effects)
|
|
994
|
+
aggregate.hookDeclarations.push(...nested.hookDeclarations)
|
|
995
|
+
aggregate.ordinaryStates.push(...nested.ordinaryStates)
|
|
996
|
+
aggregate.ordinaryRefs.push(...nested.ordinaryRefs)
|
|
997
|
+
aggregate.usesComponentId ||= nested.usesComponentId
|
|
998
|
+
replacements.set(node, nested.root)
|
|
999
|
+
count++
|
|
1000
|
+
return
|
|
1001
|
+
}
|
|
1002
|
+
ts.forEachChild(node, child => visit(child, dynamic))
|
|
1003
|
+
}
|
|
1004
|
+
visit(root)
|
|
1005
|
+
if (!count) return root
|
|
1006
|
+
const expanded = replaceSpecializedCalls(root, replacements, context)
|
|
1007
|
+
ts.setParentRecursive(expanded, false)
|
|
1008
|
+
expanded.parent = root.parent
|
|
1009
|
+
return expanded
|
|
1010
|
+
}
|
|
1011
|
+
for (const [name, component] of components) {
|
|
1012
|
+
const calls = jsxTagUses(sourceFile, name)
|
|
1013
|
+
const stateBackedCalls = calls.filter(call => isStateBackedListComponentCall(call, component.function, settersByFunction.get(nearestFunction(call)) ?? new Map()))
|
|
1014
|
+
if (!stateBackedCalls.length) continue
|
|
1015
|
+
if (isExportedDeclaration(component.declaration)) fail(component.declaration, `State-backed list component ${name} cannot be exported`)
|
|
1016
|
+
if (identifierReferenceCount(sourceFile, name) !== calls.length) fail(component.declaration, `State-backed list component ${name} may only be referenced as JSX`)
|
|
1017
|
+
if (stateBackedCalls.length !== calls.length) fail(component.declaration, `State-backed list component ${name} must receive its mapped prop from local state at every call`)
|
|
1018
|
+
for (const call of stateBackedCalls) {
|
|
1019
|
+
const specialization = specialize(call, component.function)
|
|
1020
|
+
if (specialization.effects.length) fail(call, "State-backed list components cannot declare effects")
|
|
1021
|
+
componentSpecializations.set(call, specialization)
|
|
1022
|
+
stateBackedComponentRoots.push(specialization.root)
|
|
1023
|
+
}
|
|
1024
|
+
specializedDeclarations.add(component.declaration)
|
|
1025
|
+
stateBackedComponentFunctions.add(component.function)
|
|
1026
|
+
}
|
|
1027
|
+
for (const [name, binding] of importBindings) {
|
|
1028
|
+
if (binding.kind === "namespace") continue
|
|
1029
|
+
const calls = jsxTagUses(sourceFile, name)
|
|
1030
|
+
if (!calls.some(call => jsxCallHasDirectStateProp(call, settersByFunction.get(nearestFunction(call)) ?? new Map()))) continue
|
|
1031
|
+
const imported = binding.kind === "default" ? "default" : binding.imported
|
|
1032
|
+
let component
|
|
1033
|
+
try {
|
|
1034
|
+
component = resolveComponentExport(binding.target, imported, importedSource, sourceFiles)
|
|
1035
|
+
} catch (error) {
|
|
1036
|
+
if (error.message.includes("does not export a statically analyzable keyed list component")) continue
|
|
1037
|
+
throw error
|
|
1038
|
+
}
|
|
1039
|
+
const stateBackedCalls = calls.filter(call => isStateBackedListComponentCall(call, component, settersByFunction.get(nearestFunction(call)) ?? new Map()))
|
|
1040
|
+
for (const call of stateBackedCalls) {
|
|
1041
|
+
const specialization = specialize(call, component)
|
|
1042
|
+
if (specialization.effects.length) fail(call, "State-backed list components cannot declare effects")
|
|
1043
|
+
componentSpecializations.set(call, specialization)
|
|
1044
|
+
stateBackedComponentRoots.push(specialization.root)
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
const specializeSetterCallbacks = (call, component, callbackProps, imported) => {
|
|
1048
|
+
if (componentSpecializations.has(call)) fail(call, "Setter callback props cannot be combined with another component specialization")
|
|
1049
|
+
if (component.parameters.length !== 1 || !ts.isObjectBindingPattern(component.parameters[0].name)) fail(component, "Setter-callback components must use one destructured props parameter")
|
|
1050
|
+
for (const prop of callbackProps) {
|
|
1051
|
+
const element = component.parameters[0].name.elements.find(entry => !entry.dotDotDotToken && (entry.propertyName ?? entry.name).getText() === prop)
|
|
1052
|
+
if (!element || !ts.isIdentifier(element.name)) fail(call, `Setter-callback component must destructure callback prop ${JSON.stringify(prop)}`)
|
|
1053
|
+
const references = []
|
|
1054
|
+
const collectReferences = node => {
|
|
1055
|
+
if (ts.isIdentifier(node) && node.text === element.name.text && isReferenceIdentifier(node)) references.push(node)
|
|
1056
|
+
ts.forEachChild(node, collectReferences)
|
|
1057
|
+
}
|
|
1058
|
+
collectReferences(component.body)
|
|
1059
|
+
if (references.length !== 1) fail(element, `Setter-callback prop ${JSON.stringify(prop)} must be used exactly once in the component`)
|
|
1060
|
+
}
|
|
1061
|
+
const specialization = specialize(call, component, "Setter-callback", false, true, new Set(settersForNode(call, settersByFunction).values()))
|
|
1062
|
+
if (specialization.hookDeclarations.length || specialization.effects.length) {
|
|
1063
|
+
const substitutions = new Map()
|
|
1064
|
+
const attributes = ts.isJsxElement(call) ? call.openingElement.attributes : call.attributes
|
|
1065
|
+
for (const attribute of attributes.properties) {
|
|
1066
|
+
if (!ts.isJsxAttribute(attribute) || !callbackProps.includes(attribute.name.text) || !attribute.initializer || !ts.isJsxExpression(attribute.initializer) || !ts.isIdentifier(attribute.initializer.expression)) continue
|
|
1067
|
+
const callback = functionsForNode(attribute).get(attribute.initializer.expression.text)
|
|
1068
|
+
if (callback) substitutions.set(attribute.initializer.expression.text, callback)
|
|
1069
|
+
}
|
|
1070
|
+
if (substitutions.size) {
|
|
1071
|
+
specialization.root = substituteClone(specialization.root, substitutions, factory, context)
|
|
1072
|
+
for (const effect of specialization.effects) effect.call = substituteClone(effect.call, substitutions, factory, context)
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
specialization.root = expandSetterComponents(specialization.root, component.getSourceFile(), [component], specialization, settersForNode(call, settersByFunction), stateOwnersForNode(call))
|
|
1076
|
+
if (imported) synthesizeTree(specialization.root = mergeSpecializedImports(specialization.root, component.getSourceFile(), call, specialization.effects))
|
|
1077
|
+
if (specialization.hookDeclarations.length || specialization.effects.length) {
|
|
1078
|
+
const owner = nearestFunction(call)
|
|
1079
|
+
const name = `KSetterComponent${Math.max(0, call.pos)}`
|
|
1080
|
+
const effectStatements = specialization.effects.map(entry => {
|
|
1081
|
+
const effectCall = factory.updateCallExpression(entry.call, factory.createIdentifier("__kComponentUseEffect"), entry.call.typeArguments, entry.call.arguments)
|
|
1082
|
+
synthesizeTree(effectCall)
|
|
1083
|
+
ts.setOriginalNode(effectCall, entry.source)
|
|
1084
|
+
return factory.createExpressionStatement(effectCall)
|
|
1085
|
+
})
|
|
1086
|
+
const helper = factory.createFunctionDeclaration(
|
|
1087
|
+
undefined,
|
|
1088
|
+
undefined,
|
|
1089
|
+
name,
|
|
1090
|
+
undefined,
|
|
1091
|
+
[],
|
|
1092
|
+
undefined,
|
|
1093
|
+
factory.createBlock([...specialization.hookDeclarations, ...effectStatements, factory.createReturnStatement(specialization.root)], true)
|
|
1094
|
+
)
|
|
1095
|
+
ts.setParentRecursive(helper, false)
|
|
1096
|
+
helper.parent = owner.body
|
|
1097
|
+
const helpers = setterHookHelpers.get(owner.body) ?? []
|
|
1098
|
+
helpers.push(helper)
|
|
1099
|
+
setterHookHelpers.set(owner.body, helpers)
|
|
1100
|
+
const setters = new Map(settersForNode(call, settersByFunction))
|
|
1101
|
+
for (const state of specialization.ordinaryStates) setters.set(state.setter, state.state)
|
|
1102
|
+
settersByFunction.set(helper, setters)
|
|
1103
|
+
const stateOwners = new Map(stateOwnersForNode(call))
|
|
1104
|
+
for (const state of specialization.ordinaryStates) stateOwners.set(state.state, state.analysisOwner)
|
|
1105
|
+
stateOwnersByFunction.set(helper, stateOwners)
|
|
1106
|
+
usesComponentState ||= specialization.ordinaryStates.length > 0
|
|
1107
|
+
usesComponentId ||= specialization.usesComponentId
|
|
1108
|
+
usesComponentRef ||= specialization.ordinaryRefs.length > 0
|
|
1109
|
+
usesComponentEffects ||= specialization.effects.length > 0
|
|
1110
|
+
specialization.root = factory.createJsxSelfClosingElement(factory.createIdentifier(name), undefined, factory.createJsxAttributes([]))
|
|
1111
|
+
ts.setParentRecursive(specialization.root, false)
|
|
1112
|
+
specialization.root.parent = call.parent
|
|
1113
|
+
}
|
|
1114
|
+
componentSpecializations.set(call, specialization)
|
|
1115
|
+
}
|
|
1116
|
+
for (const [name, component] of components) {
|
|
1117
|
+
for (const call of jsxTagUses(sourceFile, name)) {
|
|
1118
|
+
const callbackProps = jsxSetterCallbackProps(call, settersByFunction.get(nearestFunction(call)) ?? new Map(), functionsForNode(call), reducersForNode(call, reducersByFunction))
|
|
1119
|
+
if (callbackProps.length) specializeSetterCallbacks(call, component.function, callbackProps, false)
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
for (const [name, binding] of importBindings) {
|
|
1123
|
+
if (binding.kind === "namespace") continue
|
|
1124
|
+
const calls = jsxTagUses(sourceFile, name)
|
|
1125
|
+
const callbackCalls = calls.map(call => ({ call, callbackProps: jsxSetterCallbackProps(call, settersByFunction.get(nearestFunction(call)) ?? new Map(), functionsForNode(call), reducersForNode(call, reducersByFunction)) })).filter(entry => entry.callbackProps.length)
|
|
1126
|
+
if (!callbackCalls.length) continue
|
|
1127
|
+
const imported = binding.kind === "default" ? "default" : binding.imported
|
|
1128
|
+
let component
|
|
1129
|
+
try {
|
|
1130
|
+
component = resolveComponentExport(binding.target, imported, importedSource, sourceFiles)
|
|
1131
|
+
} catch {
|
|
1132
|
+
fail(callbackCalls[0].call, "Setter callback props require a component imported from a relative TypeScript module")
|
|
1133
|
+
}
|
|
1134
|
+
for (const { call, callbackProps } of callbackCalls) specializeSetterCallbacks(call, component, callbackProps, true)
|
|
1135
|
+
}
|
|
1136
|
+
for (const [name, component] of components) {
|
|
1137
|
+
const calls = jsxTagUses(sourceFile, name)
|
|
1138
|
+
const dispatchCalls = calls.filter(call => jsxCallHasDirectReducerProp(call, reducersForNode(call, reducersByFunction)))
|
|
1139
|
+
if (!dispatchCalls.length) continue
|
|
1140
|
+
if (isExportedDeclaration(component.declaration)) fail(component.declaration, `Reducer-dispatch component ${name} cannot be exported`)
|
|
1141
|
+
if (identifierReferenceCount(sourceFile, name) !== calls.length) fail(component.declaration, `Reducer-dispatch component ${name} may only be referenced as JSX`)
|
|
1142
|
+
if (dispatchCalls.length !== calls.length) fail(component.declaration, `Reducer-dispatch component ${name} must receive a direct local reducer dispatch at every call`)
|
|
1143
|
+
for (const call of dispatchCalls) {
|
|
1144
|
+
if (componentSpecializations.has(call)) fail(call, "Reducer dispatch props cannot be combined with another component specialization")
|
|
1145
|
+
const specialization = specialize(call, component.function, "Reducer-dispatch")
|
|
1146
|
+
registerRowHooks(call, specialization)
|
|
1147
|
+
specialization.root = expandReducerCallbacks(specialization.root, component.function.getSourceFile(), call)
|
|
1148
|
+
componentSpecializations.set(call, specialization)
|
|
1149
|
+
reducerComponentCalls.add(call)
|
|
1150
|
+
}
|
|
1151
|
+
specializedDeclarations.add(component.declaration)
|
|
1152
|
+
}
|
|
1153
|
+
for (const [name, binding] of importBindings) {
|
|
1154
|
+
if (binding.kind === "namespace") continue
|
|
1155
|
+
const calls = jsxTagUses(sourceFile, name)
|
|
1156
|
+
const dispatchCalls = calls.filter(call => jsxCallHasDirectReducerProp(call, reducersForNode(call, reducersByFunction)))
|
|
1157
|
+
if (!dispatchCalls.length) continue
|
|
1158
|
+
const imported = binding.kind === "default" ? "default" : binding.imported
|
|
1159
|
+
let component
|
|
1160
|
+
try {
|
|
1161
|
+
component = resolveComponentExport(binding.target, imported, importedSource, sourceFiles)
|
|
1162
|
+
} catch {
|
|
1163
|
+
fail(dispatchCalls[0], `Reducer dispatch props require a component imported from a relative TypeScript module`)
|
|
1164
|
+
}
|
|
1165
|
+
const componentSource = component.getSourceFile()
|
|
1166
|
+
for (const call of dispatchCalls) {
|
|
1167
|
+
if (componentSpecializations.has(call)) fail(call, "Reducer dispatch props cannot be combined with another component specialization")
|
|
1168
|
+
const specialization = specialize(call, component, "Reducer-dispatch")
|
|
1169
|
+
registerRowHooks(call, specialization)
|
|
1170
|
+
specialization.root = expandReducerCallbacks(specialization.root, componentSource, call)
|
|
1171
|
+
specialization.root = mergeSpecializedImports(specialization.root, componentSource, call, specialization.effects)
|
|
1172
|
+
synthesizeTree(specialization.root)
|
|
1173
|
+
componentSpecializations.set(call, specialization)
|
|
1174
|
+
reducerComponentCalls.add(call)
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
const rawRenderedLists = []
|
|
1178
|
+
const collectRenderedLists = node => {
|
|
1179
|
+
const specialization = componentSpecializations.get(node)
|
|
1180
|
+
if (specialization) {
|
|
1181
|
+
collectRenderedLists(specialization.root)
|
|
1182
|
+
return
|
|
1183
|
+
}
|
|
1184
|
+
if (ts.isJsxExpression(node) && node.initializer === undefined && node.expression && (ts.isJsxElement(node.parent) || ts.isJsxFragment(node.parent))) {
|
|
1185
|
+
const owner = nearestFunction(node)
|
|
1186
|
+
const setters = settersForNode(node, settersByFunction)
|
|
1187
|
+
const staticCollection = state => [...(localStateSettersByFunction.get(owner) ?? [])].some(setter => setters.get(setter) === state && !referenceIdentifiers(owner.body, setter).length)
|
|
1188
|
+
const calculatedCollection = expression => {
|
|
1189
|
+
const value = unwrapExpression(expression)
|
|
1190
|
+
if (!ts.isPropertyAccessExpression(value) || !ts.isIdentifier(value.expression)) return undefined
|
|
1191
|
+
const entries = jsxLocalDeclarations.get(nearestFunction(node))?.get(value.expression.text)
|
|
1192
|
+
if (!entries?.length) return undefined
|
|
1193
|
+
const initializer = entries.length === 1 ? unwrapExpression(entries[0].initializer) : undefined
|
|
1194
|
+
if (!initializer || !ts.isCallExpression(initializer) || !ts.isIdentifier(initializer.expression) || !importBindings.has(initializer.expression.text)) return undefined
|
|
1195
|
+
if (entries[0].node.parent?.parent?.parent !== nearestFunction(entries[0].node)?.body) fail(value.expression, `Calculated collection result "${value.expression.text}" must be one top-level immutable local`)
|
|
1196
|
+
validateImportedCalculation(initializer, value.name.text)
|
|
1197
|
+
const expanded = resolveReactiveJsxExpression(value, nearestFunction(node), setters)
|
|
1198
|
+
if (expanded === value || !referencedStateNames(expanded, setters).size) fail(value, "Calculated collection fields must directly depend on local state")
|
|
1199
|
+
return expanded
|
|
1200
|
+
}
|
|
1201
|
+
const parts = listLocalUses.find(entry => entry.node === node)?.parts ?? keyedListParts(node.expression, setters, jsxLocalDeclarations.get(owner), fail, new Set(), importedCollections, factory, context, importedCollectionTransforms, calculatedCollection, staticCollection)
|
|
1202
|
+
if (parts) {
|
|
1203
|
+
for (const declaration of parts.aliasDeclarations ?? []) if (!listLocalDeclarations.includes(declaration)) listLocalDeclarations.push(declaration)
|
|
1204
|
+
rawRenderedLists.push({ node, parts })
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
ts.forEachChild(node, collectRenderedLists)
|
|
1208
|
+
}
|
|
1209
|
+
collectRenderedLists(sourceFile)
|
|
1210
|
+
const collectionAliasUses = rawRenderedLists.flatMap(({ parts }) => parts.aliasUses ?? [])
|
|
1211
|
+
const collectionAliasDeclarations = new Set(rawRenderedLists.flatMap(({ parts }) => parts.aliasDeclarations ?? []))
|
|
1212
|
+
for (const declaration of collectionAliasDeclarations) {
|
|
1213
|
+
const owner = nearestFunction(declaration)
|
|
1214
|
+
const unsupported = identifierReferences(owner.body, declaration.name.text).find(reference => !collectionAliasUses.includes(reference))
|
|
1215
|
+
if (unsupported) fail(unsupported, `Rendered collection alias "${declaration.name.text}" may only be used as a rendered collection source`)
|
|
1216
|
+
}
|
|
1217
|
+
const rejectUnsupportedRenderControl = node => {
|
|
1218
|
+
if (ts.isIfStatement(node) && containsRenderControl(node, jsxLocalsByFunction.get(nearestFunction(node)) ?? new Set())) {
|
|
1219
|
+
const setters = settersForNode(node, settersByFunction)
|
|
1220
|
+
if (referencedStateNames(node.expression, setters).size) {
|
|
1221
|
+
fail(node, "Reactive render if statements must use terminal returns or exhaustive adjacent JSX assignment")
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
ts.forEachChild(node, rejectUnsupportedRenderControl)
|
|
1225
|
+
}
|
|
1226
|
+
rejectUnsupportedRenderControl(sourceFile)
|
|
1227
|
+
const listComponentNames = new Set(rawRenderedLists.flatMap(({ parts }) => {
|
|
1228
|
+
const tag = jsxTagName(parts.root)
|
|
1229
|
+
return tag && ts.isIdentifier(tag) && tag.text[0] === tag.text[0].toUpperCase() ? [tag.text] : []
|
|
1230
|
+
}))
|
|
1231
|
+
const keyedComponentCalls = new Set(rawRenderedLists.map(({ parts }) => parts.root))
|
|
1232
|
+
for (const call of rowHookCalls) if (!keyedComponentCalls.has(call)) fail(call, "Keyed row hooks are only supported in direct keyed map rows")
|
|
1233
|
+
for (const name of listComponentNames) {
|
|
1234
|
+
let component = components.get(name)
|
|
1235
|
+
const local = Boolean(component)
|
|
1236
|
+
if (!component) {
|
|
1237
|
+
const binding = importBindings.get(name)
|
|
1238
|
+
if (!binding || binding.kind === "namespace") fail(sourceFile, `Keyed list component ${name} must be declared locally or imported from a relative TypeScript module`)
|
|
1239
|
+
const imported = binding.kind === "default" ? "default" : binding.imported
|
|
1240
|
+
component = { function: resolveComponentExport(binding.target, imported, importedSource, sourceFiles), declaration: undefined }
|
|
1241
|
+
}
|
|
1242
|
+
const declaredCalls = jsxTagUses(sourceFile, name)
|
|
1243
|
+
if (local && identifierReferenceCount(sourceFile, name) !== declaredCalls.length) fail(component.declaration, `Keyed list component ${name} may only be referenced as JSX`)
|
|
1244
|
+
const calls = [...new Set([
|
|
1245
|
+
...declaredCalls.filter(call => !stateBackedComponentFunctions.has(nearestFunction(call))),
|
|
1246
|
+
...stateBackedComponentRoots.flatMap(root => jsxTagUses(root, name))
|
|
1247
|
+
])]
|
|
1248
|
+
for (const call of calls) {
|
|
1249
|
+
const specialization = reducerComponentCalls.has(call)
|
|
1250
|
+
? componentSpecializations.get(call)
|
|
1251
|
+
: specialize(call, component.function, "Keyed list", true)
|
|
1252
|
+
registerRowHooks(call, specialization)
|
|
1253
|
+
if (specialization.effects.length && !keyedComponentCalls.has(call)) fail(call, "Effectful keyed row components may only be used directly as keyed map rows")
|
|
1254
|
+
specialization.component = component.function
|
|
1255
|
+
specialization.componentSource = component.function.getSourceFile()
|
|
1256
|
+
specialization.imported = !local
|
|
1257
|
+
componentSpecializations.set(call, specialization)
|
|
1258
|
+
}
|
|
1259
|
+
if (local) specializedDeclarations.add(component.declaration)
|
|
1260
|
+
}
|
|
1261
|
+
const expandKeyedComponents = (root, componentSource, trail = [], aggregate) => {
|
|
1262
|
+
const replacements = new WeakMap()
|
|
1263
|
+
let count = 0
|
|
1264
|
+
const visit = (node, currentAggregate = aggregate) => {
|
|
1265
|
+
if (node !== root && ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "map" && containsJsx(node)) {
|
|
1266
|
+
const nestedAggregate = { calculations: [], effects: [], hookDeclarations: [], rowStates: [], rowRefs: [], specializations: [] }
|
|
1267
|
+
for (const argument of node.arguments) visit(argument, nestedAggregate)
|
|
1268
|
+
if (nestedAggregate.hookDeclarations.length || nestedAggregate.effects.length) nestedRowSpecializations.set(`${node.pos}:${node.end}`, nestedAggregate)
|
|
1269
|
+
return
|
|
1270
|
+
}
|
|
1271
|
+
const tag = jsxTagName(node)
|
|
1272
|
+
if (tag && (!ts.isIdentifier(tag) || tag.text[0] !== tag.text[0].toLowerCase())) {
|
|
1273
|
+
if (!ts.isIdentifier(tag)) fail(node, "Keyed list components must use identifier JSX tags")
|
|
1274
|
+
const name = tag.text
|
|
1275
|
+
let component = localComponentDeclaration(componentSource, name)
|
|
1276
|
+
let imported = false
|
|
1277
|
+
if (!component) {
|
|
1278
|
+
const binding = clientImportBindings(componentSource, componentSource.fileName, sourceFiles).get(name)
|
|
1279
|
+
if (!binding || binding.kind === "namespace") fail(node, `Keyed list component ${name} must be declared locally or imported from a relative TypeScript module`)
|
|
1280
|
+
component = resolveComponentExport(binding.target, binding.kind === "default" ? "default" : binding.imported, importedSource, sourceFiles)
|
|
1281
|
+
imported = true
|
|
1282
|
+
}
|
|
1283
|
+
if (trail.includes(component)) {
|
|
1284
|
+
const chain = [...trail, component].map(entry => entry.name?.text || "anonymous").join(" -> ")
|
|
1285
|
+
fail(node, `Keyed list component cycle: ${chain}`)
|
|
1286
|
+
}
|
|
1287
|
+
const specialization = specialize(node, component, "Keyed list", true)
|
|
1288
|
+
registerRowHooks(node, specialization)
|
|
1289
|
+
specialization.root = expandKeyedComponents(specialization.root, component.getSourceFile(), [...trail, component], specialization)
|
|
1290
|
+
if (imported) synthesizeTree(specialization.root = mergeSpecializedImports(specialization.root, component.getSourceFile(), node, specialization.effects))
|
|
1291
|
+
expandedRowSpecializations.set(specialization.root, specialization)
|
|
1292
|
+
if (currentAggregate) {
|
|
1293
|
+
currentAggregate.specializations ??= []
|
|
1294
|
+
currentAggregate.specializations.push(specialization.analysis.slot, ...(specialization.specializations ?? []))
|
|
1295
|
+
currentAggregate.effects.push(...specialization.effects)
|
|
1296
|
+
currentAggregate.hookDeclarations.push(...specialization.hookDeclarations)
|
|
1297
|
+
currentAggregate.rowStates.push(...specialization.rowStates)
|
|
1298
|
+
currentAggregate.rowRefs.push(...specialization.rowRefs)
|
|
1299
|
+
}
|
|
1300
|
+
replacements.set(node, specialization.root)
|
|
1301
|
+
count++
|
|
1302
|
+
return
|
|
1303
|
+
}
|
|
1304
|
+
ts.forEachChild(node, child => visit(child, currentAggregate))
|
|
1305
|
+
}
|
|
1306
|
+
visit(root)
|
|
1307
|
+
if (!count) return root
|
|
1308
|
+
const expanded = replaceSpecializedCalls(root, replacements, context)
|
|
1309
|
+
ts.setParentRecursive(expanded, false)
|
|
1310
|
+
expanded.parent = root.parent
|
|
1311
|
+
return expanded
|
|
1312
|
+
}
|
|
1313
|
+
const preparedRenderedLists = []
|
|
1314
|
+
const prepareListCallback = (callback, root, specialization) => {
|
|
1315
|
+
const statements = [...specialization.hookDeclarations]
|
|
1316
|
+
if (specialization.effects.length) {
|
|
1317
|
+
usesListEffects = true
|
|
1318
|
+
statements.push(...specialization.effects.map(entry => {
|
|
1319
|
+
const call = factory.updateCallExpression(entry.call, factory.createIdentifier("__kListUseEffect"), entry.call.typeArguments, entry.call.arguments)
|
|
1320
|
+
synthesizeTree(call)
|
|
1321
|
+
ts.setOriginalNode(call, entry.source)
|
|
1322
|
+
return factory.createExpressionStatement(call)
|
|
1323
|
+
}))
|
|
1324
|
+
}
|
|
1325
|
+
if (!statements.length) return callback
|
|
1326
|
+
const prepared = factory.updateArrowFunction(callback, callback.modifiers, callback.typeParameters, callback.parameters, callback.type, callback.equalsGreaterThanToken, factory.createBlock([...statements, factory.createReturnStatement(root)], true))
|
|
1327
|
+
ts.setParentRecursive(prepared, false)
|
|
1328
|
+
prepared.parent = callback.parent
|
|
1329
|
+
return prepared
|
|
1330
|
+
}
|
|
1331
|
+
for (const { node, parts: originalParts } of rawRenderedLists) {
|
|
1332
|
+
if (keyedListParentTag(node) === "table") throw new Error("Keyed table rows must be wrapped in <tbody>, <thead>, or <tfoot>")
|
|
1333
|
+
const specialization = componentSpecializations.get(originalParts.root) ?? { root: originalParts.root, calculations: [], effects: [], hookDeclarations: [], rowStates: [], rowRefs: [], ordinaryStates: [] }
|
|
1334
|
+
const componentSource = specialization.componentSource ?? sourceFile
|
|
1335
|
+
specialization.root = expandKeyedComponents(specialization.root, componentSource, specialization.component ? [specialization.component] : [], specialization)
|
|
1336
|
+
if (specialization.imported) synthesizeTree(specialization.root = mergeSpecializedImports(specialization.root, componentSource, originalParts.root, specialization.effects))
|
|
1337
|
+
if (specialization.root !== originalParts.root) componentSpecializations.set(originalParts.root, specialization)
|
|
1338
|
+
const root = specialization.root
|
|
1339
|
+
let callback = root === originalParts.root ? originalParts.callback : factory.updateArrowFunction(
|
|
1340
|
+
originalParts.callback,
|
|
1341
|
+
originalParts.callback.modifiers,
|
|
1342
|
+
originalParts.callback.typeParameters,
|
|
1343
|
+
originalParts.callback.parameters,
|
|
1344
|
+
originalParts.callback.type,
|
|
1345
|
+
originalParts.callback.equalsGreaterThanToken,
|
|
1346
|
+
root
|
|
1347
|
+
)
|
|
1348
|
+
if (callback !== originalParts.callback) {
|
|
1349
|
+
ts.setParentRecursive(callback, false)
|
|
1350
|
+
callback.parent = originalParts.callback.parent
|
|
1351
|
+
}
|
|
1352
|
+
callback = prepareListCallback(callback, root, specialization)
|
|
1353
|
+
const parts = {
|
|
1354
|
+
...originalParts,
|
|
1355
|
+
root,
|
|
1356
|
+
callback,
|
|
1357
|
+
specializations: [specialization.analysis?.slot, ...(specialization.specializations ?? [])].filter(slot => slot !== undefined),
|
|
1358
|
+
rowStates: [...specialization.rowStates, ...specialization.ordinaryStates],
|
|
1359
|
+
rowRefs: specialization.rowRefs,
|
|
1360
|
+
analysisStateOwners: new Map([...stateOwnersForNode(originalParts.root), ...[...specialization.rowStates, ...specialization.ordinaryStates].map(state => [state.state, state.analysisOwner])])
|
|
1361
|
+
}
|
|
1362
|
+
for (const calculation of specialization.calculations) {
|
|
1363
|
+
ts.setParentRecursive(calculation, false)
|
|
1364
|
+
calculation.parent = callback
|
|
1365
|
+
validateListExpression(calculation, parts.item, originalParts.root, fail)
|
|
1366
|
+
}
|
|
1367
|
+
const analysis = validateKeyedList(parts, sourceFile, settersForNode(originalParts.root, settersByFunction), specialization.rowStates, componentSpecializations, expandedRowSpecializations, nestedRowSpecializations, factory, prepareListCallback)
|
|
1368
|
+
preparedRenderedLists.push({ node, parts, analysis })
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
const compileRenderExpression = (expression, anchor) => {
|
|
1372
|
+
const parts = conditionalParts(expression)
|
|
1373
|
+
if (!parts) return ts.visitNode(expression, visitor)
|
|
1374
|
+
const setters = settersForNode(anchor, settersByFunction)
|
|
1375
|
+
const usedStates = referencedStateNames(parts.condition, setters)
|
|
1376
|
+
const captures = captureNames(parts.condition, parts.condition, setters)
|
|
1377
|
+
if (!usedStates.size && !captures.size) return ts.visitEachChild(expression, visitor, context)
|
|
1378
|
+
usesBehavior = true
|
|
1379
|
+
usesConditional = true
|
|
1380
|
+
return descriptors.compileConditional(
|
|
1381
|
+
parts.kind,
|
|
1382
|
+
parts.condition,
|
|
1383
|
+
compileRenderExpression(parts.truthy, anchor),
|
|
1384
|
+
compileRenderExpression(parts.falsy, anchor),
|
|
1385
|
+
setters
|
|
1386
|
+
)
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
let activeStateOwners
|
|
1390
|
+
let activeKeyedBlock
|
|
1391
|
+
const visitWithStateOwners = (node, stateOwners) => {
|
|
1392
|
+
const previous = activeStateOwners
|
|
1393
|
+
activeStateOwners = new Map([...(previous ?? []), ...stateOwners])
|
|
1394
|
+
const result = ts.visitNode(node, visitor)
|
|
1395
|
+
activeStateOwners = previous
|
|
1396
|
+
return result
|
|
1397
|
+
}
|
|
1398
|
+
const keyedEntry = (entries, node) => entries.find(entry => entry.node === node)
|
|
1399
|
+
const compileKeyedBlock = (node, { parts: listParts, analysis }) => {
|
|
1400
|
+
usesBehavior = true
|
|
1401
|
+
usesList = true
|
|
1402
|
+
const blockSlot = moduleIR.keyedBlocks.length
|
|
1403
|
+
let listSource = listParts.state
|
|
1404
|
+
let collection = { kind: "signal", name: listParts.state?.text }
|
|
1405
|
+
if (listParts.calculation) {
|
|
1406
|
+
usesBinding = true
|
|
1407
|
+
listSource = descriptors.compileReactiveBinding(listParts.calculation, { setters: settersForNode(node, settersByFunction), importBindings, keyedBlock: blockSlot })
|
|
1408
|
+
const exportName = ts.isCallExpression(listSource) && ts.isStringLiteral(listSource.arguments[2]) ? listSource.arguments[2].text : undefined
|
|
1409
|
+
collection = { kind: "binding", ...(exportName ? { exportName } : {}) }
|
|
1410
|
+
}
|
|
1411
|
+
const derived = listParts.selector?.length ? descriptors.registerDerived("selector", listParts.selector, listParts.selectorStates, node) : undefined
|
|
1412
|
+
const parent = activeKeyedBlock?.block
|
|
1413
|
+
const rowStates = (listParts.rowStates ?? []).map(state => ({ name: state.state, setter: state.setter, owner: state.analysisOwner, ...(analysisSource(state.source) ? { source: analysisSource(state.source) } : {}) }))
|
|
1414
|
+
const rowRefs = (listParts.rowRefs ?? []).map(ref => ({ name: ref.name, owner: ref.analysisOwner, ...(analysisSource(ref.source) ? { source: analysisSource(ref.source) } : {}) }))
|
|
1415
|
+
const specializations = [...new Set([...(listParts.specializations ?? []), ...rowStates.map(state => state.owner), ...rowRefs.map(ref => ref.owner)].filter(value => value !== undefined).map(value => typeof value === "string" ? Number(value.slice(value.lastIndexOf(":") + 1)) : value))]
|
|
1416
|
+
const block = descriptors.registerKeyedBlock({
|
|
1417
|
+
...(analysisSource(node) ? { source: analysisSource(node) } : {}),
|
|
1418
|
+
...(parent ? { parent: parent.slot } : {}),
|
|
1419
|
+
children: [],
|
|
1420
|
+
collection,
|
|
1421
|
+
key: listParts.keyField,
|
|
1422
|
+
...(listParts.ownerField ? { ownerField: listParts.ownerField } : {}),
|
|
1423
|
+
item: listParts.item,
|
|
1424
|
+
...(listParts.index ? { index: listParts.index } : {}),
|
|
1425
|
+
indexed: listParts.indexed,
|
|
1426
|
+
static: Boolean(listParts.static),
|
|
1427
|
+
...(derived ? { selector: derived.slot } : {}),
|
|
1428
|
+
selectorStates: [...(listParts.selectorStates ?? [])],
|
|
1429
|
+
specializations,
|
|
1430
|
+
rowStates,
|
|
1431
|
+
rowRefs
|
|
1432
|
+
})
|
|
1433
|
+
if (parent) parent.children.push(block.slot)
|
|
1434
|
+
const previous = activeKeyedBlock
|
|
1435
|
+
activeKeyedBlock = { analysis, block, parts: listParts }
|
|
1436
|
+
const callback = visitWithStateOwners(listParts.callback, listParts.analysisStateOwners ?? new Map())
|
|
1437
|
+
activeKeyedBlock = previous
|
|
1438
|
+
const arguments_ = [
|
|
1439
|
+
listSource,
|
|
1440
|
+
block.key === null ? factory.createNull() : factory.createStringLiteral(block.key),
|
|
1441
|
+
callback,
|
|
1442
|
+
factory.createStringLiteral(block.ownerField ?? ""),
|
|
1443
|
+
jsonExpression(derived?.selector ?? listParts.selector ?? [], factory),
|
|
1444
|
+
block.indexed ? factory.createTrue() : factory.createFalse()
|
|
1445
|
+
]
|
|
1446
|
+
if (block.selectorStates.length || block.static) arguments_.push(factory.createArrayLiteralExpression(block.selectorStates.map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), factory.createIdentifier(name)]))))
|
|
1447
|
+
if (block.static) arguments_.push(factory.createTrue())
|
|
1448
|
+
return factory.updateJsxExpression(node, factory.createCallExpression(factory.createIdentifier("__kList"), undefined, arguments_))
|
|
1449
|
+
}
|
|
1450
|
+
const visitor = node => {
|
|
1451
|
+
if (ts.isVariableDeclaration(node) && ts.isObjectBindingPattern(node.name) && customHookPrivateFields.has(node)) {
|
|
1452
|
+
const privateFields = customHookPrivateFields.get(node)
|
|
1453
|
+
return factory.updateVariableDeclaration(node, factory.updateObjectBindingPattern(node.name, [
|
|
1454
|
+
...node.name.elements,
|
|
1455
|
+
...privateFields.map(name => factory.createBindingElement(undefined, undefined, name))
|
|
1456
|
+
]), node.exclamationToken, node.type, node.initializer)
|
|
1457
|
+
}
|
|
1458
|
+
if (ts.isBlock(node) && setterHookHelpers.has(node)) {
|
|
1459
|
+
return ts.visitEachChild(factory.updateBlock(node, [...setterHookHelpers.get(node), ...node.statements]), visitor, context)
|
|
1460
|
+
}
|
|
1461
|
+
if (specializedDeclarations.has(node)) return node
|
|
1462
|
+
if (componentSpecializations.has(node)) {
|
|
1463
|
+
const specialization = componentSpecializations.get(node)
|
|
1464
|
+
const stateOwners = new Map([...stateOwnersForNode(node), ...[...specialization.rowStates, ...specialization.ordinaryStates].map(state => [state.state, state.analysisOwner])])
|
|
1465
|
+
return visitWithStateOwners(specialization.root, stateOwners)
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
if (hasLinkElements && (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) && isStylesheetLink(node)) {
|
|
1469
|
+
fail(node, "Stylesheets must be placed under src/ or declared in kudzu.config styles so Kudzu can emit them in <head>")
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text === "react") {
|
|
1473
|
+
if (!node.importClause) fail(node, "Side-effect React imports are not supported because Kudzu does not load the React runtime")
|
|
1474
|
+
if (node.importClause.isTypeOnly) return node
|
|
1475
|
+
return factory.updateImportDeclaration(node, node.modifiers, node.importClause, factory.createStringLiteral("@kudzujs/core"), node.attributes)
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && packageBindings.size && importDeclarationNames(node).some(name => packageBindings.has(name))) return undefined
|
|
1479
|
+
|
|
1480
|
+
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text.startsWith(".")) {
|
|
1481
|
+
if (!runtimeModuleReference(node)) return node
|
|
1482
|
+
if (isStaticImport(node.moduleSpecifier.text)) return staticImportEntry(node, sourceFile, file, staticFiles, importedAssets, cssModules, base, factory)?.replacement
|
|
1483
|
+
const target = resolveSourceImport(file, node.moduleSpecifier.text, sourceFiles)
|
|
1484
|
+
return factory.updateImportDeclaration(node, node.modifiers, node.importClause, factory.createStringLiteral(relativeModulePath(compiledPath(file), compiledPath(target))), node.attributes)
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
if (ts.isExportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text.startsWith(".")) {
|
|
1488
|
+
if (!runtimeModuleReference(node)) return node
|
|
1489
|
+
const target = resolveSourceImport(file, node.moduleSpecifier.text, sourceFiles)
|
|
1490
|
+
return factory.updateExportDeclaration(node, node.modifiers, node.isTypeOnly, node.exportClause, factory.createStringLiteral(relativeModulePath(compiledPath(file), compiledPath(target))), node.attributes)
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
const effectAlias = ts.isCallExpression(node) && ts.isIdentifier(node.expression) ? node.expression.text : undefined
|
|
1494
|
+
const listEffect = effectAlias === "__kListUseEffect"
|
|
1495
|
+
const specializedEffect = listEffect || effectAlias === "__kComponentUseEffect" ? (() => {
|
|
1496
|
+
const source = ts.getOriginalNode(node)
|
|
1497
|
+
const sourceFile = source.getSourceFile()
|
|
1498
|
+
return { source, sourceFile, imports: clientImportBindings(sourceFile, sourceFile.fileName, sourceFiles) }
|
|
1499
|
+
})() : undefined
|
|
1500
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && (hasUseEffectImport && effectAlias === "useEffect" || specializedEffect)) {
|
|
1501
|
+
const effectFail = (target, message) => {
|
|
1502
|
+
if (specializedEffect) throw sourceNodeError(specializedEffect.source, specializedEffect.sourceFile, message)
|
|
1503
|
+
fail(target, message)
|
|
1504
|
+
}
|
|
1505
|
+
if (node.arguments.length !== 2) effectFail(node, "useEffect() requires exactly a callback and literal dependency array")
|
|
1506
|
+
const [callbackArgument, dependencies] = node.arguments
|
|
1507
|
+
const effectOwner = nearestFunction(node)
|
|
1508
|
+
const resolveEffectFunction = expression => {
|
|
1509
|
+
if (!ts.isIdentifier(expression)) return undefined
|
|
1510
|
+
const entries = jsxLocalDeclarations.get(effectOwner)?.get(expression.text)
|
|
1511
|
+
if (entries?.length !== 1 || entries[0].node.parent?.parent?.parent !== effectOwner?.body) return undefined
|
|
1512
|
+
const initializer = entries[0].initializer
|
|
1513
|
+
return ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer) ? initializer : undefined
|
|
1514
|
+
}
|
|
1515
|
+
let callback = ts.isArrowFunction(callbackArgument) || ts.isFunctionExpression(callbackArgument) ? callbackArgument : resolveEffectFunction(callbackArgument)
|
|
1516
|
+
if (!callback) effectFail(callbackArgument, "useEffect() callback must be inline or one top-level const function")
|
|
1517
|
+
if (ts.isFunctionExpression(callback) && callback.name) effectFail(callback, "useEffect() callback function must be anonymous")
|
|
1518
|
+
if (callback.asteriskToken) effectFail(callback, "useEffect() callback cannot be a generator")
|
|
1519
|
+
if (callback.parameters.length) effectFail(callback, "useEffect() callback cannot declare parameters")
|
|
1520
|
+
if (!ts.isArrayLiteralExpression(dependencies)) effectFail(dependencies, "useEffect() dependencies must be a literal array")
|
|
1521
|
+
const setters = settersForNode(node, settersByFunction)
|
|
1522
|
+
const dependencyAnalysis = analyzeEffectDependencies({
|
|
1523
|
+
dependencies,
|
|
1524
|
+
node,
|
|
1525
|
+
listEffect,
|
|
1526
|
+
keyedItem: activeKeyedBlock?.parts.item,
|
|
1527
|
+
setters,
|
|
1528
|
+
localDeclarations: jsxLocalDeclarations.get(nearestFunction(node)),
|
|
1529
|
+
factory,
|
|
1530
|
+
fail: effectFail
|
|
1531
|
+
})
|
|
1532
|
+
const { dependencyItem, itemDependencies, ordinaryDependencies, entries: dependencyEntries, dependencyStates, substitutions: dependencySubstitutions, subscriptions: subscriptionDependencies, hasDerived: hasDerivedDependency } = dependencyAnalysis
|
|
1533
|
+
if (!effectOwner) fail(node, "useEffect() cannot be used outside a Kudzu component")
|
|
1534
|
+
if (!ts.isBlock(callback.body)) effectFail(callback, "useEffect() callback must use a block body")
|
|
1535
|
+
const cleanupSubstitutions = new Map()
|
|
1536
|
+
const collectNamedCleanups = current => {
|
|
1537
|
+
if (current !== callback && isFunctionLike(current)) return
|
|
1538
|
+
if (ts.isReturnStatement(current) && current.expression && ts.isIdentifier(unwrapExpression(current.expression))) {
|
|
1539
|
+
const cleanup = resolveEffectFunction(unwrapExpression(current.expression))
|
|
1540
|
+
if (cleanup) cleanupSubstitutions.set(unwrapExpression(current.expression).text, cleanup)
|
|
1541
|
+
}
|
|
1542
|
+
ts.forEachChild(current, collectNamedCleanups)
|
|
1543
|
+
}
|
|
1544
|
+
collectNamedCleanups(callback.body)
|
|
1545
|
+
if (cleanupSubstitutions.size) {
|
|
1546
|
+
callback = substituteClone(callback, cleanupSubstitutions, factory, context)
|
|
1547
|
+
ts.setParentRecursive(callback, false)
|
|
1548
|
+
callback.parent = callbackArgument.parent
|
|
1549
|
+
}
|
|
1550
|
+
const returns = effectReturns(callback)
|
|
1551
|
+
if (returns.invalid) effectFail(returns.invalid, "useEffect() return values must be inline cleanup functions")
|
|
1552
|
+
const invalidCleanup = returns.cleanups.find(cleanup => cleanup.parameters.length || cleanup.asteriskToken)
|
|
1553
|
+
if (invalidCleanup) effectFail(invalidCleanup, "useEffect() cleanup functions cannot declare parameters or be generators")
|
|
1554
|
+
if (returns.cleanup && callback.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) effectFail(callback, "useEffect() async callbacks cannot return cleanup functions")
|
|
1555
|
+
validateEffectOwnedBrowserResources(callback, returns, effectFail)
|
|
1556
|
+
const callbackSource = specializedEffect?.sourceFile ?? sourceFile
|
|
1557
|
+
const callbackFile = callbackSource.fileName
|
|
1558
|
+
let compiledCallback = dependencySubstitutions.size ? substituteClone(callback, dependencySubstitutions, factory, context) : callback
|
|
1559
|
+
if (compiledCallback !== callback) {
|
|
1560
|
+
ts.setParentRecursive(compiledCallback, false)
|
|
1561
|
+
compiledCallback.parent = callback.parent
|
|
1562
|
+
}
|
|
1563
|
+
let workers = []
|
|
1564
|
+
if (listEffect && callbackFile !== file) {
|
|
1565
|
+
const originalCallback = specializedEffect.source.arguments[0]
|
|
1566
|
+
workerCompiler.rejectConstructions(originalCallback, callbackSource, "Relative TypeScript Worker construction in imported keyed-row effects is not supported; construct the Worker in a directly compiled page or local component effect")
|
|
1567
|
+
} else {
|
|
1568
|
+
const rewritten = workerCompiler.rewriteEffect(compiledCallback, callbackFile, callbackSource, sourceFiles, factory, context)
|
|
1569
|
+
compiledCallback = rewritten.callback
|
|
1570
|
+
workers = rewritten.workers
|
|
1571
|
+
}
|
|
1572
|
+
const descriptor = descriptors.compileEffectCallback(compiledCallback, {
|
|
1573
|
+
setters,
|
|
1574
|
+
reducers: reducersForNode(node, reducersByFunction),
|
|
1575
|
+
importBindings: specializedEffect?.imports ?? importBindings,
|
|
1576
|
+
listItem: dependencyItem,
|
|
1577
|
+
keyedBlock: activeKeyedBlock?.block.slot,
|
|
1578
|
+
deferValues: true,
|
|
1579
|
+
snapshotNested: returns.cleanup,
|
|
1580
|
+
liveStates: customHookTimerStates
|
|
1581
|
+
})
|
|
1582
|
+
usesListItem ||= Boolean(itemDependencies.length && !listEffect)
|
|
1583
|
+
usesBehavior = true
|
|
1584
|
+
const derivedDependencies = hasDerivedDependency ? dependencyEntries.map(entry => entry.kind === "derived" ? descriptors.registerDerived("expression", entry.expression, entry.states, entry.source) : undefined) : []
|
|
1585
|
+
const effectSource = specializedEffect?.source ?? node
|
|
1586
|
+
const lexicalOwner = nearestFunction(effectSource)
|
|
1587
|
+
const effect = descriptors.registerEffect(descriptor, {
|
|
1588
|
+
cleanup: returns.cleanup,
|
|
1589
|
+
dependencies: hasDerivedDependency ? dependencyEntries.map((entry, index) => entry.kind === "derived" ? { kind: "derived", derived: derivedDependencies[index].slot, sources: [...entry.states] } : { kind: "signal", name: entry.name }) : ordinaryDependencies.map(dependency => ({ kind: "signal", name: dependency.text })),
|
|
1590
|
+
subscriptions: (hasDerivedDependency ? subscriptionDependencies : ordinaryDependencies).map(dependency => dependency.text),
|
|
1591
|
+
dependencyStates: [...dependencyStates.keys()],
|
|
1592
|
+
itemDependencies,
|
|
1593
|
+
ownership: {
|
|
1594
|
+
kind: activeKeyedBlock ? "keyed" : "component",
|
|
1595
|
+
...(activeKeyedBlock ? { keyedBlock: activeKeyedBlock.block.slot } : {}),
|
|
1596
|
+
...(lexicalOwner ? { component: { name: ownerName(lexicalOwner), ...(analysisSource(lexicalOwner) ? { source: analysisSource(lexicalOwner) } : {}) } } : {})
|
|
1597
|
+
},
|
|
1598
|
+
workers,
|
|
1599
|
+
...(analysisSource(effectSource) ? { source: analysisSource(effectSource) } : {})
|
|
1600
|
+
})
|
|
1601
|
+
const dependencyExpressions = effect.dependencies.map(dependency => dependency.kind === "derived" ? moduleIR.derived[dependency.derived].expression : ["state", dependency.name])
|
|
1602
|
+
return factory.updateCallExpression(node, node.expression, node.typeArguments, [
|
|
1603
|
+
callback,
|
|
1604
|
+
factory.createArrayLiteralExpression(effect.subscriptions.map(name => factory.createIdentifier(name))),
|
|
1605
|
+
factory.createStringLiteral(handlerUrl),
|
|
1606
|
+
factory.createStringLiteral(effect.setup.exportName),
|
|
1607
|
+
descriptor.states,
|
|
1608
|
+
descriptor.scope,
|
|
1609
|
+
factory.createStringLiteral(specializedEffect ? sourceLocation(specializedEffect.source, specializedEffect.sourceFile) : sourceLocation(node, sourceFile)),
|
|
1610
|
+
effect.cleanup ? factory.createTrue() : factory.createFalse(),
|
|
1611
|
+
factory.createArrayLiteralExpression(effect.itemDependencies.map(field => factory.createStringLiteral(field))),
|
|
1612
|
+
hasDerivedDependency ? jsonExpression(dependencyExpressions, factory) : factory.createArrayLiteralExpression(),
|
|
1613
|
+
factory.createArrayLiteralExpression(effect.dependencyStates.map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), factory.createIdentifier(name)])))
|
|
1614
|
+
])
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
if (ts.isVariableDeclaration(node) && ts.isArrayBindingPattern(node.name) && node.initializer && ts.isCallExpression(node.initializer) && ts.isIdentifier(node.initializer.expression) && ((node.initializer.expression.text === "useState" || node.initializer.expression.text === "__kRowUseState" || node.initializer.expression.text === "__kComponentUseState") && node.initializer.arguments.length === 1 || node.initializer.expression.text === "useReducer" && node.initializer.arguments.length === 2)) {
|
|
1618
|
+
const stateElement = node.name.elements[0]
|
|
1619
|
+
if (!stateElement || !ts.isBindingElement(stateElement) || !ts.isIdentifier(stateElement.name)) return node
|
|
1620
|
+
const initializer = factory.updateCallExpression(node.initializer, node.initializer.expression, node.initializer.typeArguments, [
|
|
1621
|
+
...node.initializer.arguments,
|
|
1622
|
+
factory.createStringLiteral(stateElement.name.text)
|
|
1623
|
+
])
|
|
1624
|
+
return factory.updateVariableDeclaration(node, node.name, node.exclamationToken, node.type, initializer)
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
if (ts.isVariableDeclaration(node) && listLocalDeclarations.includes(node)) {
|
|
1628
|
+
return factory.updateVariableDeclaration(node, node.name, node.exclamationToken, node.type, factory.createIdentifier("undefined"))
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isCallExpression(node.initializer) && ts.isIdentifier(node.initializer.expression) && importBindings.has(node.initializer.expression.text)) {
|
|
1632
|
+
const setters = settersForNode(node, settersByFunction)
|
|
1633
|
+
const stateNames = new Set(setters.values())
|
|
1634
|
+
const rewrite = current => {
|
|
1635
|
+
if (ts.isShorthandPropertyAssignment(current) && stateNames.has(current.name.text)) return factory.createPropertyAssignment(current.name, factory.createPropertyAccessExpression(current.name, "value"))
|
|
1636
|
+
if (ts.isIdentifier(current) && stateNames.has(current.text) && isReferenceIdentifier(current)) return factory.createPropertyAccessExpression(current, "value")
|
|
1637
|
+
return ts.visitEachChild(current, rewrite, context)
|
|
1638
|
+
}
|
|
1639
|
+
if (referencedStateNames(node.initializer, setters).size) return factory.updateVariableDeclaration(node, node.name, node.exclamationToken, node.type, ts.visitNode(node.initializer, rewrite))
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && jsxLocalsByFunction.get(nearestFunction(node))?.has(node.name.text) && referencesIdentifier(nearestFunction(node).body, node.name.text)) {
|
|
1643
|
+
const compiled = compileRenderExpression(node.initializer, node)
|
|
1644
|
+
if (compiled !== node.initializer) return factory.updateVariableDeclaration(node, node.name, node.exclamationToken, node.type, compiled)
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
if (ts.isReturnStatement(node) && node.expression && isJsxLocalValue(node.expression, jsxLocalsByFunction.get(nearestFunction(node)) ?? new Set())) {
|
|
1648
|
+
const compiled = compileRenderExpression(node.expression, node)
|
|
1649
|
+
if (compiled !== node.expression) return factory.updateReturnStatement(node, compiled)
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
const listCondition = ts.isJsxExpression(node) && node.expression ? keyedEntry(activeKeyedBlock?.analysis.conditions ?? [], node.expression) : undefined
|
|
1653
|
+
if (listCondition) {
|
|
1654
|
+
const entry = listCondition.value
|
|
1655
|
+
return factory.updateJsxExpression(node, descriptors.compileListConditional({
|
|
1656
|
+
...entry,
|
|
1657
|
+
keyedBlock: activeKeyedBlock.block.slot,
|
|
1658
|
+
truthy: ts.visitNode(entry.truthy, visitor),
|
|
1659
|
+
falsy: ts.visitNode(entry.falsy, visitor)
|
|
1660
|
+
}))
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
const listValue = ts.isJsxExpression(node) && node.expression ? keyedEntry(activeKeyedBlock?.analysis.values ?? [], node.expression) : undefined
|
|
1664
|
+
if (listValue) {
|
|
1665
|
+
return factory.updateJsxExpression(node, descriptors.compileListValue(node.expression, { ...listValue.value, keyedBlock: activeKeyedBlock.block.slot }))
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
const attributeListValue = ts.isJsxAttribute(node) && node.initializer && ts.isJsxExpression(node.initializer) && node.initializer.expression ? keyedEntry(activeKeyedBlock?.analysis.values ?? [], node.initializer.expression) : undefined
|
|
1669
|
+
if (attributeListValue) {
|
|
1670
|
+
return factory.updateJsxAttribute(node, node.name, factory.createJsxExpression(undefined, descriptors.compileListValue(node.initializer.expression, { ...attributeListValue.value, keyedBlock: activeKeyedBlock.block.slot })))
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
if (ts.isJsxExpression(node) && node.initializer === undefined && node.expression && (ts.isJsxElement(node.parent) || ts.isJsxFragment(node.parent))) {
|
|
1674
|
+
const renderedList = keyedEntry(preparedRenderedLists, node)
|
|
1675
|
+
const nestedList = keyedEntry(activeKeyedBlock?.analysis.nested ?? [], unwrapExpression(node.expression))
|
|
1676
|
+
if (renderedList || nestedList) return compileKeyedBlock(node, renderedList ?? nestedList)
|
|
1677
|
+
const conditional = conditionalParts(node.expression)
|
|
1678
|
+
if (conditional) {
|
|
1679
|
+
const compiled = compileRenderExpression(node.expression, node)
|
|
1680
|
+
if (compiled !== node.expression) return factory.updateJsxExpression(node, compiled)
|
|
1681
|
+
}
|
|
1682
|
+
const setters = settersForNode(node, settersByFunction)
|
|
1683
|
+
const expression = resolveReactiveJsxExpression(node.expression, nearestFunction(node), setters)
|
|
1684
|
+
const usedStates = referencedStateNames(expression, setters)
|
|
1685
|
+
const captures = captureNames(expression, expression, setters)
|
|
1686
|
+
if ((usedStates.size || captures.size) && !ts.isIdentifier(expression) && !containsJsx(expression)) {
|
|
1687
|
+
usesBehavior = true
|
|
1688
|
+
usesBinding = true
|
|
1689
|
+
return factory.updateJsxExpression(node, descriptors.compileReactiveBinding(expression, { setters, importBindings }))
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
if (ts.isJsxAttribute(node) && node.initializer && ts.isJsxExpression(node.initializer) && node.initializer.expression && !isContextProviderValue(node, contexts) && !/^on/i.test(node.name.text) && !["key", "ref", "dangerouslysetinnerhtml"].includes(node.name.text.toLowerCase())) {
|
|
1694
|
+
const sourceExpression = node.initializer.expression
|
|
1695
|
+
const setters = settersForNode(node, settersByFunction)
|
|
1696
|
+
const expression = resolveReactiveJsxExpression(sourceExpression, nearestFunction(node), setters)
|
|
1697
|
+
const usedStates = referencedStateNames(expression, setters)
|
|
1698
|
+
const captures = captureNames(expression, expression, setters)
|
|
1699
|
+
if ((usedStates.size || captures.size) && !ts.isIdentifier(expression)) {
|
|
1700
|
+
usesBehavior = true
|
|
1701
|
+
usesBinding = true
|
|
1702
|
+
const compiled = descriptors.compileReactiveBinding(expression, { setters, importBindings })
|
|
1703
|
+
return factory.updateJsxAttribute(node, node.name, factory.createJsxExpression(undefined, compiled))
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
if (ts.isJsxAttribute(node) && node.initializer && ts.isJsxExpression(node.initializer) && node.initializer.expression && /^on[A-Z]/.test(node.name.text)) {
|
|
1708
|
+
const setters = settersForNode(node, settersByFunction)
|
|
1709
|
+
const event = descriptors.compileEvent(node.initializer.expression, {
|
|
1710
|
+
owner: fallbackOwner(node),
|
|
1711
|
+
stateOwners: activeStateOwners ?? stateOwnersForNode(node),
|
|
1712
|
+
setters,
|
|
1713
|
+
reducers: reducersForNode(node, reducersByFunction),
|
|
1714
|
+
functions: functionsForNode(node),
|
|
1715
|
+
listItem: activeKeyedBlock ? { item: activeKeyedBlock.parts.item, index: activeKeyedBlock.parts.index } : undefined,
|
|
1716
|
+
keyedBlock: activeKeyedBlock?.block.slot,
|
|
1717
|
+
importBindings: new Map([...importBindings, ...packageBindings])
|
|
1718
|
+
})
|
|
1719
|
+
if (event) {
|
|
1720
|
+
usesBehavior = true
|
|
1721
|
+
return factory.updateJsxAttribute(node, node.name, factory.createJsxExpression(undefined, event))
|
|
1722
|
+
}
|
|
1723
|
+
if (ts.isIdentifier(node.initializer.expression) && isDestructuredParameter(node.initializer.expression, nearestFunction(node))) return node
|
|
1724
|
+
const position = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile))
|
|
1725
|
+
throw new Error(`${sourceFile.fileName}:${position.line + 1}:${position.character + 1} ${node.name.text} must reference a function`)
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
return ts.visitEachChild(node, visitor, context)
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
const transformed = ts.visitNode(sourceFile, visitor)
|
|
1732
|
+
descriptors.finalize()
|
|
1733
|
+
if (!usesBehavior) return transformed
|
|
1734
|
+
|
|
1735
|
+
const behaviorImports = [factory.createImportSpecifier(false, factory.createIdentifier("behavior"), factory.createIdentifier("__kBehavior"))]
|
|
1736
|
+
if (moduleIR.handlers.some(handler => handler.kind === "module-export" && handler.role === "native")) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("nativeBehavior"), factory.createIdentifier("__kNativeBehavior")))
|
|
1737
|
+
if (usesBinding) {
|
|
1738
|
+
behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("binding"), factory.createIdentifier("__kBinding")))
|
|
1739
|
+
behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("select"), factory.createIdentifier("__kSelect")))
|
|
1740
|
+
}
|
|
1741
|
+
if (usesConditional) {
|
|
1742
|
+
behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("conditional"), factory.createIdentifier("__kConditional")))
|
|
1743
|
+
behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("stateConditional"), factory.createIdentifier("__kStateConditional")))
|
|
1744
|
+
}
|
|
1745
|
+
if (usesList) {
|
|
1746
|
+
behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("list"), factory.createIdentifier("__kList")))
|
|
1747
|
+
behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("listExpression"), factory.createIdentifier("__kListExpression")))
|
|
1748
|
+
behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("listField"), factory.createIdentifier("__kListField")))
|
|
1749
|
+
behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("listItem"), factory.createIdentifier("__kListItem")))
|
|
1750
|
+
behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("listIndex"), factory.createIdentifier("__kListIndex")))
|
|
1751
|
+
behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("listConditional"), factory.createIdentifier("__kListConditional")))
|
|
1752
|
+
}
|
|
1753
|
+
if (usesListItem && !usesList) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("listItem"), factory.createIdentifier("__kListItem")))
|
|
1754
|
+
if (usesListEffects) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("useEffect"), factory.createIdentifier("__kListUseEffect")))
|
|
1755
|
+
if (usesRowState) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("useState"), factory.createIdentifier("__kRowUseState")))
|
|
1756
|
+
if (usesRowRef) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("useRef"), factory.createIdentifier("__kRowUseRef")))
|
|
1757
|
+
if (usesComponentState) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("useState"), factory.createIdentifier("__kComponentUseState")))
|
|
1758
|
+
if (usesComponentId) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("useId"), factory.createIdentifier("__kComponentUseId")))
|
|
1759
|
+
if (usesComponentRef) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("useRef"), factory.createIdentifier("__kComponentUseRef")))
|
|
1760
|
+
if (usesComponentEffects) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("useEffect"), factory.createIdentifier("__kComponentUseEffect")))
|
|
1761
|
+
if (usesBinding || usesConditional) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("bindingValue"), factory.createIdentifier("__kBindingValue")))
|
|
1762
|
+
const behaviorImport = factory.createImportDeclaration(
|
|
1763
|
+
undefined,
|
|
1764
|
+
factory.createImportClause(false, undefined, factory.createNamedImports(behaviorImports)),
|
|
1765
|
+
factory.createStringLiteral("@kudzujs/core")
|
|
1766
|
+
)
|
|
1767
|
+
return factory.updateSourceFile(transformed, [behaviorImport, ...transformed.statements])
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
function containsRenderControl(root, knownLocals) {
|
|
1772
|
+
let found = false
|
|
1773
|
+
const visit = node => {
|
|
1774
|
+
if (isFunctionLike(node) && node !== root) return
|
|
1775
|
+
if (ts.isReturnStatement(node) && node.expression && isJsxLocalValue(node.expression, knownLocals)) found = true
|
|
1776
|
+
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken && containsJsx(node.right)) found = true
|
|
1777
|
+
if (!found) ts.forEachChild(node, visit)
|
|
1778
|
+
}
|
|
1779
|
+
visit(root)
|
|
1780
|
+
return found
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
function keyedListParts(expression, setters, declarations, fail, aliases = new Set(), importedCollections = new Set(), factory = ts.factory, context, importedCollectionTransforms = new Map(), calculatedCollection, staticCollection) {
|
|
1784
|
+
const value = unwrapExpression(expression)
|
|
1785
|
+
const directFrom = isArrayFromCall(value) && value.arguments.length === 2 && containsJsx(value.arguments[1])
|
|
1786
|
+
if (!directFrom && (!ts.isCallExpression(value) || value.arguments.length !== 1 || !ts.isPropertyAccessExpression(value.expression) || value.expression.name.text !== "map")) return undefined
|
|
1787
|
+
let collection = analyzeCollectionPipeline(directFrom ? value.arguments[0] : value.expression.expression, {
|
|
1788
|
+
setters, declarations, fail, aliases, importedCollections, stateNames: new Set(setters.values()), importedCollectionTransforms, calculatedCollection, staticCollection
|
|
1789
|
+
})
|
|
1790
|
+
if (!collection?.state && !collection?.calculation) return undefined
|
|
1791
|
+
if (directFrom) collection.selector.push(["from", undefined])
|
|
1792
|
+
let callback = directFrom ? value.arguments[1] : value.arguments[0]
|
|
1793
|
+
const parameters = collectionParameters(callback, "Keyed list map", fail)
|
|
1794
|
+
let root = unwrapExpression(callback.body)
|
|
1795
|
+
if (ts.isBlock(root)) {
|
|
1796
|
+
if (!context || root.statements.length !== 2 || !ts.isVariableStatement(root.statements[0]) || (root.statements[0].declarationList.flags & ts.NodeFlags.Const) === 0 || root.statements[0].declarationList.declarations.length !== 1 || !ts.isReturnStatement(root.statements[1]) || !root.statements[1].expression) fail(root, "Block-bodied keyed list map callbacks require one computed child collection const and a final JSX return")
|
|
1797
|
+
const declaration = root.statements[0].declarationList.declarations[0]
|
|
1798
|
+
if (!ts.isIdentifier(declaration.name) || !declaration.initializer) fail(declaration, "Computed child collections must initialize one const identifier")
|
|
1799
|
+
const computed = analyzeCollectionPipeline(declaration.initializer, { fail, importedCollectionTransforms })
|
|
1800
|
+
if (!computed?.ownerField || computed.parentItem !== parameters.item) fail(declaration.initializer, `Computed child collections must start from ${parameters.item}.<field>`)
|
|
1801
|
+
const returned = root.statements[1].expression
|
|
1802
|
+
if (identifierReferenceCount(returned, declaration.name.text) !== 1) fail(declaration.name, `Computed child collection alias "${declaration.name.text}" must be used exactly once`)
|
|
1803
|
+
root = unwrapExpression(substituteClone(returned, new Map([[declaration.name.text, declaration.initializer]]), factory, context))
|
|
1804
|
+
callback = factory.updateArrowFunction(callback, callback.modifiers, callback.typeParameters, callback.parameters, callback.type, callback.equalsGreaterThanToken, root)
|
|
1805
|
+
ts.setParentRecursive(callback, false)
|
|
1806
|
+
callback.parent = value
|
|
1807
|
+
}
|
|
1808
|
+
const conditional = conditionalKeyedMapRoot(callback, root, parameters, collection, fail, new Set(setters.values()), factory, value)
|
|
1809
|
+
if (conditional) ({ callback, root, collection } = conditional)
|
|
1810
|
+
if (!ts.isJsxElement(root) && !ts.isJsxSelfClosingElement(root)) fail(callback.body, "Keyed list map callback must return one JSX element")
|
|
1811
|
+
const attributes = ts.isJsxElement(root) ? root.openingElement.attributes : root.attributes
|
|
1812
|
+
const key = attributes.properties.find(attribute => ts.isJsxAttribute(attribute) && ts.isIdentifier(attribute.name) && attribute.name.text === "key")
|
|
1813
|
+
const keyExpression = key && ts.isJsxAttribute(key) && key.initializer && ts.isJsxExpression(key.initializer) && key.initializer.expression
|
|
1814
|
+
const field = keyExpression && directProperty(keyExpression, parameters.item)
|
|
1815
|
+
const positional = Boolean(keyExpression && parameters.index && ts.isIdentifier(unwrapExpression(keyExpression)) && unwrapExpression(keyExpression).text === parameters.index)
|
|
1816
|
+
if (!field && !positional) fail(key ?? root, `Keyed list root must have key={${parameters.item}.<field>} or key={${parameters.index ?? "index"}}`)
|
|
1817
|
+
return { ...collection, static: collection.static && (!collection.localStatic || collection.selector.length > 0), callback, root, item: parameters.item, index: parameters.index, indexed: Boolean(parameters.index), keyField: positional ? null : field }
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
function nestedKeyedListParts(expression, parentItem, fail) {
|
|
1821
|
+
const value = unwrapExpression(expression)
|
|
1822
|
+
if (!ts.isCallExpression(value) || value.arguments.length !== 1 || !ts.isPropertyAccessExpression(value.expression) || value.expression.name.text !== "map") return undefined
|
|
1823
|
+
let collection = analyzeCollectionPipeline(value.expression.expression, { fail })
|
|
1824
|
+
if (!collection?.ownerField || collection.parentItem !== parentItem) return undefined
|
|
1825
|
+
let callback = value.arguments[0]
|
|
1826
|
+
const parameters = collectionParameters(callback, "Nested keyed list map", fail)
|
|
1827
|
+
let root = unwrapExpression(callback.body)
|
|
1828
|
+
const conditional = conditionalKeyedMapRoot(callback, root, parameters, collection, fail, new Set(), ts.factory, value)
|
|
1829
|
+
if (conditional) ({ callback, root, collection } = conditional)
|
|
1830
|
+
if (!ts.isJsxElement(root) && !ts.isJsxSelfClosingElement(root)) fail(callback.body, "Nested keyed list map callback must return one JSX element")
|
|
1831
|
+
const attributes = ts.isJsxElement(root) ? root.openingElement.attributes : root.attributes
|
|
1832
|
+
const key = attributes.properties.find(attribute => ts.isJsxAttribute(attribute) && ts.isIdentifier(attribute.name) && attribute.name.text === "key")
|
|
1833
|
+
const keyExpression = key && ts.isJsxAttribute(key) && key.initializer && ts.isJsxExpression(key.initializer) && key.initializer.expression
|
|
1834
|
+
const keyField = keyExpression && directProperty(keyExpression, parameters.item)
|
|
1835
|
+
const positional = Boolean(keyExpression && parameters.index && ts.isIdentifier(unwrapExpression(keyExpression)) && unwrapExpression(keyExpression).text === parameters.index)
|
|
1836
|
+
if (!keyField && !positional) fail(key ?? root, `Nested keyed list root must have key={${parameters.item}.<field>} or key={${parameters.index ?? "index"}}`)
|
|
1837
|
+
return { ...collection, callback, root, item: parameters.item, index: parameters.index, indexed: Boolean(parameters.index), keyField: positional ? null : keyField }
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
function conditionalKeyedMapRoot(callback, root, parameters, collection, fail, stateNames, factory, parent) {
|
|
1841
|
+
let condition
|
|
1842
|
+
let rendered
|
|
1843
|
+
if (ts.isBinaryExpression(root) && root.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken && (ts.isJsxElement(unwrapExpression(root.right)) || ts.isJsxSelfClosingElement(unwrapExpression(root.right)))) {
|
|
1844
|
+
condition = root.left
|
|
1845
|
+
rendered = unwrapExpression(root.right)
|
|
1846
|
+
} else if (ts.isConditionalExpression(root) && (ts.isJsxElement(unwrapExpression(root.whenTrue)) || ts.isJsxSelfClosingElement(unwrapExpression(root.whenTrue)))) {
|
|
1847
|
+
if (unwrapExpression(root.whenFalse).kind !== ts.SyntaxKind.NullKeyword) fail(root.whenFalse, "Conditional keyed map callbacks require condition ? <Element> : null")
|
|
1848
|
+
condition = root.condition
|
|
1849
|
+
rendered = unwrapExpression(root.whenTrue)
|
|
1850
|
+
} else {
|
|
1851
|
+
return undefined
|
|
1852
|
+
}
|
|
1853
|
+
if (parameters.index) fail(callback.parameters[1], "Conditional keyed map callbacks cannot use a map index because filtering changes index semantics; use an explicit filter(...).map((item, index) => ...) when a filtered index is intended")
|
|
1854
|
+
const selectorStates = new Set(collection.selectorStates)
|
|
1855
|
+
const selector = collectionExpression(condition, { parameters, fail, stateNames, selectorStates })
|
|
1856
|
+
const normalized = factory.updateArrowFunction(callback, callback.modifiers, callback.typeParameters, callback.parameters, callback.type, callback.equalsGreaterThanToken, rendered)
|
|
1857
|
+
ts.setParentRecursive(normalized, false)
|
|
1858
|
+
normalized.parent = parent
|
|
1859
|
+
return { callback: normalized, root: rendered, collection: { ...collection, selector: [...collection.selector, ["filter", selector]], selectorStates } }
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
function jsonExpression(value, factory) {
|
|
1863
|
+
return factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("JSON"), "parse"), undefined, [factory.createStringLiteral(JSON.stringify(value))])
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
function isStateBackedListComponentCall(call, component, setters) {
|
|
1867
|
+
if (component.parameters.length !== 1 || !ts.isObjectBindingPattern(component.parameters[0].name)) return false
|
|
1868
|
+
const attributes = ts.isJsxElement(call) ? call.openingElement.attributes : call.attributes
|
|
1869
|
+
const stateNames = new Set(setters.values())
|
|
1870
|
+
const mappedProps = new Set()
|
|
1871
|
+
for (const element of component.parameters[0].name.elements) {
|
|
1872
|
+
if (!ts.isIdentifier(element.name)) continue
|
|
1873
|
+
const prop = (element.propertyName ?? element.name).getText()
|
|
1874
|
+
const attribute = attributes.properties.find(entry => ts.isJsxAttribute(entry) && entry.name.getText() === prop)
|
|
1875
|
+
const value = attribute?.initializer && ts.isJsxExpression(attribute.initializer) ? unwrapExpression(attribute.initializer.expression) : undefined
|
|
1876
|
+
if (value && ts.isIdentifier(value) && stateNames.has(value.text)) mappedProps.add(element.name.text)
|
|
1877
|
+
}
|
|
1878
|
+
if (!mappedProps.size) return false
|
|
1879
|
+
const returned = ts.isBlock(component.body)
|
|
1880
|
+
? [...component.body.statements].reverse().find(ts.isReturnStatement)?.expression
|
|
1881
|
+
: component.body
|
|
1882
|
+
if (!returned || !containsJsx(returned)) return false
|
|
1883
|
+
let found = false
|
|
1884
|
+
const visit = node => {
|
|
1885
|
+
if (found || node !== returned && isFunctionLike(node)) return
|
|
1886
|
+
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "map" && ts.isIdentifier(node.expression.expression) && mappedProps.has(node.expression.expression.text)) {
|
|
1887
|
+
found = true
|
|
1888
|
+
return
|
|
1889
|
+
}
|
|
1890
|
+
ts.forEachChild(node, visit)
|
|
1891
|
+
}
|
|
1892
|
+
visit(returned)
|
|
1893
|
+
return found
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
function jsxCallHasDirectStateProp(call, setters) {
|
|
1897
|
+
const attributes = ts.isJsxElement(call) ? call.openingElement.attributes : call.attributes
|
|
1898
|
+
const stateNames = new Set(setters.values())
|
|
1899
|
+
return attributes.properties.some(attribute => {
|
|
1900
|
+
const value = ts.isJsxAttribute(attribute) && attribute.initializer && ts.isJsxExpression(attribute.initializer) ? unwrapExpression(attribute.initializer.expression) : undefined
|
|
1901
|
+
return value && ts.isIdentifier(value) && stateNames.has(value.text)
|
|
1902
|
+
})
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
function jsxSetterCallbackProps(call, setters, functions, reducers) {
|
|
1906
|
+
const attributes = ts.isJsxElement(call) ? call.openingElement.attributes : call.attributes
|
|
1907
|
+
return attributes.properties.flatMap(attribute => {
|
|
1908
|
+
if (!ts.isJsxAttribute(attribute) || !/^on[A-Z]/.test(attribute.name.text) || !attribute.initializer || !ts.isJsxExpression(attribute.initializer) || !attribute.initializer.expression) return []
|
|
1909
|
+
const value = unwrapExpression(attribute.initializer.expression)
|
|
1910
|
+
if (ts.isIdentifier(value) && setters.has(value.text)) return [attribute.name.text]
|
|
1911
|
+
const callback = ts.isArrowFunction(value) || ts.isFunctionExpression(value) ? value : ts.isIdentifier(value) ? functions.get(value.text) : undefined
|
|
1912
|
+
return callback && !nativeCaptureNames(callback, setters).size && !referencedReducerDispatches(callback.body, reducers, callback).size && referencedStateNames(callback.body, setters, callback).size ? [attribute.name.text] : []
|
|
1913
|
+
})
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
function jsxCallHasDirectReducerProp(call, reducers) {
|
|
1917
|
+
const attributes = ts.isJsxElement(call) ? call.openingElement.attributes : call.attributes
|
|
1918
|
+
return attributes.properties.some(attribute => {
|
|
1919
|
+
const value = ts.isJsxAttribute(attribute) && attribute.initializer && ts.isJsxExpression(attribute.initializer) ? unwrapExpression(attribute.initializer.expression) : undefined
|
|
1920
|
+
return value && ts.isIdentifier(value) && reducers.has(value.text)
|
|
1921
|
+
})
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
function jsxCallHasReducerCallbackProp(call, reducers) {
|
|
1925
|
+
const attributes = ts.isJsxElement(call) ? call.openingElement.attributes : call.attributes
|
|
1926
|
+
return attributes.properties.some(attribute => {
|
|
1927
|
+
const value = ts.isJsxAttribute(attribute) && attribute.initializer && ts.isJsxExpression(attribute.initializer) ? unwrapExpression(attribute.initializer.expression) : undefined
|
|
1928
|
+
return value && referencedReducerDispatches(value, reducers, value).size
|
|
1929
|
+
})
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
function runtimeImportNames(sourceFile, relative) {
|
|
1933
|
+
const names = new Set()
|
|
1934
|
+
for (const statement of sourceFile.statements) {
|
|
1935
|
+
if (!ts.isImportDeclaration(statement) || !statement.importClause || statement.importClause.isTypeOnly || !ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text.startsWith(".") !== relative || isStaticImport(statement.moduleSpecifier.text)) continue
|
|
1936
|
+
const clause = statement.importClause
|
|
1937
|
+
if (clause.name) names.add(clause.name.text)
|
|
1938
|
+
if (clause.namedBindings && ts.isNamespaceImport(clause.namedBindings)) names.add(clause.namedBindings.name.text)
|
|
1939
|
+
if (clause.namedBindings && ts.isNamedImports(clause.namedBindings)) for (const entry of clause.namedBindings.elements) if (!entry.isTypeOnly) names.add(entry.name.text)
|
|
1940
|
+
}
|
|
1941
|
+
return names
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
function insideJsxEventHandler(node, root) {
|
|
1945
|
+
for (let current = node.parent; current && current !== root.parent; current = current.parent) {
|
|
1946
|
+
if (ts.isJsxAttribute(current) && /^on[A-Z]/.test(current.name.text)) return true
|
|
1947
|
+
}
|
|
1948
|
+
return false
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
function validateKeyedList(parts, sourceFile, setters, rowStates, componentSpecializations, expandedRowSpecializations, nestedRowSpecializations, factory, prepareListCallback) {
|
|
1952
|
+
const fail = (node, message) => {
|
|
1953
|
+
throw sourceNodeError(node, sourceFile, message)
|
|
1954
|
+
}
|
|
1955
|
+
const analysis = { values: [], conditions: [], nested: [] }
|
|
1956
|
+
const root = parts.root
|
|
1957
|
+
const item = parts.item
|
|
1958
|
+
const nestedDiagnostic = "Nested keyed list collections must be a direct property of the parent item"
|
|
1959
|
+
const validateElement = node => {
|
|
1960
|
+
const tag = ts.isJsxElement(node) ? node.openingElement.tagName : node.tagName
|
|
1961
|
+
if (!ts.isIdentifier(tag) || tag.text[0] !== tag.text[0].toLowerCase()) fail(node, "Keyed list items must use intrinsic JSX elements")
|
|
1962
|
+
}
|
|
1963
|
+
const visit = node => {
|
|
1964
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "useId") fail(node, "useId() is not supported in keyed rows")
|
|
1965
|
+
if (ts.isJsxFragment(node)) fail(node, "Fragments are not supported in keyed lists")
|
|
1966
|
+
if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) validateElement(node)
|
|
1967
|
+
if (node !== root && ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "map" && containsJsx(node)) fail(node, nestedDiagnostic)
|
|
1968
|
+
if (ts.isJsxSpreadAttribute(node) && referencesIdentifier(node.expression, item)) fail(node, "Keyed list item spreads are not supported")
|
|
1969
|
+
if (ts.isJsxAttribute(node) && /^on[A-Z]/.test(node.name.text)) {
|
|
1970
|
+
return
|
|
1971
|
+
}
|
|
1972
|
+
if (ts.isJsxExpression(node) && node.expression) {
|
|
1973
|
+
const expression = unwrapExpression(node.expression)
|
|
1974
|
+
if (containsJsx(expression) && ts.isCallExpression(expression) && ts.isPropertyAccessExpression(expression.expression) && expression.expression.name.text === "map") {
|
|
1975
|
+
const nested = nestedKeyedListParts(expression, item, fail)
|
|
1976
|
+
if (!nested) fail(expression, nestedDiagnostic)
|
|
1977
|
+
if (["__proto__", "constructor", "prototype"].includes(nested.ownerField)) fail(expression, `Nested keyed list owner property "${nested.ownerField}" is not supported`)
|
|
1978
|
+
if (referenceIdentifiers(nested.callback, item).length) fail(nested.root, "Nested keyed list rows cannot capture the parent item")
|
|
1979
|
+
const specialization = componentSpecializations.get(nested.root) ?? expandedRowSpecializations.get(nested.root) ?? nestedRowSpecializations.get(`${expression.pos}:${expression.end}`)
|
|
1980
|
+
const root = specialization?.root ?? nested.root
|
|
1981
|
+
let callback = root === nested.root ? nested.callback : factory.updateArrowFunction(
|
|
1982
|
+
nested.callback,
|
|
1983
|
+
nested.callback.modifiers,
|
|
1984
|
+
nested.callback.typeParameters,
|
|
1985
|
+
nested.callback.parameters,
|
|
1986
|
+
nested.callback.type,
|
|
1987
|
+
nested.callback.equalsGreaterThanToken,
|
|
1988
|
+
root
|
|
1989
|
+
)
|
|
1990
|
+
if (callback !== nested.callback) {
|
|
1991
|
+
ts.setParentRecursive(callback, false)
|
|
1992
|
+
callback.parent = nested.callback.parent
|
|
1993
|
+
}
|
|
1994
|
+
callback = prepareListCallback(callback, root, specialization ?? { hookDeclarations: [], effects: [] })
|
|
1995
|
+
const specializedStates = [...(specialization?.rowStates ?? []), ...(specialization?.ordinaryStates ?? [])]
|
|
1996
|
+
const nestedParts = {
|
|
1997
|
+
...nested,
|
|
1998
|
+
root,
|
|
1999
|
+
callback,
|
|
2000
|
+
state: parts.state,
|
|
2001
|
+
nested: true,
|
|
2002
|
+
specializations: [specialization?.analysis?.slot, ...(specialization?.specializations ?? [])].filter(slot => slot !== undefined),
|
|
2003
|
+
rowStates: specializedStates,
|
|
2004
|
+
rowRefs: specialization?.rowRefs ?? [],
|
|
2005
|
+
analysisStateOwners: new Map([...(parts.analysisStateOwners ?? []), ...specializedStates.map(state => [state.state, state.analysisOwner])])
|
|
2006
|
+
}
|
|
2007
|
+
for (const calculation of specialization?.calculations ?? []) {
|
|
2008
|
+
ts.setParentRecursive(calculation, false)
|
|
2009
|
+
calculation.parent = callback
|
|
2010
|
+
validateListExpression(calculation, nested.item, nested.root, fail)
|
|
2011
|
+
}
|
|
2012
|
+
const nestedAnalysis = validateKeyedList(nestedParts, sourceFile, setters, specialization?.rowStates ?? [], componentSpecializations, expandedRowSpecializations, nestedRowSpecializations, factory, prepareListCallback)
|
|
2013
|
+
analysis.nested.push({ node: expression, parts: nestedParts, analysis: nestedAnalysis })
|
|
2014
|
+
return
|
|
2015
|
+
}
|
|
2016
|
+
const condition = conditionalParts(expression)
|
|
2017
|
+
if (condition && containsJsx(expression)) {
|
|
2018
|
+
if (rowStates.some(rowState => referencedStateNames(condition.condition, setters).has(rowState.state))) {
|
|
2019
|
+
visit(condition.truthy)
|
|
2020
|
+
visit(condition.falsy)
|
|
2021
|
+
return
|
|
2022
|
+
}
|
|
2023
|
+
if (!referencesIdentifier(condition.condition, item) && !(parts.index && referencesIdentifier(condition.condition, parts.index))) fail(node, "Keyed list item conditions must read the item or index")
|
|
2024
|
+
validateListExpression(condition.condition, item, node, fail, parts.index)
|
|
2025
|
+
analysis.conditions.push({ node: node.expression, value: { ...condition, item, index: parts.index } })
|
|
2026
|
+
visit(condition.truthy)
|
|
2027
|
+
visit(condition.falsy)
|
|
2028
|
+
return
|
|
2029
|
+
}
|
|
2030
|
+
const field = directProperty(expression, item)
|
|
2031
|
+
const isRootKey = ts.isJsxAttribute(node.parent) && node.parent.name.text === "key"
|
|
2032
|
+
if (field && ["__proto__", "constructor", "prototype"].includes(field)) fail(node, `Keyed list item property "${field}" is not supported`)
|
|
2033
|
+
if (field && ts.isJsxAttribute(node.parent) && ["ref", "dangerouslysetinnerhtml"].includes(node.parent.name.text.toLowerCase())) fail(node, `Keyed list item ${node.parent.name.text} is not supported`)
|
|
2034
|
+
if (isRootKey) return
|
|
2035
|
+
if (field) {
|
|
2036
|
+
analysis.values.push({ node: node.expression, value: { field } })
|
|
2037
|
+
return
|
|
2038
|
+
}
|
|
2039
|
+
if (referencesIdentifier(expression, item) || parts.index && referencesIdentifier(expression, parts.index)) {
|
|
2040
|
+
const states = referencedStateNames(expression, setters)
|
|
2041
|
+
for (const rowState of rowStates) states.delete(rowState.state)
|
|
2042
|
+
if (parts.nested && states.size) fail(node, "Nested keyed list item expressions cannot read parent state")
|
|
2043
|
+
validateListExpression(expression, item, node, fail, parts.index, states)
|
|
2044
|
+
if (ts.isJsxAttribute(node.parent) && ["ref", "dangerouslysetinnerhtml"].includes(node.parent.name.text.toLowerCase())) fail(node, `Keyed list item ${node.parent.name.text} is not supported`)
|
|
2045
|
+
analysis.values.push({ node: node.expression, value: { item, index: parts.index, states } })
|
|
2046
|
+
return
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
ts.forEachChild(node, visit)
|
|
2050
|
+
}
|
|
2051
|
+
visit(root)
|
|
2052
|
+
return analysis
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
function directConstObjectLiteral(expression, call) {
|
|
2056
|
+
expression = unwrapExpression(expression)
|
|
2057
|
+
if (ts.isObjectLiteralExpression(expression)) return expression
|
|
2058
|
+
if (!ts.isIdentifier(expression)) return
|
|
2059
|
+
const scopes = []
|
|
2060
|
+
for (let current = call.parent; current; current = current.parent) {
|
|
2061
|
+
if (isFunctionLike(current) && ts.isBlock(current.body)) scopes.push(current.body)
|
|
2062
|
+
if (ts.isSourceFile(current)) scopes.push(current)
|
|
2063
|
+
}
|
|
2064
|
+
for (const scope of scopes) {
|
|
2065
|
+
const declarations = []
|
|
2066
|
+
for (const statement of scope.statements) {
|
|
2067
|
+
if (!ts.isVariableStatement(statement)) continue
|
|
2068
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
2069
|
+
if (ts.isIdentifier(declaration.name) && declaration.name.text === expression.text) declarations.push({ declaration, constant: (statement.declarationList.flags & ts.NodeFlags.Const) !== 0 })
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
if (!declarations.length) continue
|
|
2073
|
+
if (declarations.length !== 1 || !declarations[0].constant || !declarations[0].declaration.initializer || declarations[0].declaration.end >= call.pos) return
|
|
2074
|
+
const initializer = unwrapExpression(declarations[0].declaration.initializer)
|
|
2075
|
+
if (ts.isObjectLiteralExpression(initializer)) return initializer
|
|
2076
|
+
return
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
function specializedSpreadEntries(expression, call, fail, label, seen = new Set()) {
|
|
2081
|
+
const object = directConstObjectLiteral(expression, call)
|
|
2082
|
+
if (!object) fail(expression, `${label} component prop spreads must use an inline object literal or one direct const object literal declared in the calling component`)
|
|
2083
|
+
if (seen.has(object)) fail(expression, `${label} component prop spreads cannot be circular`)
|
|
2084
|
+
seen.add(object)
|
|
2085
|
+
const entries = []
|
|
2086
|
+
for (const property of object.properties) {
|
|
2087
|
+
if (ts.isSpreadAssignment(property)) {
|
|
2088
|
+
entries.push(...specializedSpreadEntries(property.expression, call, fail, label, seen))
|
|
2089
|
+
continue
|
|
2090
|
+
}
|
|
2091
|
+
if (ts.isShorthandPropertyAssignment(property)) {
|
|
2092
|
+
entries.push([property.name.text, property.name, property])
|
|
2093
|
+
continue
|
|
2094
|
+
}
|
|
2095
|
+
if (!ts.isPropertyAssignment(property) || ts.isComputedPropertyName(property.name) || !ts.isIdentifier(property.name) && !ts.isStringLiteral(property.name) && !ts.isNumericLiteral(property.name)) {
|
|
2096
|
+
fail(property, `${label} component prop spreads must contain only direct properties`)
|
|
2097
|
+
}
|
|
2098
|
+
entries.push([property.name.text, property.initializer, property])
|
|
2099
|
+
}
|
|
2100
|
+
seen.delete(object)
|
|
2101
|
+
return entries
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
function specializedCallChildren(call, factory) {
|
|
2105
|
+
if (!ts.isJsxElement(call)) return []
|
|
2106
|
+
return call.children.flatMap(child => {
|
|
2107
|
+
if (ts.isJsxText(child)) {
|
|
2108
|
+
const lines = child.text.split(/\r\n|\n|\r/)
|
|
2109
|
+
const text = lines.length === 1
|
|
2110
|
+
? child.text
|
|
2111
|
+
: lines.map((line, index) => {
|
|
2112
|
+
let text = line.replace(/\t/g, " ")
|
|
2113
|
+
if (index) text = text.trimStart()
|
|
2114
|
+
if (index < lines.length - 1) text = text.trimEnd()
|
|
2115
|
+
return text
|
|
2116
|
+
}).filter(Boolean).join(" ")
|
|
2117
|
+
return text ? [factory.createStringLiteral(text)] : []
|
|
2118
|
+
}
|
|
2119
|
+
if (ts.isJsxExpression(child)) return child.expression ? [child.expression] : []
|
|
2120
|
+
return [child]
|
|
2121
|
+
})
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
function flattenForwardedComponentChildren(root, factory, context) {
|
|
2125
|
+
const forwarded = expression => {
|
|
2126
|
+
const value = unwrapExpression(expression)
|
|
2127
|
+
if (ts.isJsxElement(value) || ts.isJsxSelfClosingElement(value)) return [value]
|
|
2128
|
+
if (ts.isJsxFragment(value)) return [...value.children]
|
|
2129
|
+
if (ts.isArrayLiteralExpression(value) && !value.elements.some(ts.isSpreadElement)) {
|
|
2130
|
+
return value.elements.flatMap(element => {
|
|
2131
|
+
if (ts.isJsxFragment(element)) return [...element.children]
|
|
2132
|
+
if (ts.isJsxElement(element) || ts.isJsxSelfClosingElement(element)) return [element]
|
|
2133
|
+
return [factory.createJsxExpression(undefined, element)]
|
|
2134
|
+
})
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
const visit = node => {
|
|
2138
|
+
if (ts.isJsxElement(node)) {
|
|
2139
|
+
const children = node.children.flatMap(child => {
|
|
2140
|
+
const values = ts.isJsxExpression(child) && child.expression ? forwarded(child.expression) : undefined
|
|
2141
|
+
return (values ?? [child]).map(entry => ts.visitNode(entry, visit))
|
|
2142
|
+
})
|
|
2143
|
+
return factory.updateJsxElement(node, ts.visitNode(node.openingElement, visit), children, ts.visitNode(node.closingElement, visit))
|
|
2144
|
+
}
|
|
2145
|
+
return ts.visitEachChild(node, visit, context)
|
|
2146
|
+
}
|
|
2147
|
+
return ts.visitNode(root, visit)
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
function expandSpecializedRest(root, returned, component, rest, entries, factory, context, fail, label) {
|
|
2151
|
+
const sourceRoot = unwrapExpression(returned)
|
|
2152
|
+
const sourceTag = jsxTagName(sourceRoot)
|
|
2153
|
+
if (!sourceTag || !ts.isIdentifier(sourceTag) || sourceTag.text[0] !== sourceTag.text[0].toLowerCase()) {
|
|
2154
|
+
fail(returned, `${label} component rest props must be forwarded exactly once to the direct intrinsic root`)
|
|
2155
|
+
}
|
|
2156
|
+
const sourceAttributes = ts.isJsxElement(sourceRoot) ? sourceRoot.openingElement.attributes : sourceRoot.attributes
|
|
2157
|
+
const spreads = sourceAttributes.properties.filter(attribute => ts.isJsxSpreadAttribute(attribute) && ts.isIdentifier(unwrapExpression(attribute.expression)) && unwrapExpression(attribute.expression).text === rest.name)
|
|
2158
|
+
const references = referenceIdentifiers(component.body, rest.name)
|
|
2159
|
+
if (spreads.length !== 1 || references.length !== 1 || unwrapExpression(spreads[0].expression) !== references[0]) {
|
|
2160
|
+
fail(rest.node, `${label} component rest props must be forwarded exactly once to the direct intrinsic root`)
|
|
2161
|
+
}
|
|
2162
|
+
for (const [name] of entries) {
|
|
2163
|
+
if (["__proto__", "constructor", "prototype"].includes(name)) fail(rest.node, `${label} component rest prop ${JSON.stringify(name)} is not supported`)
|
|
2164
|
+
if (name === "children") fail(rest.node, `${label} component rest props cannot forward children; destructure children explicitly`)
|
|
2165
|
+
}
|
|
2166
|
+
const attributes = ts.isJsxElement(root) ? root.openingElement.attributes : root.attributes
|
|
2167
|
+
const expanded = attributes.properties.flatMap(attribute => {
|
|
2168
|
+
if (!ts.isJsxSpreadAttribute(attribute) || !ts.isIdentifier(unwrapExpression(attribute.expression)) || unwrapExpression(attribute.expression).text !== rest.name) return [attribute]
|
|
2169
|
+
return entries.map(([name, value]) => factory.createJsxAttribute(factory.createIdentifier(name), factory.createJsxExpression(undefined, cloneAst(value, factory, context))))
|
|
2170
|
+
})
|
|
2171
|
+
const last = new Map()
|
|
2172
|
+
expanded.forEach((attribute, index) => {
|
|
2173
|
+
if (ts.isJsxAttribute(attribute)) last.set(attribute.name.text, index)
|
|
2174
|
+
})
|
|
2175
|
+
const properties = expanded.filter((attribute, index) => !ts.isJsxAttribute(attribute) || last.get(attribute.name.text) === index)
|
|
2176
|
+
if (ts.isJsxSelfClosingElement(root)) return factory.updateJsxSelfClosingElement(root, root.tagName, root.typeArguments, factory.updateJsxAttributes(attributes, properties))
|
|
2177
|
+
const opening = factory.updateJsxOpeningElement(root.openingElement, root.openingElement.tagName, root.openingElement.typeArguments, factory.updateJsxAttributes(attributes, properties))
|
|
2178
|
+
return factory.updateJsxElement(root, opening, root.children, root.closingElement)
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
function specializeComponentCall(call, component, sourceFile, factory, context, fail, label = "Keyed list", allowComponentRoot = false, ordinaryHooks = false, ordinaryStateNames = new Set()) {
|
|
2182
|
+
if (component.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword) || component.asteriskToken) fail(component, `${label} components must be synchronous`)
|
|
2183
|
+
if (component.parameters.length !== 1 || !ts.isObjectBindingPattern(component.parameters[0].name)) fail(component, `${label} components must use one destructured props parameter`)
|
|
2184
|
+
const callAttributes = ts.isJsxElement(call) ? call.openingElement.attributes : call.attributes
|
|
2185
|
+
const props = new Map()
|
|
2186
|
+
const directProps = new Set()
|
|
2187
|
+
let key
|
|
2188
|
+
for (const attribute of callAttributes.properties) {
|
|
2189
|
+
if (ts.isJsxSpreadAttribute(attribute)) {
|
|
2190
|
+
for (const [name, value, property] of specializedSpreadEntries(attribute.expression, call, fail, label)) {
|
|
2191
|
+
if (["__proto__", "constructor", "prototype"].includes(name)) fail(property, `${label} component prop spread property ${JSON.stringify(name)} is not supported`)
|
|
2192
|
+
if (name === "key") fail(property, `${label} component prop spreads cannot declare key`)
|
|
2193
|
+
props.set(name, value)
|
|
2194
|
+
}
|
|
2195
|
+
continue
|
|
2196
|
+
}
|
|
2197
|
+
const name = attribute.name.text
|
|
2198
|
+
if (directProps.has(name) || name === "key" && key) fail(attribute, `Duplicate ${label.toLowerCase()} component prop "${name}"`)
|
|
2199
|
+
const value = !attribute.initializer
|
|
2200
|
+
? factory.createTrue()
|
|
2201
|
+
: ts.isStringLiteral(attribute.initializer)
|
|
2202
|
+
? factory.createStringLiteral(attribute.initializer.text)
|
|
2203
|
+
: ts.isJsxExpression(attribute.initializer) && attribute.initializer.expression
|
|
2204
|
+
? attribute.initializer.expression
|
|
2205
|
+
: factory.createIdentifier("undefined")
|
|
2206
|
+
if (name === "key") key = attribute
|
|
2207
|
+
else {
|
|
2208
|
+
props.set(name, value)
|
|
2209
|
+
directProps.add(name)
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
const children = specializedCallChildren(call, factory)
|
|
2213
|
+
if (children.length) {
|
|
2214
|
+
if (directProps.has("children")) fail(call, `Duplicate ${label.toLowerCase()} component prop "children"`)
|
|
2215
|
+
props.set("children", children.length === 1 ? children[0] : factory.createArrayLiteralExpression(children))
|
|
2216
|
+
}
|
|
2217
|
+
const substitutions = new Map()
|
|
2218
|
+
const acceptedProps = new Set()
|
|
2219
|
+
let rest
|
|
2220
|
+
const elements = component.parameters[0].name.elements
|
|
2221
|
+
for (const [index, element] of elements.entries()) {
|
|
2222
|
+
if (element.dotDotDotToken) {
|
|
2223
|
+
if (!ts.isIdentifier(element.name) || element.propertyName || element.initializer || index !== elements.length - 1) fail(element, `${label} component rest props must be one final identifier binding`)
|
|
2224
|
+
rest = { name: element.name.text, node: element }
|
|
2225
|
+
continue
|
|
2226
|
+
}
|
|
2227
|
+
if (!ts.isIdentifier(element.name)) fail(element, `${label} component props cannot use nested destructuring`)
|
|
2228
|
+
if (element.initializer && !isSerializableStateLiteral(element.initializer)) fail(element.initializer, `${label} component prop defaults must be directly serializable primitive, plain-object, or array literals`)
|
|
2229
|
+
const prop = (element.propertyName ?? element.name).text
|
|
2230
|
+
acceptedProps.add(prop)
|
|
2231
|
+
substitutions.set(element.name.text, props.has(prop) ? props.get(prop) : element.initializer ?? factory.createIdentifier("undefined"))
|
|
2232
|
+
}
|
|
2233
|
+
const restEntries = [...props].filter(([prop]) => !acceptedProps.has(prop))
|
|
2234
|
+
if (!rest) for (const [prop] of restEntries) fail(call, `Unknown ${label.toLowerCase()} component prop "${prop}"`)
|
|
2235
|
+
const propAnalysis = elements.map(element => ({
|
|
2236
|
+
name: (element.propertyName ?? element.name).getText(),
|
|
2237
|
+
local: element.name.getText(),
|
|
2238
|
+
provided: element.dotDotDotToken ? restEntries.length > 0 : props.has((element.propertyName ?? element.name).text),
|
|
2239
|
+
...(element.dotDotDotToken ? { rest: true } : {}),
|
|
2240
|
+
...(element.initializer ? { hasDefault: true, defaultApplied: !props.has((element.propertyName ?? element.name).text) } : {})
|
|
2241
|
+
}))
|
|
2242
|
+
|
|
2243
|
+
let returned
|
|
2244
|
+
const calculations = []
|
|
2245
|
+
const effectCalls = []
|
|
2246
|
+
const hookDeclarations = []
|
|
2247
|
+
const rowStates = []
|
|
2248
|
+
const rowRefs = []
|
|
2249
|
+
const ordinaryStates = []
|
|
2250
|
+
const ordinaryRefs = []
|
|
2251
|
+
const ordinaryIds = []
|
|
2252
|
+
if (!ts.isBlock(component.body)) {
|
|
2253
|
+
returned = component.body
|
|
2254
|
+
} else {
|
|
2255
|
+
const statements = [...component.body.statements]
|
|
2256
|
+
const last = statements.pop()
|
|
2257
|
+
if (!last || !ts.isReturnStatement(last) || !last.expression) fail(component.body, `${label} component must end with one JSX return`)
|
|
2258
|
+
for (const statement of statements) {
|
|
2259
|
+
if (ts.isExpressionStatement(statement) && ts.isCallExpression(statement.expression) && ts.isIdentifier(statement.expression.expression) && statement.expression.expression.text === "useEffect") {
|
|
2260
|
+
effectCalls.push(statement.expression)
|
|
2261
|
+
continue
|
|
2262
|
+
}
|
|
2263
|
+
if (!ts.isVariableStatement(statement) || (statement.declarationList.flags & ts.NodeFlags.Const) === 0 || statement.declarationList.declarations.length !== 1) fail(statement, `${label} component locals must be single const declarations`)
|
|
2264
|
+
const declaration = statement.declarationList.declarations[0]
|
|
2265
|
+
if (declaration.initializer && ts.isCallExpression(declaration.initializer) && ts.isIdentifier(declaration.initializer.expression) && declaration.initializer.expression.text === "useState") {
|
|
2266
|
+
const hookLabel = ordinaryHooks ? "Setter-callback component" : "Keyed row"
|
|
2267
|
+
const initialArgument = declaration.initializer.arguments[0]
|
|
2268
|
+
const propReceiver = ordinaryHooks && initialArgument && ts.isCallExpression(initialArgument) && initialArgument.arguments.length === 0 && !initialArgument.questionDotToken && ts.isPropertyAccessExpression(initialArgument.expression) && !initialArgument.expression.questionDotToken && initialArgument.expression.name.text === "toString" && ts.isIdentifier(initialArgument.expression.expression) ? initialArgument.expression.expression : undefined
|
|
2269
|
+
const substitutedProp = propReceiver ? substitutions.get(propReceiver.text) : undefined
|
|
2270
|
+
const propStringInitializer = substitutedProp && ts.isIdentifier(unwrapExpression(substitutedProp)) && ordinaryStateNames.has(unwrapExpression(substitutedProp).text)
|
|
2271
|
+
if (declaration.initializer.arguments.length !== 1 || !isSerializableStateLiteral(initialArgument) && !propStringInitializer) throw sourceNodeError(declaration.initializer, component.getSourceFile(), `${hookLabel} useState() must use one directly serializable primitive, plain object, or array initial value${ordinaryHooks ? " or direct primitive state prop.toString()" : ""}; other dynamic initializers are not supported`)
|
|
2272
|
+
if (!ts.isArrayBindingPattern(declaration.name) || declaration.name.elements.length !== 2 || declaration.name.elements.some(element => !element || !ts.isBindingElement(element) || !ts.isIdentifier(element.name) || element.initializer || element.dotDotDotToken)) throw sourceNodeError(declaration.name, component.getSourceFile(), `${hookLabel} useState() must use [state, setter] identifier destructuring`)
|
|
2273
|
+
const suffix = `${Math.max(0, call.pos)}_${ordinaryHooks ? ordinaryStates.length : rowStates.length}`
|
|
2274
|
+
const state = ordinaryHooks ? `__kComponentState${suffix}` : `__kRowState${suffix}`
|
|
2275
|
+
const setter = ordinaryHooks ? `__kComponentSetter${suffix}` : `__kRowSetter${suffix}`
|
|
2276
|
+
substitutions.set(declaration.name.elements[0].name.text, factory.createIdentifier(state))
|
|
2277
|
+
substitutions.set(declaration.name.elements[1].name.text, factory.createIdentifier(setter))
|
|
2278
|
+
const binding = factory.createArrayBindingPattern([
|
|
2279
|
+
factory.createBindingElement(undefined, undefined, factory.createIdentifier(state)),
|
|
2280
|
+
factory.createBindingElement(undefined, undefined, factory.createIdentifier(setter))
|
|
2281
|
+
])
|
|
2282
|
+
const initialValue = propStringInitializer ? substituteClone(initialArgument, substitutions, factory, context) : cloneAst(initialArgument, factory, context)
|
|
2283
|
+
synthesizeTree(initialValue)
|
|
2284
|
+
const initializer = factory.createCallExpression(factory.createIdentifier(ordinaryHooks ? "__kComponentUseState" : "__kRowUseState"), undefined, [initialValue])
|
|
2285
|
+
hookDeclarations.push(factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(binding, undefined, undefined, initializer)], ts.NodeFlags.Const)))
|
|
2286
|
+
if (ordinaryHooks) ordinaryStates.push({ state, setter, source: declaration })
|
|
2287
|
+
else rowStates.push({ state, setter, source: declaration })
|
|
2288
|
+
continue
|
|
2289
|
+
}
|
|
2290
|
+
if (declaration.initializer && ts.isCallExpression(declaration.initializer) && ts.isIdentifier(declaration.initializer.expression) && declaration.initializer.expression.text === "useRef") {
|
|
2291
|
+
const hookLabel = ordinaryHooks ? "Setter-callback component" : "Keyed row"
|
|
2292
|
+
if (declaration.initializer.arguments.length !== 1 || declaration.initializer.arguments[0].kind !== ts.SyntaxKind.NullKeyword) throw sourceNodeError(declaration.initializer, component.getSourceFile(), `${hookLabel} useRef() must use the direct initial value null`)
|
|
2293
|
+
if (!ts.isIdentifier(declaration.name)) throw sourceNodeError(declaration.name, component.getSourceFile(), `${hookLabel} useRef() must be assigned to one identifier`)
|
|
2294
|
+
const refs = ordinaryHooks ? ordinaryRefs : rowRefs
|
|
2295
|
+
const name = `${ordinaryHooks ? "__kComponentRef" : "__kRowRef"}${Math.max(0, call.pos)}_${refs.length}`
|
|
2296
|
+
substitutions.set(declaration.name.text, factory.createIdentifier(name))
|
|
2297
|
+
const initializer = factory.createCallExpression(factory.createIdentifier(ordinaryHooks ? "__kComponentUseRef" : "__kRowUseRef"), declaration.initializer.typeArguments?.map(type => cloneAst(type, factory, context)), [factory.createNull()])
|
|
2298
|
+
hookDeclarations.push(factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(factory.createIdentifier(name), undefined, undefined, initializer)], ts.NodeFlags.Const)))
|
|
2299
|
+
refs.push({ name, source: declaration })
|
|
2300
|
+
continue
|
|
2301
|
+
}
|
|
2302
|
+
if (declaration.initializer && ts.isCallExpression(declaration.initializer) && ts.isIdentifier(declaration.initializer.expression) && declaration.initializer.expression.text === "useId") {
|
|
2303
|
+
if (!ordinaryHooks) throw sourceNodeError(declaration.initializer, component.getSourceFile(), "useId() is not supported in keyed row components")
|
|
2304
|
+
if (declaration.initializer.arguments.length || !ts.isIdentifier(declaration.name)) throw sourceNodeError(declaration.initializer, component.getSourceFile(), "Setter-callback component useId() must initialize one top-level const identifier without arguments")
|
|
2305
|
+
const name = `__kComponentId${Math.max(0, call.pos)}_${hookDeclarations.length}`
|
|
2306
|
+
substitutions.set(declaration.name.text, factory.createIdentifier(name))
|
|
2307
|
+
const initializer = factory.createCallExpression(factory.createIdentifier("__kComponentUseId"), undefined, [])
|
|
2308
|
+
hookDeclarations.push(factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(factory.createIdentifier(name), undefined, undefined, initializer)], ts.NodeFlags.Const)))
|
|
2309
|
+
ordinaryIds.push({ name, source: declaration })
|
|
2310
|
+
continue
|
|
2311
|
+
}
|
|
2312
|
+
if (!ts.isIdentifier(declaration.name) || !declaration.initializer) fail(declaration, `${label} component locals must be initialized identifiers`)
|
|
2313
|
+
const calculation = substituteClone(declaration.initializer, substitutions, factory, context)
|
|
2314
|
+
calculations.push({ name: declaration.name.text, expression: calculation })
|
|
2315
|
+
substitutions.set(declaration.name.text, calculation)
|
|
2316
|
+
}
|
|
2317
|
+
returned = last.expression
|
|
2318
|
+
}
|
|
2319
|
+
let unsupportedHook
|
|
2320
|
+
const findUnsupportedHook = node => {
|
|
2321
|
+
if (unsupportedHook) return
|
|
2322
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && ["useState", "useRef", "useId"].includes(node.expression.text)) unsupportedHook = node
|
|
2323
|
+
ts.forEachChild(node, findUnsupportedHook)
|
|
2324
|
+
}
|
|
2325
|
+
findUnsupportedHook(returned)
|
|
2326
|
+
for (const calculation of calculations) findUnsupportedHook(calculation.expression)
|
|
2327
|
+
if (unsupportedHook) throw sourceNodeError(unsupportedHook, component.getSourceFile(), `${ordinaryHooks ? "Setter-callback component" : "Keyed row"} ${unsupportedHook.expression.text}() must be one top-level const declaration`)
|
|
2328
|
+
let root = unwrapExpression(flattenForwardedComponentChildren(substituteClone(returned, substitutions, factory, context), factory, context))
|
|
2329
|
+
if (rest) root = expandSpecializedRest(root, returned, component, rest, restEntries, factory, context, fail, label)
|
|
2330
|
+
if (!ts.isJsxElement(root) && !ts.isJsxSelfClosingElement(root)) fail(returned, `${label} component must return one JSX element`)
|
|
2331
|
+
const tag = jsxTagName(root)
|
|
2332
|
+
if (!ts.isIdentifier(tag) || !allowComponentRoot && tag.text[0] !== tag.text[0].toLowerCase()) fail(returned, `${label} component must directly return an intrinsic JSX element`)
|
|
2333
|
+
const rootAttributes = ts.isJsxElement(root) ? root.openingElement.attributes : root.attributes
|
|
2334
|
+
if (rootAttributes.properties.some(attribute => ts.isJsxAttribute(attribute) && attribute.name.text === "key")) fail(root, `${label} component intrinsic root cannot declare key`)
|
|
2335
|
+
if (key) root = addJsxAttribute(root, cloneAst(key, factory, context), factory)
|
|
2336
|
+
ts.setParentRecursive(root, false)
|
|
2337
|
+
root.parent = call.parent
|
|
2338
|
+
const effects = effectCalls.map(source => ({ source, call: substituteClone(source, substitutions, factory, context) }))
|
|
2339
|
+
return {
|
|
2340
|
+
root,
|
|
2341
|
+
calculations: calculations
|
|
2342
|
+
.filter(calculation => label !== "Reducer-dispatch" || !isFunctionLike(calculation.expression) || !isEventOnlyComponentLocal(returned, calculation.name))
|
|
2343
|
+
.map(calculation => calculation.expression),
|
|
2344
|
+
effects,
|
|
2345
|
+
hookDeclarations,
|
|
2346
|
+
rowStates,
|
|
2347
|
+
rowRefs,
|
|
2348
|
+
ordinaryStates,
|
|
2349
|
+
ordinaryRefs,
|
|
2350
|
+
ordinaryIds,
|
|
2351
|
+
propExpressions: props,
|
|
2352
|
+
props: propAnalysis,
|
|
2353
|
+
usesComponentId: ordinaryIds.length > 0
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
function isSerializableStateLiteral(node) {
|
|
2358
|
+
const value = unwrapExpression(node)
|
|
2359
|
+
if (isPrimitiveDefaultLiteral(value)) return true
|
|
2360
|
+
if (ts.isArrayLiteralExpression(value)) return value.elements.every(element => !ts.isSpreadElement(element) && !ts.isOmittedExpression(element) && isSerializableStateLiteral(element))
|
|
2361
|
+
if (!ts.isObjectLiteralExpression(value)) return false
|
|
2362
|
+
return value.properties.every(property => ts.isPropertyAssignment(property) && !ts.isComputedPropertyName(property.name) && property.name.text !== "__proto__" && isSerializableStateLiteral(property.initializer))
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
function synthesizeSerializableStateLiteral(node, factory) {
|
|
2366
|
+
node = unwrapExpression(node)
|
|
2367
|
+
if (ts.isStringLiteral(node)) return factory.createStringLiteral(node.text)
|
|
2368
|
+
if (ts.isNumericLiteral(node)) return factory.createNumericLiteral(node.text)
|
|
2369
|
+
if (node.kind === ts.SyntaxKind.TrueKeyword) return factory.createTrue()
|
|
2370
|
+
if (node.kind === ts.SyntaxKind.FalseKeyword) return factory.createFalse()
|
|
2371
|
+
if (node.kind === ts.SyntaxKind.NullKeyword) return factory.createNull()
|
|
2372
|
+
if (ts.isPrefixUnaryExpression(node)) return factory.createPrefixUnaryExpression(node.operator, synthesizeSerializableStateLiteral(node.operand, factory))
|
|
2373
|
+
if (ts.isArrayLiteralExpression(node)) return factory.createArrayLiteralExpression(node.elements.map(element => synthesizeSerializableStateLiteral(element, factory)))
|
|
2374
|
+
return factory.createObjectLiteralExpression(node.properties.map(property => {
|
|
2375
|
+
const name = ts.isIdentifier(property.name) ? factory.createIdentifier(property.name.text) : ts.isNumericLiteral(property.name) ? factory.createNumericLiteral(property.name.text) : factory.createStringLiteral(property.name.text)
|
|
2376
|
+
return factory.createPropertyAssignment(name, synthesizeSerializableStateLiteral(property.initializer, factory))
|
|
2377
|
+
}))
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2380
|
+
function isPrimitiveDefaultLiteral(node) {
|
|
2381
|
+
return ts.isStringLiteral(node) || ts.isNumericLiteral(node) ||
|
|
2382
|
+
(ts.isPrefixUnaryExpression(node) && (node.operator === ts.SyntaxKind.PlusToken || node.operator === ts.SyntaxKind.MinusToken) && ts.isNumericLiteral(node.operand)) ||
|
|
2383
|
+
node.kind === ts.SyntaxKind.TrueKeyword || node.kind === ts.SyntaxKind.FalseKeyword || node.kind === ts.SyntaxKind.NullKeyword
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
function isEventOnlyComponentLocal(root, name) {
|
|
2387
|
+
let found = false
|
|
2388
|
+
let eventOnly = true
|
|
2389
|
+
const visit = node => {
|
|
2390
|
+
if (ts.isIdentifier(node) && node.text === name && isReferenceIdentifier(node)) {
|
|
2391
|
+
found = true
|
|
2392
|
+
let parent = node.parent
|
|
2393
|
+
while (parent && parent !== root) {
|
|
2394
|
+
if (ts.isJsxAttribute(parent)) {
|
|
2395
|
+
if (!/^on[A-Z]/.test(parent.name.text)) eventOnly = false
|
|
2396
|
+
return
|
|
2397
|
+
}
|
|
2398
|
+
parent = parent.parent
|
|
2399
|
+
}
|
|
2400
|
+
eventOnly = false
|
|
2401
|
+
return
|
|
2402
|
+
}
|
|
2403
|
+
ts.forEachChild(node, visit)
|
|
2404
|
+
}
|
|
2405
|
+
visit(root)
|
|
2406
|
+
return found && eventOnly
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2409
|
+
function substituteClone(root, substitutions, factory, context) {
|
|
2410
|
+
const visit = (node, shadowed = new Set()) => {
|
|
2411
|
+
if (ts.isTypeNode(node)) return cloneAst(node, factory, context)
|
|
2412
|
+
if (ts.isShorthandPropertyAssignment(node) && substitutions.has(node.name.text) && !shadowed.has(node.name.text)) {
|
|
2413
|
+
return factory.createPropertyAssignment(cloneAst(node.name, factory, context), cloneAst(substitutions.get(node.name.text), factory, context))
|
|
2414
|
+
}
|
|
2415
|
+
if (ts.isIdentifier(node) && substitutions.has(node.text) && !shadowed.has(node.text) && isReferenceIdentifier(node) && !isJsxSyntaxIdentifier(node)) {
|
|
2416
|
+
return cloneAst(substitutions.get(node.text), factory, context)
|
|
2417
|
+
}
|
|
2418
|
+
const nextShadowed = isFunctionLike(node)
|
|
2419
|
+
? new Set([...shadowed, ...node.parameters.flatMap(parameter => bindingNames(parameter.name))])
|
|
2420
|
+
: shadowed
|
|
2421
|
+
const clone = factory.cloneNode(node)
|
|
2422
|
+
ts.setTextRange(clone, node)
|
|
2423
|
+
ts.setOriginalNode(clone, node)
|
|
2424
|
+
return ts.visitEachChild(clone, child => visit(child, nextShadowed), context)
|
|
2425
|
+
}
|
|
2426
|
+
return visit(root)
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2429
|
+
function replaceSpecializedCalls(root, replacements, context) {
|
|
2430
|
+
const visit = node => replacements.get(node) ?? ts.visitEachChild(node, visit, context)
|
|
2431
|
+
return ts.visitNode(root, visit)
|
|
2432
|
+
}
|
|
2433
|
+
|
|
2434
|
+
function cloneAst(root, factory, context) {
|
|
2435
|
+
const visit = node => {
|
|
2436
|
+
const clone = factory.cloneNode(node)
|
|
2437
|
+
ts.setTextRange(clone, node)
|
|
2438
|
+
ts.setOriginalNode(clone, node)
|
|
2439
|
+
return ts.visitEachChild(clone, visit, context)
|
|
2440
|
+
}
|
|
2441
|
+
return visit(root)
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
function synthesizeTree(root) {
|
|
2445
|
+
const visit = node => {
|
|
2446
|
+
ts.setTextRange(node, { pos: -1, end: -1 })
|
|
2447
|
+
ts.setOriginalNode(node, undefined)
|
|
2448
|
+
ts.forEachChild(node, visit)
|
|
2449
|
+
}
|
|
2450
|
+
visit(root)
|
|
2451
|
+
return root
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
function addJsxAttribute(root, attribute, factory) {
|
|
2455
|
+
if (ts.isJsxSelfClosingElement(root)) {
|
|
2456
|
+
return factory.updateJsxSelfClosingElement(root, root.tagName, root.typeArguments, factory.updateJsxAttributes(root.attributes, [attribute, ...root.attributes.properties]))
|
|
2457
|
+
}
|
|
2458
|
+
const opening = factory.updateJsxOpeningElement(root.openingElement, root.openingElement.tagName, root.openingElement.typeArguments, factory.updateJsxAttributes(root.openingElement.attributes, [attribute, ...root.openingElement.attributes.properties]))
|
|
2459
|
+
return factory.updateJsxElement(root, opening, root.children, root.closingElement)
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
function jsxTagName(node) {
|
|
2463
|
+
return ts.isJsxElement(node) ? node.openingElement.tagName : ts.isJsxSelfClosingElement(node) ? node.tagName : undefined
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
function isStylesheetLink(node) {
|
|
2467
|
+
const element = ts.isJsxElement(node) ? node.openingElement : node
|
|
2468
|
+
if (!ts.isIdentifier(element.tagName) || element.tagName.text.toLowerCase() !== "link") return false
|
|
2469
|
+
const attribute = element.attributes.properties.find(property => ts.isJsxAttribute(property) && property.name.getText().toLowerCase() === "rel")
|
|
2470
|
+
if (!attribute?.initializer) return false
|
|
2471
|
+
const value = ts.isStringLiteral(attribute.initializer)
|
|
2472
|
+
? attribute.initializer.text
|
|
2473
|
+
: ts.isJsxExpression(attribute.initializer) && attribute.initializer.expression && (ts.isStringLiteral(attribute.initializer.expression) || ts.isNoSubstitutionTemplateLiteral(attribute.initializer.expression))
|
|
2474
|
+
? attribute.initializer.expression.text
|
|
2475
|
+
: undefined
|
|
2476
|
+
return value?.toLowerCase().split(/\s+/).includes("stylesheet") ?? false
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
function isContextProviderValue(node, contexts) {
|
|
2480
|
+
if (node.name.text !== "value") return false
|
|
2481
|
+
const element = node.parent?.parent
|
|
2482
|
+
const tag = ts.isJsxOpeningElement(element) || ts.isJsxSelfClosingElement(element) ? element.tagName : undefined
|
|
2483
|
+
return ts.isPropertyAccessExpression(tag) && tag.name.text === "Provider" && ts.isIdentifier(tag.expression) && contexts.has(tag.expression.text)
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
function isJsxSyntaxIdentifier(node) {
|
|
2487
|
+
const parent = node.parent
|
|
2488
|
+
return (ts.isJsxOpeningElement(parent) || ts.isJsxClosingElement(parent) || ts.isJsxSelfClosingElement(parent)) && parent.tagName === node || ts.isJsxAttribute(parent) && parent.name === node
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2491
|
+
function isDestructuredParameter(identifier, fn) {
|
|
2492
|
+
return fn?.parameters.some(parameter => ts.isObjectBindingPattern(parameter.name) && parameter.name.elements.some(element => ts.isIdentifier(element.name) && element.name.text === identifier.text)) ?? false
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2495
|
+
function isExportedDeclaration(node) {
|
|
2496
|
+
const statement = ts.isVariableDeclaration(node) ? node.parent?.parent : node
|
|
2497
|
+
return statement?.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword || modifier.kind === ts.SyntaxKind.DefaultKeyword) ?? false
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2500
|
+
function jsxTagUses(root, name) {
|
|
2501
|
+
const uses = []
|
|
2502
|
+
const visit = node => {
|
|
2503
|
+
const tag = ts.isJsxElement(node) ? node.openingElement.tagName : ts.isJsxSelfClosingElement(node) ? node.tagName : undefined
|
|
2504
|
+
if (tag && ts.isIdentifier(tag) && tag.text === name) uses.push(node)
|
|
2505
|
+
ts.forEachChild(node, visit)
|
|
2506
|
+
}
|
|
2507
|
+
visit(root)
|
|
2508
|
+
return uses
|
|
2509
|
+
}
|
|
2510
|
+
|
|
2511
|
+
const pureListGlobals = new Set(["Boolean", "Infinity", "Math", "NaN", "Number", "String", "undefined"])
|
|
2512
|
+
const assignmentOperators = new Set([
|
|
2513
|
+
ts.SyntaxKind.EqualsToken, ts.SyntaxKind.PlusEqualsToken, ts.SyntaxKind.MinusEqualsToken,
|
|
2514
|
+
ts.SyntaxKind.AsteriskEqualsToken, ts.SyntaxKind.AsteriskAsteriskEqualsToken, ts.SyntaxKind.SlashEqualsToken,
|
|
2515
|
+
ts.SyntaxKind.PercentEqualsToken, ts.SyntaxKind.LessThanLessThanEqualsToken, ts.SyntaxKind.GreaterThanGreaterThanEqualsToken,
|
|
2516
|
+
ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken, ts.SyntaxKind.AmpersandEqualsToken, ts.SyntaxKind.BarEqualsToken,
|
|
2517
|
+
ts.SyntaxKind.CaretEqualsToken, ts.SyntaxKind.BarBarEqualsToken, ts.SyntaxKind.AmpersandAmpersandEqualsToken,
|
|
2518
|
+
ts.SyntaxKind.QuestionQuestionEqualsToken
|
|
2519
|
+
])
|
|
2520
|
+
|
|
2521
|
+
function validateListExpression(expression, item, source, fail, index, states = new Set()) {
|
|
2522
|
+
const visit = node => {
|
|
2523
|
+
if (ts.isTypeNode(node)) return
|
|
2524
|
+
if (ts.isElementAccessExpression(node) && referencesIdentifier(node.expression, item)) {
|
|
2525
|
+
const key = node.argumentExpression
|
|
2526
|
+
if (!ts.isStringLiteral(key) && !ts.isNumericLiteral(key)) fail(source, "Derived keyed list item computed properties require a direct string or numeric literal key")
|
|
2527
|
+
if (ts.isStringLiteral(key) && ["__proto__", "constructor", "prototype"].includes(key.text)) fail(source, `Derived keyed list item property "${key.text}" is not supported`)
|
|
2528
|
+
}
|
|
2529
|
+
if (ts.isPropertyAccessExpression(node) && ["__proto__", "constructor", "prototype"].includes(node.name.text) || ts.isElementAccessExpression(node) && ts.isStringLiteral(node.argumentExpression) && ["__proto__", "constructor", "prototype"].includes(node.argumentExpression.text)) {
|
|
2530
|
+
fail(source, "Derived keyed list item expressions cannot read __proto__, prototype, or constructor")
|
|
2531
|
+
}
|
|
2532
|
+
if (ts.isBinaryExpression(node) && assignmentOperators.has(node.operatorToken.kind) || ts.isPostfixUnaryExpression(node) || ts.isPrefixUnaryExpression(node) && [ts.SyntaxKind.PlusPlusToken, ts.SyntaxKind.MinusMinusToken].includes(node.operator)) {
|
|
2533
|
+
fail(source, "Derived keyed list item expressions must be pure; assignments and updates are not supported")
|
|
2534
|
+
}
|
|
2535
|
+
if (ts.isDeleteExpression(node) || ts.isAwaitExpression(node) || ts.isNewExpression(node) || ts.isYieldExpression(node)) {
|
|
2536
|
+
fail(source, "Derived keyed list item expressions must be synchronous and side-effect free; delete, await, yield, and new are not supported")
|
|
2537
|
+
}
|
|
2538
|
+
if (ts.isArrowFunction(node) || ts.isFunctionExpression(node) || ts.isTaggedTemplateExpression(node)) {
|
|
2539
|
+
fail(source, "Derived keyed list item expressions cannot create or invoke arbitrary functions")
|
|
2540
|
+
}
|
|
2541
|
+
if (ts.isCallExpression(node)) {
|
|
2542
|
+
if (ts.isPropertyAccessExpression(node.expression)) {
|
|
2543
|
+
const method = node.expression.name.text
|
|
2544
|
+
if (mutatingListMethods.has(method)) fail(source, `Derived keyed list item expressions cannot call mutating method "${method}"`)
|
|
2545
|
+
const receiver = node.expression.expression
|
|
2546
|
+
const mathCall = ts.isIdentifier(receiver) && receiver.text === "Math" && pureMathMethods.has(method)
|
|
2547
|
+
if (!mathCall && !pureListMethods.has(method)) fail(source, `Derived keyed list item expressions cannot call arbitrary method "${method}"`)
|
|
2548
|
+
} else if (!ts.isIdentifier(node.expression) || !["Boolean", "Number", "String"].includes(node.expression.text)) {
|
|
2549
|
+
fail(source, "Derived keyed list item expressions cannot call arbitrary functions")
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
if (ts.isIdentifier(node) && isReferenceIdentifier(node) && !isJsxSyntaxIdentifier(node) && node.text !== item && node.text !== index && !states.has(node.text) && !pureListGlobals.has(node.text)) {
|
|
2553
|
+
fail(source, `Derived keyed list item expression identifier "${node.text}" is not allowed`)
|
|
2554
|
+
}
|
|
2555
|
+
ts.forEachChild(node, visit)
|
|
2556
|
+
}
|
|
2557
|
+
visit(expression)
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2560
|
+
function directProperty(expression, objectName) {
|
|
2561
|
+
const value = unwrapExpression(expression)
|
|
2562
|
+
if (!ts.isPropertyAccessExpression(value) || !ts.isIdentifier(value.expression)) return undefined
|
|
2563
|
+
if (objectName !== undefined && value.expression.text !== objectName) return undefined
|
|
2564
|
+
return value.name.text
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
function keyedListParentTag(node) {
|
|
2568
|
+
for (let current = node.parent; current; current = current.parent) {
|
|
2569
|
+
if (ts.isJsxElement(current)) return current.openingElement.tagName.getText().toLowerCase()
|
|
2570
|
+
}
|
|
2571
|
+
return undefined
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
function identifierReferenceCount(root, name) {
|
|
2575
|
+
return identifierReferences(root, name).length
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2578
|
+
function identifierReferences(root, name) {
|
|
2579
|
+
const references = []
|
|
2580
|
+
const visit = node => {
|
|
2581
|
+
if (ts.isIdentifier(node) && node.text === name && isReferenceIdentifier(node) && !ts.isJsxClosingElement(node.parent)) references.push(node)
|
|
2582
|
+
ts.forEachChild(node, visit)
|
|
2583
|
+
}
|
|
2584
|
+
visit(root)
|
|
2585
|
+
return references
|
|
2586
|
+
}
|
|
2587
|
+
|
|
2588
|
+
function isJsxLocalValue(expression, known) {
|
|
2589
|
+
const value = unwrapExpression(expression)
|
|
2590
|
+
if (ts.isJsxElement(value) || ts.isJsxSelfClosingElement(value) || ts.isJsxFragment(value)) return true
|
|
2591
|
+
if (ts.isIdentifier(value)) return known.has(value.text)
|
|
2592
|
+
const parts = conditionalParts(value)
|
|
2593
|
+
return Boolean(parts && (isJsxLocalValue(parts.truthy, known) || isJsxLocalValue(parts.falsy, known)))
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
function conditionalParts(expression) {
|
|
2597
|
+
const unwrap = node => ts.isParenthesizedExpression(node) ? unwrap(node.expression) : node
|
|
2598
|
+
const value = unwrap(expression)
|
|
2599
|
+
if (ts.isBinaryExpression(value) && value.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
|
|
2600
|
+
return { kind: "and", condition: value.left, truthy: unwrap(value.right), falsy: factoryNull() }
|
|
2601
|
+
}
|
|
2602
|
+
if (ts.isConditionalExpression(value)) {
|
|
2603
|
+
return { kind: "ternary", condition: value.condition, truthy: unwrap(value.whenTrue), falsy: unwrap(value.whenFalse) }
|
|
2604
|
+
}
|
|
2605
|
+
return undefined
|
|
2606
|
+
}
|
|
2607
|
+
|
|
2608
|
+
function factoryNull() {
|
|
2609
|
+
return ts.factory.createNull()
|
|
2610
|
+
}
|
|
2611
|
+
|
|
2612
|
+
function settersForNode(node, settersByFunction) {
|
|
2613
|
+
for (let current = node.parent; current; current = current.parent) {
|
|
2614
|
+
if (!ts.isFunctionDeclaration(current) && !ts.isFunctionExpression(current) && !ts.isArrowFunction(current)) continue
|
|
2615
|
+
const setters = settersByFunction.get(current)
|
|
2616
|
+
if (setters) return setters
|
|
2617
|
+
}
|
|
2618
|
+
return new Map()
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2621
|
+
function reducersForNode(node, reducersByFunction) {
|
|
2622
|
+
for (let current = node.parent; current; current = current.parent) {
|
|
2623
|
+
if (!ts.isFunctionDeclaration(current) && !ts.isFunctionExpression(current) && !ts.isArrowFunction(current)) continue
|
|
2624
|
+
const reducers = reducersByFunction.get(current)
|
|
2625
|
+
if (reducers) return reducers
|
|
2626
|
+
}
|
|
2627
|
+
return new Map()
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2630
|
+
function clientImportBindings(sourceFile, file, sourceFiles) {
|
|
2631
|
+
const bindings = new Map()
|
|
2632
|
+
for (const node of sourceFile.statements) {
|
|
2633
|
+
if (!ts.isImportDeclaration(node) || !node.importClause || node.importClause.isTypeOnly || !ts.isStringLiteral(node.moduleSpecifier) || !node.moduleSpecifier.text.startsWith(".") || isStaticImport(node.moduleSpecifier.text)) continue
|
|
2634
|
+
let target
|
|
2635
|
+
try {
|
|
2636
|
+
target = resolveSourceImport(file, node.moduleSpecifier.text, sourceFiles)
|
|
2637
|
+
} catch (error) {
|
|
2638
|
+
throw sourceNodeError(node.moduleSpecifier, sourceFile, error.message)
|
|
2639
|
+
}
|
|
2640
|
+
if (node.importClause.name) bindings.set(node.importClause.name.text, { kind: "default", local: node.importClause.name.text, target })
|
|
2641
|
+
const named = node.importClause.namedBindings
|
|
2642
|
+
if (named && ts.isNamespaceImport(named)) bindings.set(named.name.text, { kind: "namespace", local: named.name.text, target })
|
|
2643
|
+
if (named && ts.isNamedImports(named)) {
|
|
2644
|
+
for (const entry of named.elements) {
|
|
2645
|
+
if (!entry.isTypeOnly) bindings.set(entry.name.text, { kind: "named", imported: (entry.propertyName ?? entry.name).text, local: entry.name.text, target })
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
return bindings
|
|
2650
|
+
}
|
|
2651
|
+
|
|
2652
|
+
function hasFrameworkImport(sourceFile, name) {
|
|
2653
|
+
return sourceFile.statements.some(node => {
|
|
2654
|
+
if (!ts.isImportDeclaration(node) || node.importClause?.isTypeOnly || !ts.isStringLiteral(node.moduleSpecifier) || !["react", "@kudzujs/core"].includes(node.moduleSpecifier.text)) return false
|
|
2655
|
+
const bindings = node.importClause?.namedBindings
|
|
2656
|
+
return bindings && ts.isNamedImports(bindings) && bindings.elements.some(entry => !entry.isTypeOnly && entry.name.text === name && (entry.propertyName ?? entry.name).text === name)
|
|
2657
|
+
})
|
|
2658
|
+
}
|
|
2659
|
+
|
|
2660
|
+
function packageImportBindings(sourceFile) {
|
|
2661
|
+
const bindings = new Map()
|
|
2662
|
+
const rejectDynamic = node => {
|
|
2663
|
+
if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
|
|
2664
|
+
const specifier = node.arguments.length === 1 && ts.isStringLiteralLike(node.arguments[0]) ? node.arguments[0].text : null
|
|
2665
|
+
if (specifier === null) throw sourceNodeError(node, sourceFile, "Dynamic import specifiers are not supported")
|
|
2666
|
+
if (!specifier.startsWith(".")) throw sourceNodeError(node, sourceFile, `Dynamic package import ${JSON.stringify(specifier)} is not supported`)
|
|
2667
|
+
}
|
|
2668
|
+
ts.forEachChild(node, rejectDynamic)
|
|
2669
|
+
}
|
|
2670
|
+
rejectDynamic(sourceFile)
|
|
2671
|
+
for (const node of sourceFile.statements) {
|
|
2672
|
+
if (!ts.isImportDeclaration(node) || !ts.isStringLiteral(node.moduleSpecifier)) continue
|
|
2673
|
+
const target = node.moduleSpecifier.text
|
|
2674
|
+
if (!node.importClause) {
|
|
2675
|
+
if (!target.startsWith(".") && !["react", "react-router-dom", "@kudzujs/core"].includes(target) && !target.startsWith("@kudzujs/core/")) throw sourceNodeError(node, sourceFile, `Side-effect package import ${JSON.stringify(target)} is not supported`)
|
|
2676
|
+
continue
|
|
2677
|
+
}
|
|
2678
|
+
if (node.importClause.isTypeOnly) continue
|
|
2679
|
+
if (target.startsWith(".") || target.startsWith("node:") || target === "react" || target === "react-router-dom" || target === "@kudzujs/core" || target.startsWith("@kudzujs/core/")) continue
|
|
2680
|
+
if (node.importClause.name) bindings.set(node.importClause.name.text, { kind: "default", local: node.importClause.name.text, target, package: true })
|
|
2681
|
+
const named = node.importClause.namedBindings
|
|
2682
|
+
if (named && ts.isNamespaceImport(named)) bindings.set(named.name.text, { kind: "namespace", local: named.name.text, target, package: true })
|
|
2683
|
+
if (named && ts.isNamedImports(named)) for (const entry of named.elements) if (!entry.isTypeOnly) bindings.set(entry.name.text, { kind: "named", imported: (entry.propertyName ?? entry.name).text, local: entry.name.text, target, package: true })
|
|
2684
|
+
}
|
|
2685
|
+
return bindings
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2688
|
+
function importedSerializableCollectionNames(sourceFile, file, sourceFiles, sourceIndex) {
|
|
2689
|
+
return new Set(importedSerializableCollections(sourceFile, file, sourceFiles, sourceIndex).keys())
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2692
|
+
function importedSerializableCollections(sourceFile, file, sourceFiles, sourceIndex) {
|
|
2693
|
+
const collections = new Map()
|
|
2694
|
+
for (const [name, binding] of clientImportBindings(sourceFile, file, sourceFiles)) {
|
|
2695
|
+
if (binding.kind !== "named") continue
|
|
2696
|
+
const imported = parseSourceFile(binding.target, sourceIndex.get(binding.target))
|
|
2697
|
+
for (const statement of imported.statements) {
|
|
2698
|
+
if (!ts.isVariableStatement(statement) || !(statement.declarationList.flags & ts.NodeFlags.Const) || !statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword)) continue
|
|
2699
|
+
const declaration = statement.declarationList.declarations.find(entry => ts.isIdentifier(entry.name) && entry.name.text === binding.imported)
|
|
2700
|
+
if (declaration?.initializer && ts.isArrayLiteralExpression(unwrapExpression(declaration.initializer)) && isSerializableStateLiteral(declaration.initializer)) collections.set(name, unwrapExpression(declaration.initializer))
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
return collections
|
|
2704
|
+
}
|
|
2705
|
+
|
|
2706
|
+
function normalizeImportedStaticCollections(sourceFile, collections, factory, context) {
|
|
2707
|
+
if (!collections.size) return sourceFile
|
|
2708
|
+
const visitor = node => {
|
|
2709
|
+
if (ts.isPropertyAccessExpression(node) && node.name.text === "map" && ts.isIdentifier(node.expression) && collections.has(node.expression.text) && !isShadowedIdentifier(node.expression, sourceFile)) {
|
|
2710
|
+
return factory.updatePropertyAccessExpression(node, synthesizeTree(cloneAst(collections.get(node.expression.text), factory, context)), node.name)
|
|
2711
|
+
}
|
|
2712
|
+
return ts.visitEachChild(node, visitor, context)
|
|
2713
|
+
}
|
|
2714
|
+
return ts.visitNode(sourceFile, visitor)
|
|
2715
|
+
}
|
|
2716
|
+
|
|
2717
|
+
function resolveComponentExport(file, exportName, getSource, sourceFiles, trail = []) {
|
|
2718
|
+
const key = `${file}:${exportName}`
|
|
2719
|
+
if (trail.includes(key)) throw new Error(`Imported keyed list component re-export cycle: ${[...trail, key].map(entry => relative(root, entry.slice(0, entry.lastIndexOf(":")))).join(" -> ")}`)
|
|
2720
|
+
const sourceFile = getSource(file)
|
|
2721
|
+
const nextTrail = [...trail, key]
|
|
2722
|
+
|
|
2723
|
+
for (const statement of sourceFile.statements) {
|
|
2724
|
+
if (ts.isFunctionDeclaration(statement)) {
|
|
2725
|
+
const isDefault = statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.DefaultKeyword)
|
|
2726
|
+
const isExported = statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword)
|
|
2727
|
+
if (exportName === "default" && isDefault || exportName !== "default" && isExported && statement.name?.text === exportName) return statement
|
|
2728
|
+
}
|
|
2729
|
+
if (ts.isVariableStatement(statement) && statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword) && exportName !== "default") {
|
|
2730
|
+
const declaration = statement.declarationList.declarations.find(entry => ts.isIdentifier(entry.name) && entry.name.text === exportName)
|
|
2731
|
+
if (declaration?.initializer && (ts.isArrowFunction(declaration.initializer) || ts.isFunctionExpression(declaration.initializer))) return declaration.initializer
|
|
2732
|
+
}
|
|
2733
|
+
if (exportName === "default" && ts.isExportAssignment(statement) && !statement.isExportEquals && ts.isIdentifier(statement.expression)) {
|
|
2734
|
+
const component = localComponentDeclaration(sourceFile, statement.expression.text)
|
|
2735
|
+
if (component) return component
|
|
2736
|
+
}
|
|
2737
|
+
if (ts.isExportDeclaration(statement) && ts.isNamedExports(statement.exportClause)) {
|
|
2738
|
+
const entry = statement.exportClause.elements.find(element => !element.isTypeOnly && element.name.text === exportName)
|
|
2739
|
+
if (!entry) continue
|
|
2740
|
+
const imported = (entry.propertyName ?? entry.name).text
|
|
2741
|
+
if (statement.moduleSpecifier && ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
2742
|
+
if (!statement.moduleSpecifier.text.startsWith(".")) throw sourceNodeError(statement, sourceFile, "Imported keyed list components must use relative TypeScript re-exports")
|
|
2743
|
+
const target = resolveSourceImport(file, statement.moduleSpecifier.text, sourceFiles)
|
|
2744
|
+
return resolveComponentExport(target, imported, getSource, sourceFiles, nextTrail)
|
|
2745
|
+
}
|
|
2746
|
+
const component = localComponentDeclaration(sourceFile, imported)
|
|
2747
|
+
if (component) return component
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
throw new Error(`${relative(root, file)} does not export a statically analyzable keyed list component named ${JSON.stringify(exportName)}`)
|
|
2751
|
+
}
|
|
2752
|
+
|
|
2753
|
+
function localComponentDeclaration(sourceFile, name) {
|
|
2754
|
+
for (const statement of sourceFile.statements) {
|
|
2755
|
+
if (ts.isFunctionDeclaration(statement) && statement.name?.text === name) return statement
|
|
2756
|
+
if (ts.isVariableStatement(statement)) {
|
|
2757
|
+
const declaration = statement.declarationList.declarations.find(entry => ts.isIdentifier(entry.name) && entry.name.text === name)
|
|
2758
|
+
if (declaration?.initializer && (ts.isArrowFunction(declaration.initializer) || ts.isFunctionExpression(declaration.initializer))) return declaration.initializer
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
return undefined
|
|
2762
|
+
}
|
|
2763
|
+
|
|
2764
|
+
export async function collectClientModules(entries, sourceFiles) {
|
|
2765
|
+
const modules = new Set()
|
|
2766
|
+
const queue = [...new Set(entries)]
|
|
2767
|
+
while (queue.length) {
|
|
2768
|
+
const file = queue.shift()
|
|
2769
|
+
if (modules.has(file)) continue
|
|
2770
|
+
const source = await readFile(file, "utf8")
|
|
2771
|
+
const sourceFile = parseSourceFile(file, source)
|
|
2772
|
+
workerCompiler.rejectConstructions(sourceFile, sourceFile, "Relative TypeScript Worker construction is only supported directly inside an inline useEffect() callback, not imported client helpers")
|
|
2773
|
+
if (containsJsx(sourceFile)) throw new Error(`${relative(root, file)} Imported client helpers must not contain JSX`)
|
|
2774
|
+
rejectUnsupportedClientImports(sourceFile, file)
|
|
2775
|
+
modules.add(file)
|
|
2776
|
+
for (const node of sourceFile.statements) {
|
|
2777
|
+
if ((!ts.isImportDeclaration(node) && !ts.isExportDeclaration(node)) || !node.moduleSpecifier || !ts.isStringLiteral(node.moduleSpecifier) || !runtimeModuleReference(node)) continue
|
|
2778
|
+
if (!node.moduleSpecifier.text.startsWith(".")) throw new Error(`${relative(root, file)} Imported client helpers may only use relative runtime imports`)
|
|
2779
|
+
if (isStaticImport(node.moduleSpecifier.text)) continue
|
|
2780
|
+
queue.push(resolveSourceImport(file, node.moduleSpecifier.text, sourceFiles))
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
const outputs = new Map()
|
|
2784
|
+
for (const file of modules) {
|
|
2785
|
+
const output = clientModulePath(file)
|
|
2786
|
+
if (outputs.has(output)) throw new Error(`${relative(root, file)} and ${relative(root, outputs.get(output))} emit the same client module path`)
|
|
2787
|
+
outputs.set(output, file)
|
|
2788
|
+
}
|
|
2789
|
+
return [...modules].sort()
|
|
2790
|
+
}
|
|
2791
|
+
|
|
2792
|
+
export async function compileClientModule(file, sourceFiles, staticFiles, cssModules, base) {
|
|
2793
|
+
const importedAssets = new Set()
|
|
2794
|
+
const source = await readFile(file, "utf8")
|
|
2795
|
+
const transformer = context => sourceFile => {
|
|
2796
|
+
const factory = context.factory
|
|
2797
|
+
const visitor = node => {
|
|
2798
|
+
if (ts.isImportDeclaration(node) && runtimeModuleReference(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text.startsWith(".")) {
|
|
2799
|
+
if (isStaticImport(node.moduleSpecifier.text)) return staticImportEntry(node, sourceFile, file, staticFiles, importedAssets, cssModules, base, factory)?.replacement
|
|
2800
|
+
const target = resolveSourceImport(file, node.moduleSpecifier.text, sourceFiles)
|
|
2801
|
+
return factory.updateImportDeclaration(node, node.modifiers, node.importClause, factory.createStringLiteral(relativeModulePath(clientModulePath(file), clientModulePath(target))), node.attributes)
|
|
2802
|
+
}
|
|
2803
|
+
if (ts.isExportDeclaration(node) && runtimeModuleReference(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text.startsWith(".")) {
|
|
2804
|
+
const target = resolveSourceImport(file, node.moduleSpecifier.text, sourceFiles)
|
|
2805
|
+
return factory.updateExportDeclaration(node, node.modifiers, node.isTypeOnly, node.exportClause, factory.createStringLiteral(relativeModulePath(clientModulePath(file), clientModulePath(target))), node.attributes)
|
|
2806
|
+
}
|
|
2807
|
+
return ts.visitEachChild(node, visitor, context)
|
|
2808
|
+
}
|
|
2809
|
+
return ts.visitNode(sourceFile, visitor)
|
|
2810
|
+
}
|
|
2811
|
+
const result = ts.transpileModule(source, {
|
|
2812
|
+
fileName: file,
|
|
2813
|
+
compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext },
|
|
2814
|
+
transformers: { before: [transformer] },
|
|
2815
|
+
reportDiagnostics: true
|
|
2816
|
+
})
|
|
2817
|
+
const errors = result.diagnostics?.filter(diagnostic => diagnostic.category === ts.DiagnosticCategory.Error) ?? []
|
|
2818
|
+
if (errors.length) throw new Error(errors.map(error => ts.flattenDiagnosticMessageText(error.messageText, "\n")).join("\n"))
|
|
2819
|
+
return { file: relative(root, file).replaceAll(sep, "/"), path: clientModulePath(file), code: result.outputText, importedAssets: [...importedAssets].map(file => relative(root, file).replaceAll(sep, "/")).sort() }
|
|
2820
|
+
}
|
|
2821
|
+
|
|
2822
|
+
function staticImportExtension(specifier) {
|
|
2823
|
+
return extname(specifier.split(/[?#]/, 1)[0]).toLowerCase()
|
|
2824
|
+
}
|
|
2825
|
+
|
|
2826
|
+
function isStaticImport(specifier) {
|
|
2827
|
+
const extension = staticImportExtension(specifier)
|
|
2828
|
+
return extension === ".css" || staticAssetExtensions.has(extension)
|
|
2829
|
+
}
|
|
2830
|
+
|
|
2831
|
+
function resolveStaticImport(importer, specifier, staticFiles) {
|
|
2832
|
+
const target = resolve(dirname(importer), specifier.split(/[?#]/, 1)[0])
|
|
2833
|
+
if (!staticFiles.has(target)) throw new Error(`${relative(root, importer)} Relative asset import ${JSON.stringify(specifier)} must resolve to an existing regular file under src/`)
|
|
2834
|
+
return target
|
|
2835
|
+
}
|
|
2836
|
+
|
|
2837
|
+
export async function safeStaticFiles(files) {
|
|
2838
|
+
const sourceRoot = await realpath(sourceDirectory)
|
|
2839
|
+
const entries = await Promise.all(files.map(async file => {
|
|
2840
|
+
try {
|
|
2841
|
+
const target = await realpath(file)
|
|
2842
|
+
const path = relative(sourceRoot, target)
|
|
2843
|
+
if (path === ".." || path.startsWith(`..${sep}`) || isAbsolute(path) || !(await stat(target)).isFile()) return undefined
|
|
2844
|
+
return file
|
|
2845
|
+
} catch {
|
|
2846
|
+
return undefined
|
|
2847
|
+
}
|
|
2848
|
+
}))
|
|
2849
|
+
return new Set(entries.filter(Boolean))
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2852
|
+
export function orderSourceStyles(cssFiles, sourceFiles, sourceIndex, staticFiles) {
|
|
2853
|
+
const ordered = []
|
|
2854
|
+
const seenStyles = new Set()
|
|
2855
|
+
const seenSources = new Set()
|
|
2856
|
+
const sourceSet = new Set(sourceFiles)
|
|
2857
|
+
const visit = file => {
|
|
2858
|
+
if (seenSources.has(file)) return
|
|
2859
|
+
seenSources.add(file)
|
|
2860
|
+
const sourceFile = parseSourceFile(file, sourceIndex.get(file))
|
|
2861
|
+
for (const statement of sourceFile.statements) {
|
|
2862
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier) || !statement.moduleSpecifier.text.startsWith(".")) continue
|
|
2863
|
+
const specifier = statement.moduleSpecifier.text
|
|
2864
|
+
if (staticImportExtension(specifier) === ".css") {
|
|
2865
|
+
let target
|
|
2866
|
+
try { target = resolveStaticImport(file, specifier, staticFiles) } catch { continue }
|
|
2867
|
+
if (!seenStyles.has(target)) {
|
|
2868
|
+
seenStyles.add(target)
|
|
2869
|
+
ordered.push(target)
|
|
2870
|
+
}
|
|
2871
|
+
continue
|
|
2872
|
+
}
|
|
2873
|
+
if (isStaticImport(specifier)) continue
|
|
2874
|
+
try { visit(resolveSourceImport(file, specifier, sourceSet)) } catch {}
|
|
2875
|
+
}
|
|
2876
|
+
}
|
|
2877
|
+
for (const file of sourceFiles.filter(file => file.startsWith(`${pagesDirectory}${sep}`) && file.endsWith(".tsx"))) visit(file)
|
|
2878
|
+
for (const file of sourceFiles) visit(file)
|
|
2879
|
+
return [...ordered, ...cssFiles.filter(file => !seenStyles.has(file))]
|
|
2880
|
+
}
|
|
2881
|
+
|
|
2882
|
+
function staticImportEntry(node, sourceFile, file, staticFiles, importedAssets, cssModules, base, factory) {
|
|
2883
|
+
const specifier = node.moduleSpecifier.text
|
|
2884
|
+
if (specifier.includes("\\") || specifier.includes("#")) throw sourceNodeError(node.moduleSpecifier, sourceFile, "Static asset imports require forward-slash paths without hash suffixes")
|
|
2885
|
+
const queryIndex = specifier.indexOf("?")
|
|
2886
|
+
const query = queryIndex === -1 ? "" : specifier.slice(queryIndex + 1)
|
|
2887
|
+
if (query && query !== "url") throw sourceNodeError(node.moduleSpecifier, sourceFile, "Static asset imports support only the ?url query")
|
|
2888
|
+
let target
|
|
2889
|
+
try {
|
|
2890
|
+
target = resolveStaticImport(file, specifier, staticFiles)
|
|
2891
|
+
} catch (error) {
|
|
2892
|
+
throw sourceNodeError(node.moduleSpecifier, sourceFile, error.message)
|
|
2893
|
+
}
|
|
2894
|
+
if (node.attributes) throw sourceNodeError(node.attributes, sourceFile, "Static asset import attributes are not supported")
|
|
2895
|
+
const extension = staticImportExtension(specifier)
|
|
2896
|
+
if (query === "url") {
|
|
2897
|
+
if (!node.importClause?.name || node.importClause.isTypeOnly || node.importClause.namedBindings) throw sourceNodeError(node, sourceFile, "Static assets require one default import")
|
|
2898
|
+
if (extension !== ".css") importedAssets.add(target)
|
|
2899
|
+
const value = factory.createStringLiteral(assetPath(base, `assets/${relative(sourceDirectory, target).replaceAll(sep, "/")}`))
|
|
2900
|
+
return staticImportReplacement(node.importClause.name.text, value, factory)
|
|
2901
|
+
}
|
|
2902
|
+
if (extension === ".css") {
|
|
2903
|
+
const classes = cssModules.get(target)
|
|
2904
|
+
if (!node.importClause) return undefined
|
|
2905
|
+
if (!classes || !node.importClause.name || node.importClause.isTypeOnly || node.importClause.namedBindings) {
|
|
2906
|
+
const message = classes ? "CSS Modules require one default import" : "CSS imports must be side-effect imports"
|
|
2907
|
+
throw sourceNodeError(node.importClause, sourceFile, message)
|
|
2908
|
+
}
|
|
2909
|
+
const value = factory.createObjectLiteralExpression(Object.entries(classes).sort(([left], [right]) => left.localeCompare(right)).map(([name, scoped]) => factory.createPropertyAssignment(factory.createStringLiteral(name), factory.createStringLiteral(scoped))))
|
|
2910
|
+
return staticImportReplacement(node.importClause.name.text, value, factory)
|
|
2911
|
+
}
|
|
2912
|
+
if (!node.importClause?.name || node.importClause.isTypeOnly || node.importClause.namedBindings) throw sourceNodeError(node, sourceFile, "Static assets require one default import")
|
|
2913
|
+
importedAssets.add(target)
|
|
2914
|
+
const value = factory.createStringLiteral(assetPath(base, `assets/${relative(sourceDirectory, target).replaceAll(sep, "/")}`))
|
|
2915
|
+
return staticImportReplacement(node.importClause.name.text, value, factory)
|
|
2916
|
+
}
|
|
2917
|
+
|
|
2918
|
+
function staticImportReplacement(name, value, factory) {
|
|
2919
|
+
return {
|
|
2920
|
+
name,
|
|
2921
|
+
value,
|
|
2922
|
+
replacement: factory.createVariableStatement(undefined, factory.createVariableDeclarationList([
|
|
2923
|
+
factory.createVariableDeclaration(name, undefined, undefined, value)
|
|
2924
|
+
], ts.NodeFlags.Const))
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
|
|
2928
|
+
function rejectUnsupportedClientImports(sourceFile, file) {
|
|
2929
|
+
const visit = node => {
|
|
2930
|
+
if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) throw new Error(`${relative(root, file)} Dynamic imports are not supported in imported client helpers`)
|
|
2931
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "require") throw new Error(`${relative(root, file)} require() is not supported in imported client helpers`)
|
|
2932
|
+
ts.forEachChild(node, visit)
|
|
2933
|
+
}
|
|
2934
|
+
visit(sourceFile)
|
|
2935
|
+
}
|
|
2936
|
+
|
|
2937
|
+
export function layoutExportError(file, source) {
|
|
2938
|
+
const sourceFile = parseSourceFile(file, source)
|
|
2939
|
+
for (const statement of sourceFile.statements) {
|
|
2940
|
+
if (ts.isExportDeclaration(statement) && statement.exportClause && ts.isNamedExports(statement.exportClause)) {
|
|
2941
|
+
const specifier = statement.exportClause.elements.find(entry => entry.name.text === "layout")
|
|
2942
|
+
if (specifier) return sourceNodeError(specifier, sourceFile, "layout export must be a function")
|
|
2943
|
+
}
|
|
2944
|
+
if (ts.isVariableStatement(statement) && statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword)) {
|
|
2945
|
+
const declaration = statement.declarationList.declarations.find(entry => ts.isIdentifier(entry.name) && entry.name.text === "layout")
|
|
2946
|
+
if (declaration) return sourceNodeError(declaration, sourceFile, "layout export must be a function")
|
|
2947
|
+
}
|
|
2948
|
+
if (ts.isFunctionDeclaration(statement) && statement.name?.text === "layout") return sourceNodeError(statement, sourceFile, "layout export must be a function")
|
|
2949
|
+
}
|
|
2950
|
+
return new Error(`${relative(root, file)} layout export must be a function`)
|
|
2951
|
+
}
|
|
2952
|
+
|
|
2953
|
+
export function clientModulePath(file) {
|
|
2954
|
+
return `modules/${relative(sourceDirectory, file).replaceAll(sep, "/").replace(/\.(?:ts|tsx)$/, ".js")}`
|
|
2955
|
+
}
|
|
2956
|
+
|
|
2957
|
+
export function compiledPath(file) {
|
|
2958
|
+
return join(workDirectory, relative(sourceDirectory, file)).replace(/\.(?:ts|tsx)$/, ".mjs")
|
|
2959
|
+
}
|
|
2960
|
+
|
|
2961
|
+
const compileEventCommand = createCommandSpecializer({ isPrimitiveLiteral: isPrimitiveDefaultLiteral })
|
|
2962
|
+
const { analyzeZustandStores, normalizeZustandMigrationSyntax } = createZustandPass({ isSerializableStateLiteral, nativeCaptureNames, sourceDirectory })
|
|
2963
|
+
const workerCompiler = createWorkerCompiler({ root, sourceDirectory, assetPath, parseSourceFile, resolveSourceImport, runtimeModuleReference })
|
|
2964
|
+
const handlerLowering = createHandlerLowering({ cloneAst, synthesizeTree })
|
|
2965
|
+
const printHandlerModule = createHandlerCodegen({
|
|
2966
|
+
resolveClientImport: (entry, handlerPath) => entry.package ? entry.target : relativeModulePath(handlerPath, clientModulePath(entry.target))
|
|
2967
|
+
})
|
|
2968
|
+
const { normalizeReactMigrationSyntax, validateUseIdSyntax } = createReactMigrationPass({ cloneAst, jsxTagName })
|
|
2969
|
+
const normalizeReactRouterSyntax = createRouterPass({ withBase })
|