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