@kudzujs/core 0.8.14 → 0.8.16

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.
Files changed (32) hide show
  1. package/MIGRATION_ROADMAP.md +247 -0
  2. package/PERFORMANCE.md +212 -0
  3. package/README.md +34 -6
  4. package/RELEASES.md +64 -0
  5. package/docs/next-architecture/README.md +42 -0
  6. package/docs/next-architecture/compiler-current-architecture.md +71 -0
  7. package/docs/next-architecture/goal-a-compiler-foundation.md +152 -0
  8. package/docs/next-architecture/goal-b-optimization-benchmarks.md +59 -0
  9. package/docs/next-architecture/goal-c-state-resource-research.md +50 -0
  10. package/docs/next-architecture/goal-d-routing-compatibility-decisions.md +51 -0
  11. package/docs/next-architecture/performance-gates.md +50 -0
  12. package/docs/next-architecture/versioning.md +42 -0
  13. package/framework/README.md +23 -2
  14. package/framework/build.mjs +285 -3569
  15. package/framework/compiler/animation-frame-pass.mjs +103 -0
  16. package/framework/compiler/ast-helpers.mjs +181 -0
  17. package/framework/compiler/browser-signal-passes.mjs +182 -0
  18. package/framework/compiler/collection-analysis.mjs +187 -0
  19. package/framework/compiler/custom-hook-timer-pass.mjs +126 -0
  20. package/framework/compiler/descriptor-session.mjs +222 -0
  21. package/framework/compiler/effect-codegen.mjs +884 -0
  22. package/framework/compiler/event-command-pass.mjs +35 -0
  23. package/framework/compiler/handler-codegen.mjs +296 -0
  24. package/framework/compiler/normalization-pipeline.mjs +9 -0
  25. package/framework/compiler/react-migration-pass.mjs +339 -0
  26. package/framework/compiler/render-control-pass.mjs +96 -0
  27. package/framework/compiler/route-capability-planner.mjs +118 -0
  28. package/framework/compiler/router-pass.mjs +245 -0
  29. package/framework/compiler/worker-compiler.mjs +163 -0
  30. package/framework/compiler/zustand-pass.mjs +95 -0
  31. package/framework/dev-server.mjs +244 -0
  32. package/package.json +4 -1
@@ -1,12 +1,29 @@
1
- import { createServer } from "node:http"
2
1
  import { createHash, randomUUID } from "node:crypto"
3
- import { cp, mkdir, readFile, readdir, realpath, rm, stat, watch, writeFile } from "node:fs/promises"
2
+ import { cp, mkdir, readFile, readdir, realpath, rm, stat, writeFile } from "node:fs/promises"
4
3
  import { dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path"
5
4
  import { pathToFileURL } from "node:url"
6
5
  import { build as bundle, transform } from "esbuild"
7
6
  import ts from "typescript"
7
+ import { normalizeEffectAnimationFrameRefs } from "./compiler/animation-frame-pass.mjs"
8
+ import { bindingNames, containsJsx, effectReturns, functionVarDeclaresName, importDeclarationNames, isFunctionLike, isLocalConst, isReferenceIdentifier, isShadowedByParameter, isShadowedIdentifier, isUnshadowedGlobal, nearestFunction, referenceIdentifiers, referencesIdentifier, sourceLocation, sourceNodeError, statementDeclaresName, unwrapExpression } from "./compiler/ast-helpers.mjs"
9
+ import { normalizeMediaQueryExternalStores, normalizeNavigatorCapabilityConditions } from "./compiler/browser-signal-passes.mjs"
10
+ import { analyzeCollectionPipeline, collectionExpression, collectionParameters, isArrayFromCall, mutatingCollectionMethods as mutatingListMethods, pureCollectionMathMethods as pureMathMethods, pureCollectionMethods as pureListMethods } from "./compiler/collection-analysis.mjs"
11
+ import { normalizeCustomHookTimerRefs } from "./compiler/custom-hook-timer-pass.mjs"
12
+ import { captureNames, createDescriptorSession, createSemanticArtifact, nativeCaptureNames, referencedReducerDispatches, referencedStateNames } from "./compiler/descriptor-session.mjs"
13
+ import { createEffectCodegen } from "./compiler/effect-codegen.mjs"
14
+ import { createEventCommandCompiler } from "./compiler/event-command-pass.mjs"
15
+ import { createHandlerCodegen } from "./compiler/handler-codegen.mjs"
16
+ import { applyNormalizationPasses } from "./compiler/normalization-pipeline.mjs"
17
+ import { createReactMigrationPass, reactMemoExpression } from "./compiler/react-migration-pass.mjs"
18
+ import { normalizeRenderControlFlow } from "./compiler/render-control-pass.mjs"
19
+ import { createRouterPass } from "./compiler/router-pass.mjs"
20
+ import { planRouteCapabilities, usesRouteDependencyRuntime } from "./compiler/route-capability-planner.mjs"
21
+ import { createWorkerCompiler } from "./compiler/worker-compiler.mjs"
22
+ import { createZustandPass } from "./compiler/zustand-pass.mjs"
8
23
  import { renderPage } from "./core.mjs"
9
- import { stateSchema } from "./dev-state.js"
24
+ import { parseDevHost, parseDevPort, startDevServer } from "./dev-server.mjs"
25
+
26
+ export { parseDevHost, parseDevPort }
10
27
 
11
28
  const root = process.cwd()
12
29
  const sourceDirectory = join(root, "src")
@@ -14,8 +31,8 @@ const pagesDirectory = join(sourceDirectory, "pages")
14
31
  const workDirectory = join(root, ".kudzu")
15
32
  const outputDirectory = join(root, "dist")
16
33
  const staticAssetExtensions = new Set([".avif", ".gif", ".ico", ".jpeg", ".jpg", ".otf", ".png", ".svg", ".ttf", ".webp", ".woff", ".woff2"])
17
-
18
- const devClient = (session, revision, schema) => `<script>(()=>{const show=event=>{let box=document.getElementById("__kudzu_error");if(!box){box=document.createElement("div");box.id="__kudzu_error";box.setAttribute("role","alert");box.setAttribute("aria-live","assertive");box.style.cssText="position:fixed;inset:0;z-index:2147483647;overflow:auto;padding:2rem;background:#200;color:#fff;font:16px/1.5 ui-monospace,monospace";const title=document.createElement("strong"),text=document.createElement("pre");title.textContent="Kudzu build error";text.style.whiteSpace="pre-wrap";box.append(title,text);document.body.append(box)}box.querySelector("pre").textContent=event.data};const schema=${inlineJson(schema)},route=location.pathname+location.search+location.hash,urls=[...document.querySelectorAll('script[type="module"][src]')].map(node=>node.src).filter(url=>/\\/assets\\/kudzu(?:-(?:binding|list|native))?\\.js$/.test(new URL(url).pathname));const devImport=import("/__kudzu_dev.js"),runtimeImports=Promise.allSettled(urls.map(url=>import(url)));const ready=(async()=>{const dev=await devImport,modules=await runtimeImports,runtime=modules.find(result=>result.status==="fulfilled"&&result.value.browserState instanceof Map&&typeof result.value.commitDom==="function")?.value;try{dev.restoreState(sessionStorage,route,runtime?.browserState,schema,runtime?.commitDom)}catch{}return{dev,runtime}})().catch(()=>({}));const events=new EventSource("/__kudzu_reload?session=${session}&revision=${revision}");let reloading=false;events.addEventListener("reload",async()=>{if(reloading)return;reloading=true;try{const{dev,runtime}=await ready;dev?.snapshotState(sessionStorage,route,runtime?.browserState,schema)}catch{}location.reload()});events.addEventListener("build-error",show)})()</script>`
34
+ const compileEventCommand = createEventCommandCompiler({ isPrimitiveLiteral: isPrimitiveDefaultLiteral, synthesizeSerializableStateLiteral })
35
+ const { analyzeZustandStores, normalizeZustandMigrationSyntax } = createZustandPass({ isSerializableStateLiteral, nativeCaptureNames, sourceDirectory })
19
36
 
20
37
  export async function build({ quiet = false, minify = true } = {}) {
21
38
  const config = await loadConfig()
@@ -63,14 +80,8 @@ export async function build({ quiet = false, minify = true } = {}) {
63
80
  if (handlerModule) handlerModules.push(handlerModule)
64
81
  }
65
82
 
66
- let behaviorCount = 0
67
- let regularBehaviorCount = 0
68
- let bindingCount = 0
69
- let listCount = 0
70
- let listStyleCount = 0
71
- let regularStateSeedCount = 0
72
- let dependencyStateSeedCount = 0
73
83
  const plans = []
84
+ const routeCapabilities = new Map()
74
85
  const pageEntries = []
75
86
  const effectEntries = []
76
87
  const nativeEntries = []
@@ -151,27 +162,24 @@ export async function build({ quiet = false, minify = true } = {}) {
151
162
  navigationGroup.hasEffects ||= result.hasEffects
152
163
  navigationGroup.hasParams ||= result.hasParams
153
164
  }
154
- const hasDependencies = result.plan.effects.some(effect => effect.dependencies?.length)
155
- const usesDependencyRuntime = !navigable && hasDependencies && !result.plan.effects.some(effect => effect.owner) && !result.hasBindings && !result.hasLists && !result.plan.events.some(event => event.native)
165
+ const usesDependencyRuntime = usesRouteDependencyRuntime({ plan: result.plan, navigable, hasBindings: result.hasBindings, hasLists: result.hasLists })
156
166
  pageEntries.push({ route, html: result.html, usesDependencyRuntime })
157
167
  plans.push({ route: routePath, ...result.plan })
168
+ routeCapabilities.set(routePath, {
169
+ navigable,
170
+ usesDependencyRuntime,
171
+ hasBehaviors: result.hasBehaviors,
172
+ hasBindings: result.hasBindings,
173
+ hasLists: result.hasLists,
174
+ hasListStyles: result.hasListStyles,
175
+ hasStateSeed: result.hasStateSeed
176
+ })
158
177
  if (result.hasParams) paramEntries.push({ path: paramPath, schema: runtimeSchema, params: result.plan.params, searchParams: result.plan.searchParams, searchParamsWritable: result.plan.searchParamsWritable, usesDependencyRuntime, navigable })
159
178
  if (result.hasEffects) effectEntries.push({ path: effectPath, effects: runtimeEffects(result.plan.effects, navigable), paramPath: result.hasParams ? paramPath : undefined, usesDependencyRuntime, navigable })
160
179
  if (result.plan.events.some(event => event.native)) nativeEntries.push({
161
180
  path: nativePath,
162
181
  modules: [...new Set(result.plan.events.filter(event => event.native).map(event => event.native.module))]
163
182
  })
164
- if (result.hasBehaviors) {
165
- behaviorCount++
166
- if (!usesDependencyRuntime) regularBehaviorCount++
167
- }
168
- if (result.hasBindings) bindingCount++
169
- if (result.hasLists) listCount++
170
- if (result.hasListStyles) listStyleCount++
171
- if (result.hasStateSeed) {
172
- if (usesDependencyRuntime) dependencyStateSeedCount++
173
- else regularStateSeedCount++
174
- }
175
183
  }
176
184
  }
177
185
 
@@ -189,7 +197,7 @@ export async function build({ quiet = false, minify = true } = {}) {
189
197
  const renderedEffects = new Set(plans.flatMap(plan => plan.effects.map(effect => `${effect.module}:${effect.handler}`)))
190
198
  const renderedWorkerReferences = workerReferences.filter(reference => renderedEffects.has(`${reference.module}:${reference.handler}`))
191
199
  if (renderedWorkerReferences.length && await exists(join(publicDirectory, "assets", "workers"))) throw new Error("public/assets/workers collides with Kudzu's generated Worker asset namespace")
192
- const workerAssets = await emitWorkers(renderedWorkerReferences, sourceFileSet, assetsDirectory, base, minify)
200
+ const workerAssets = await workerCompiler.emit(renderedWorkerReferences, sourceFileSet, assetsDirectory, base, minify)
193
201
  for (const module of emittedHandlerModules) {
194
202
  for (const reference of workerReferences) {
195
203
  if (reference.module !== assetPath(base, `assets/${module.path}`)) continue
@@ -198,43 +206,41 @@ export async function build({ quiet = false, minify = true } = {}) {
198
206
  }
199
207
  if (module.code.includes("/__kudzu_worker_")) throw new Error(`Worker URL placeholder survived in ${module.path}`)
200
208
  }
201
- const commandEvents = [...new Set(plans.flatMap(plan => plan.events.filter(event => event.commands).map(event => event.event)))].sort()
202
- const nativeEvents = [...new Set(plans.flatMap(plan => plan.events.filter(event => event.native).map(event => event.event)))].sort()
203
- const hasTextBindings = plans.some(plan => plan.bindings.some(binding => binding.target === "text"))
204
- const hasSvgConditions = plans.some(plan => plan.conditions.some(condition => condition.svg))
205
- const hasListConditions = plans.some(plan => plan.lists.some(list => list.conditions))
206
- const hasSvgLists = plans.some(plan => plan.lists.some(list => list.svg))
207
- const hasDeepListConditions = plans.some(plan => plan.lists.some(list => list.conditionHandlers))
208
- const hasListTextRanges = plans.some(plan => plan.lists.some(list => list.textRanges))
209
- const hasListAttributes = plans.some(plan => plan.lists.some(list => list.attributes))
210
- const hasListEvents = plans.some(plan => plan.lists.some(list => list.events))
211
- const hasListExpressions = plans.some(plan => plan.lists.some(list => list.expressions))
212
- const hasListExpressionAttributes = plans.some(plan => plan.lists.some(list => list.expressionAttributes))
213
- const hasListSeeds = plans.some(plan => plan.lists.some(list => list.seed || list.valueSeed))
214
- const hasListEffects = plans.some(plan => plan.lists.some(list => list.effects))
215
- const hasListRowHooks = plans.some(plan => plan.lists.some(list => list.rowStates?.length || list.rowRefs?.length))
216
- const hasListRowRefs = plans.some(plan => plan.lists.some(list => list.rowRefs?.length))
217
- const hasComplexListRowState = plans.some(plan => plan.lists.some(list => list.rowStates?.some(state => state.initialValue !== null && typeof state.initialValue === "object")))
218
- const hasNestedLists = plans.some(plan => plan.lists.some(list => list.ownerField))
219
- const hasCollectionSelectors = plans.some(plan => plan.lists.some(list => list.selector))
220
- const hasCalculatedCollections = plans.some(plan => plan.lists.some(list => list.source))
221
- const hasDerivedEffectDependencies = plans.some(plan => plan.effects.some(effect => effect.dependencyExpressions?.length))
222
- const hasStaticCollections = plans.some(plan => plan.lists.some(list => list.static))
223
- const hasListIndexes = plans.some(plan => plan.lists.some(list => list.indexed))
224
- const hasListStableFastPaths = plans.some(plan => plan.lists.some(list => !list.children && !list.ownerField && list.key !== null && !list.indexed && !list.reducer && !list.selector))
225
- const hasGeneralListRowHooks = hasListRowRefs || hasComplexListRowState || plans.some(plan => plan.lists.some(list => list.ownerField && (list.rowStates?.length || list.rowRefs?.length)))
226
- const hasItemDependencies = plans.some(plan => plan.effects.some(effect => effect.itemDependencies?.length))
227
- const hasListAsyncParts = hasListExpressions || hasListExpressionAttributes || hasListConditions
228
- const hasListMounts = hasListConditions || hasNestedLists || plans.some(plan => plan.lists.some(list => list.mount))
229
- const hasNestedStateCaptures = hasNestedCaptureState(plans)
230
- const hasSetterCaptures = hasCaptureType(plans, "setter")
231
- const hasEffectCaptures = plans.some(plan => plan.effects.some(effect => Object.keys(effect.scope).length))
232
- const hasNativeHandlers = nativeEntries.length > 0
233
- const hasEffects = effectEntries.length > 0
234
- const hasNavigableEffects = effectEntries.some(entry => entry.navigable)
235
- const hasNavigableOwners = effectEntries.some(entry => entry.navigable && entry.effects.some(effect => effect.owner))
236
- const hasSharedRuntime = bindingCount || listCount || hasNativeHandlers || navigationRoutes.length
237
- const hasDependencyRuntime = pageEntries.some(entry => entry.usesDependencyRuntime)
209
+ const capabilityManifest = planRouteCapabilities(plans, { routes: routeCapabilities, navigationRouteCount: navigationRoutes.length })
210
+ const {
211
+ routes: { behaviors: behaviorCount, regularBehaviors: regularBehaviorCount, regularStateSeeds: regularStateSeedCount, dependencyStateSeeds: dependencyStateSeedCount },
212
+ events: { command: commandEvents, native: nativeEvents, hasNativeHandlers },
213
+ bindings: { count: bindingCount, text: hasTextBindings, svgConditions: hasSvgConditions },
214
+ lists: {
215
+ count: listCount,
216
+ styleCount: listStyleCount,
217
+ conditions: hasListConditions,
218
+ svg: hasSvgLists,
219
+ deepConditions: hasDeepListConditions,
220
+ textRanges: hasListTextRanges,
221
+ attributes: hasListAttributes,
222
+ events: hasListEvents,
223
+ expressions: hasListExpressions,
224
+ expressionAttributes: hasListExpressionAttributes,
225
+ seeds: hasListSeeds,
226
+ effects: hasListEffects,
227
+ rowHooks: hasListRowHooks,
228
+ rowRefs: hasListRowRefs,
229
+ complexRowState: hasComplexListRowState,
230
+ nested: hasNestedLists,
231
+ selectors: hasCollectionSelectors,
232
+ calculated: hasCalculatedCollections,
233
+ static: hasStaticCollections,
234
+ indexes: hasListIndexes,
235
+ stableFastPaths: hasListStableFastPaths,
236
+ generalRowHooks: hasGeneralListRowHooks,
237
+ asyncParts: hasListAsyncParts,
238
+ mounts: hasListMounts
239
+ },
240
+ effects: { any: hasEffects, derivedDependencies: hasDerivedEffectDependencies, itemDependencies: hasItemDependencies, captures: hasEffectCaptures, navigable: hasNavigableEffects, navigableOwners: hasNavigableOwners },
241
+ captures: { nestedState: hasNestedStateCaptures, setter: hasSetterCaptures },
242
+ runtime: { shared: hasSharedRuntime, dependency: hasDependencyRuntime }
243
+ } = capabilityManifest
238
244
  const runtimeName = usesDependencyRuntime => usesDependencyRuntime ? "kudzu-deps.js" : "kudzu.js"
239
245
  for (const entry of pageEntries) {
240
246
  const routeDirectory = join(outputDirectory, entry.route)
@@ -399,11 +405,7 @@ export async function build({ quiet = false, minify = true } = {}) {
399
405
  for (const entry of effectEntries) {
400
406
  const output = join(assetsDirectory, entry.path)
401
407
  await mkdir(dirname(output), { recursive: true })
402
- await writeJavaScript(output, entry.navigable
403
- ? entry.effects.some(effect => effect.owner)
404
- ? printOwnedNavigableEffectEntry(entry.effects, output, emittedHandlerModules, assetsDirectory, base)
405
- : printNavigableEffectEntry(entry.effects, output, emittedHandlerModules, assetsDirectory, base)
406
- : printEffectEntry(entry.effects, output, emittedHandlerModules, assetsDirectory, base, entry.paramPath, runtimeName(entry.usesDependencyRuntime)), minify)
408
+ await writeJavaScript(output, printEffectEntry(entry.effects, output, emittedHandlerModules, assetsDirectory, base, entry.paramPath, runtimeName(entry.usesDependencyRuntime), entry.navigable), minify)
407
409
  }
408
410
  const clientModules = await collectClientModules(emittedHandlerModules.flatMap(module => module.clientImports), sourceFileSet)
409
411
  for (const file of clientModules) {
@@ -540,559 +542,6 @@ async function printNativeEntry(entry, assetsDirectory, base, minify) {
540
542
  await writeJavaScript(output, `import { registerNativeModules } from ${JSON.stringify(runtime)}\n${imports}\nregisterNativeModules([${registrations}])`, minify)
541
543
  }
542
544
 
543
- function printEffectEntry(effects, output, handlerModules, assetsDirectory, base, paramPath, runtimeName) {
544
- const hasCleanup = effects.some(effect => effect.cleanup)
545
- const hasDependencies = effects.some(effect => effect.dependencies?.length || effect.itemDependencies?.length)
546
- const hasOwners = effects.some(effect => effect.owner)
547
- const hasDependencyExpressions = effects.some(effect => effect.dependencyExpressions?.length)
548
- const moduleUrls = [...new Set(effects.map(effect => effect.module))]
549
- const modules = moduleUrls.map(url => {
550
- const module = handlerModules.find(entry => assetPath(base, `assets/${entry.path}`) === url)
551
- if (!module) throw new Error(`Effect handler module was not emitted: ${url}`)
552
- return module
553
- })
554
- const imports = [
555
- hasCleanup || hasDependencies || hasOwners
556
- ? `import * as __kRuntime from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, runtimeName)))}\nconst { browserState, commitDom } = __kRuntime`
557
- : `import { browserState, commitDom } from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, runtimeName)))}`,
558
- `import { createEffectContext } from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, "kudzu-effect.js")))}`,
559
- ...(hasDependencyExpressions ? [`import { evaluateCollectionExpression as __kEvaluateDependency } from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, "kudzu-collection-selector.js")))}`] : []),
560
- ...(paramPath ? [`import ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, paramPath)))}`] : []),
561
- ...modules.map((module, index) => `import * as __kEffectModule${index} from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, module.path)))}`)
562
- ]
563
- const entries = moduleUrls.map((url, index) => `[${JSON.stringify(url)}, __kEffectModule${index}]`).join(",")
564
- if (hasOwners) return printOwnedEffectEntry(imports, effects, entries)
565
- if (effects.length === 1 && effects[0].dependencies?.length === 1 && !hasDependencyExpressions) return printSingleDependencyEffect(imports, effects[0], hasCleanup)
566
- const disposal = hasCleanup ? `
567
- let disposed = false
568
- const dispose = root => {
569
- if (root !== document || disposed) return
570
- disposed = true
571
- active = false
572
- pending.clear()
573
- for (const record of records) invokeCleanup(record)
574
- }
575
-
576
- if (__kRuntime.registerUnmountHook) __kRuntime.registerUnmountHook(dispose)
577
- addEventListener("pagehide", event => {
578
- if (event.persisted) return
579
- if (__kRuntime.unmountDom) __kRuntime.unmountDom(document)
580
- else dispose(document)
581
- })` : ""
582
- if (hasDependencies) return `${imports.join("\n")}
583
- const effects = ${inlineJson(effects)}
584
- const modules = new Map([${entries}])
585
- const records = effects.map((effect, index) => ({ effect, index, values: undefined, cleanup: undefined, token: undefined }))
586
- const dependencies = new Map()
587
- const pending = new Set()
588
- let scheduled = false
589
- let flushing = false
590
- let active = true
591
- for (const record of records) {
592
- for (const id of record.effect.dependencies ?? []) {
593
- const subscribers = dependencies.get(id) ?? new Set()
594
- subscribers.add(record)
595
- dependencies.set(id, subscribers)
596
- }
597
- }
598
- __kRuntime.registerCommitter(id => {
599
- if (!active) return
600
- for (const record of dependencies.get(id) ?? []) pending.add(record)
601
- schedule()
602
- })
603
- for (const record of records) {
604
- try {
605
- record.values = readDependencies(record)
606
- invoke(record)
607
- } catch (error) {
608
- console.error(error)
609
- }
610
- }
611
- function schedule() {
612
- if (!pending.size || scheduled || flushing) return
613
- scheduled = true
614
- queueMicrotask(flush)
615
- }
616
- async function flush() {
617
- scheduled = false
618
- if (!active) return pending.clear()
619
- flushing = true
620
- try {
621
- const selected = [...pending].sort((left, right) => left.index - right.index)
622
- pending.clear()
623
- const changed = []
624
- for (const record of selected) {
625
- try {
626
- const values = readDependencies(record)
627
- if (!record.values || values.some((value, index) => !Object.is(value, record.values[index]))) {
628
- record.values = values
629
- changed.push(record)
630
- }
631
- } catch (error) {
632
- console.error(error)
633
- }
634
- }
635
- for (const record of changed) await invokeCleanup(record)
636
- if (active) for (const record of changed) invoke(record)
637
- } finally {
638
- flushing = false
639
- if (active) schedule()
640
- }
641
- }
642
- function readDependencies(record) {
643
- ${hasDependencyExpressions ? printDerivedDependencyRead("browserState") : ""}
644
- return (record.effect.dependencies ?? []).map(id => {
645
- const value = browserState.get(id)
646
- if (value !== null && typeof value !== "string" && typeof value !== "boolean" && !(typeof value === "number" && Number.isFinite(value) && !Object.is(value, -0))) throw new Error("useEffect() dependency state must remain a JSON-safe primitive")
647
- return value
648
- })
649
- }
650
- function invoke(record) {
651
- try {
652
- const effect = record.effect
653
- const token = { active: true }
654
- record.token = token
655
- const result = modules.get(effect.module)[effect.handler](createEffectContext(browserState, effect.states, commitDom, effect.scope, () => active && token.active && record.token === token))
656
- if (effect.cleanup && typeof result === "function") record.cleanup = result
657
- else if (result && typeof result.then === "function") result.catch(error => console.error(error))
658
- } catch (error) {
659
- console.error(error)
660
- }
661
- }
662
- async function invokeCleanup(record) {
663
- if (record.token) record.token.active = false
664
- record.token = undefined
665
- const cleanup = record.cleanup
666
- record.cleanup = undefined
667
- if (!cleanup) return
668
- try {
669
- await cleanup()
670
- } catch (error) {
671
- console.error(error)
672
- }
673
- }${disposal}`
674
- if (!hasCleanup) return `${imports.join("\n")}
675
- const effects = ${inlineJson(effects)}
676
- const modules = new Map([${entries}])
677
- for (const effect of effects) {
678
- try {
679
- const result = modules.get(effect.module)[effect.handler](createEffectContext(browserState, effect.states, commitDom, effect.scope))
680
- if (result && typeof result.then === "function") result.catch(error => console.error(error))
681
- } catch (error) {
682
- console.error(error)
683
- }
684
- }`
685
- return `${imports.join("\n")}
686
- const effects = ${inlineJson(effects)}
687
- const modules = new Map([${entries}])
688
- const cleanups = []
689
- for (const effect of effects) {
690
- try {
691
- const result = modules.get(effect.module)[effect.handler](createEffectContext(browserState, effect.states, commitDom, effect.scope))
692
- if (effect.cleanup && typeof result === "function") cleanups.push(result)
693
- else if (result && typeof result.then === "function") result.catch(error => console.error(error))
694
- } catch (error) {
695
- console.error(error)
696
- }
697
- }
698
- let cleaned = false
699
- const dispose = root => {
700
- if (root !== document || cleaned) return
701
- cleaned = true
702
- for (const cleanup of cleanups) {
703
- try {
704
- const result = cleanup()
705
- if (result && typeof result.then === "function") result.catch(error => console.error(error))
706
- } catch (error) {
707
- console.error(error)
708
- }
709
- }
710
- cleanups.length = 0
711
- }
712
- if (__kRuntime.registerUnmountHook) __kRuntime.registerUnmountHook(dispose)
713
- addEventListener("pagehide", event => {
714
- if (event.persisted) return
715
- if (__kRuntime.unmountDom) __kRuntime.unmountDom(document)
716
- else dispose(document)
717
- })`
718
- }
719
-
720
- function printNavigableEffectEntry(effects, output, handlerModules, assetsDirectory, base) {
721
- const hasDependencyExpressions = effects.some(effect => effect.dependencyExpressions?.length)
722
- const moduleUrls = [...new Set(effects.map(effect => effect.module))]
723
- const modules = moduleUrls.map(url => {
724
- const module = handlerModules.find(entry => assetPath(base, `assets/${entry.path}`) === url)
725
- if (!module) throw new Error(`Effect handler module was not emitted: ${url}`)
726
- return module
727
- })
728
- const imports = [
729
- `import * as __kRuntime from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, "kudzu.js")))}`,
730
- `import { createEffectContext } from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, "kudzu-effect.js")))}`,
731
- ...(hasDependencyExpressions ? [`import { evaluateCollectionExpression as __kEvaluateDependency } from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, "kudzu-collection-selector.js")))}`] : []),
732
- ...modules.map((module, index) => `import * as __kEffectModule${index} from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, module.path)))}`)
733
- ]
734
- const entries = moduleUrls.map((url, index) => `[${JSON.stringify(url)}, __kEffectModule${index}]`).join(",")
735
- return `${imports.join("\n")}
736
- const effects = ${inlineJson(effects)}
737
- const modules = new Map([${entries}])
738
- export const mountLayoutEffects = () => mount("layout")
739
- export const mountRouteEffects = () => mount("route")
740
- function mount(lifetime) {
741
- let active = true
742
- let flushing
743
- const records = effects.filter(effect => effect.lifetime === lifetime).map((effect, index) => ({ effect, index, values: undefined, cleanup: undefined, token: undefined }))
744
- const dependencies = new Map()
745
- const pending = new Set()
746
- let scheduled = false
747
- for (const record of records) for (const id of record.effect.dependencies ?? []) {
748
- const subscribers = dependencies.get(id) ?? new Set()
749
- subscribers.add(record)
750
- dependencies.set(id, subscribers)
751
- }
752
- const unsubscribe = dependencies.size ? __kRuntime.registerCommitter(id => {
753
- if (!active) return
754
- for (const record of dependencies.get(id) ?? []) pending.add(record)
755
- if (pending.size && !scheduled && !flushing) {
756
- scheduled = true
757
- queueMicrotask(flush)
758
- }
759
- }) : undefined
760
- for (const record of records) {
761
- try {
762
- record.values = readDependencies(record)
763
- invoke(record)
764
- } catch (error) {
765
- console.error(error)
766
- }
767
- }
768
- async function flush() {
769
- scheduled = false
770
- if (!active) return pending.clear()
771
- const operation = (async () => {
772
- const selected = [...pending].sort((left, right) => left.index - right.index)
773
- pending.clear()
774
- const changed = []
775
- for (const record of selected) {
776
- try {
777
- const values = readDependencies(record)
778
- if (!record.values || values.some((value, index) => !Object.is(value, record.values[index]))) {
779
- record.values = values
780
- changed.push(record)
781
- }
782
- } catch (error) {
783
- console.error(error)
784
- }
785
- }
786
- for (const record of changed) await cleanup(record)
787
- if (active) for (const record of changed) invoke(record)
788
- })()
789
- flushing = operation
790
- try { await operation } finally {
791
- if (flushing === operation) flushing = undefined
792
- if (active && pending.size && !scheduled) {
793
- scheduled = true
794
- queueMicrotask(flush)
795
- }
796
- }
797
- }
798
- function readDependencies(record) {
799
- ${hasDependencyExpressions ? printDerivedDependencyRead("__kRuntime.browserState") : ""}
800
- return (record.effect.dependencies ?? []).map(id => {
801
- const value = __kRuntime.browserState.get(id)
802
- if (value !== null && typeof value !== "string" && typeof value !== "boolean" && !(typeof value === "number" && Number.isFinite(value) && !Object.is(value, -0))) throw new Error("useEffect() dependency state must remain a JSON-safe primitive")
803
- return value
804
- })
805
- }
806
- function invoke(record) {
807
- const token = { active: true }
808
- record.token = token
809
- try {
810
- const effect = record.effect
811
- const result = modules.get(effect.module)[effect.handler](createEffectContext(__kRuntime.browserState, effect.states, __kRuntime.commitDom, effect.scope, () => active && token.active && record.token === token))
812
- if (effect.cleanup && typeof result === "function") record.cleanup = result
813
- else if (result && typeof result.then === "function") result.catch(error => console.error(error))
814
- } catch (error) {
815
- console.error(error)
816
- }
817
- }
818
- async function cleanup(record) {
819
- if (record.token) record.token.active = false
820
- record.token = undefined
821
- const current = record.cleanup
822
- record.cleanup = undefined
823
- if (!current) return
824
- try { await current() } catch (error) { console.error(error) }
825
- }
826
- let disposal
827
- return async function dispose() {
828
- if (disposal) return disposal
829
- disposal = (async () => {
830
- active = false
831
- unsubscribe?.()
832
- pending.clear()
833
- for (const record of records) if (record.token) record.token.active = false
834
- if (flushing) await flushing
835
- for (const record of records) await cleanup(record)
836
- })()
837
- return disposal
838
- }
839
- }`
840
- }
841
-
842
- function printOwnedNavigableEffectEntry(effects, output, handlerModules, assetsDirectory, base) {
843
- const hasItemDependencies = effects.some(effect => effect.itemDependencies?.length)
844
- const hasDependencyExpressions = effects.some(effect => effect.dependencyExpressions?.length)
845
- const moduleUrls = [...new Set(effects.map(effect => effect.module))]
846
- const modules = moduleUrls.map(url => {
847
- const module = handlerModules.find(entry => assetPath(base, `assets/${entry.path}`) === url)
848
- if (!module) throw new Error(`Effect handler module was not emitted: ${url}`)
849
- return module
850
- })
851
- const imports = [
852
- `import * as __kRuntime from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, "kudzu.js")))}`,
853
- `import { createEffectContext } from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, "kudzu-effect.js")))}`,
854
- ...(hasDependencyExpressions ? [`import { evaluateCollectionExpression as __kEvaluateDependency } from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, "kudzu-collection-selector.js")))}`] : []),
855
- ...modules.map((module, index) => `import * as __kEffectModule${index} from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, module.path)))}`)
856
- ]
857
- const entries = moduleUrls.map((url, index) => `[${JSON.stringify(url)}, __kEffectModule${index}]`).join(",")
858
- return `${imports.join("\n")}
859
- const effects = ${inlineJson(effects)}
860
- const modules = new Map([${entries}])
861
- export const mountLayoutEffects = () => mount("layout")
862
- export const mountRouteEffects = () => mount("route")
863
- function mount(lifetime) {
864
- let active = true
865
- let flushing
866
- let order = 0
867
- const selectedEffects = effects.map((effect, index) => ({ effect, index })).filter(entry => entry.effect.lifetime === lifetime)
868
- const records = new Set()
869
- const owners = new Map()
870
- const listTemplates = new Map()
871
- const registrations = new WeakMap()
872
- const dependencies = new Map()
873
- const pending = new Set()
874
- const startedCleanups = new Set()
875
- let scheduled = false
876
- for (const template of selectedEffects) {
877
- if (template.effect.list) listTemplates.set(template.effect.owner, template)
878
- else {
879
- const record = createRecord(template, !template.effect.owner)
880
- if (template.effect.owner) owners.set(template.effect.owner, record)
881
- }
882
- }
883
- const unsubscribeCommitter = selectedEffects.some(({ effect }) => effect.dependencies?.length) ? __kRuntime.registerCommitter(id => {
884
- if (!active) return
885
- for (const record of dependencies.get(id) ?? []) if (record.mounted) pending.add(record)
886
- schedule()
887
- }) : undefined
888
- const unsubscribeMount = __kRuntime.registerMountHook(mountOwned)
889
- const unsubscribeUnmount = __kRuntime.registerUnmountHook(unmountOwned)
890
- ${hasItemDependencies ? `const unsubscribeItems = [...new Set(selectedEffects.filter(({ effect }) => effect.itemDependencies?.length).map(({ effect }) => effect.listState))].map(listState => __kRuntime.registerListItemHook(listState, root => {
891
- if (!active) return
892
- for (const record of registrations.get(root) ?? []) if (record.mounted && record.effect.itemDependencies) pending.add(record)
893
- schedule()
894
- }))
895
- ` : ""}for (const record of records) if (record.mounted) start(record)
896
- mountOwned(document)
897
- function createRecord(template, mounted = true) {
898
- const record = { ...template, order: order++, mounted, marker: undefined, version: 0, values: undefined, cleanup: undefined, disposal: undefined, token: undefined }
899
- records.add(record)
900
- registerDependencies(record)
901
- return record
902
- }
903
- function registerDependencies(record) {
904
- for (const id of record.effect.dependencies ?? []) {
905
- const subscribers = dependencies.get(id) ?? new Set()
906
- subscribers.add(record)
907
- dependencies.set(id, subscribers)
908
- }
909
- }
910
- function unregisterDependencies(record) {
911
- for (const id of record.effect.dependencies ?? []) {
912
- const subscribers = dependencies.get(id)
913
- subscribers?.delete(record)
914
- if (!subscribers?.size) dependencies.delete(id)
915
- }
916
- }
917
- function mountOwned(root) {
918
- if (!active) return
919
- for (const marker of matching(root)) {
920
- if (!marker.isConnected) continue
921
- if (marker.dataset.kEffects) {
922
- if (registrations.has(marker)) continue
923
- const rowRecords = JSON.parse(marker.dataset.kEffects).flatMap(owner => {
924
- const template = listTemplates.get(owner)
925
- if (!template) return []
926
- const record = createRecord(template)
927
- record.marker = marker
928
- start(record)
929
- return [record]
930
- })
931
- if (rowRecords.length) registrations.set(marker, rowRecords)
932
- continue
933
- }
934
- const record = owners.get(marker.dataset.kEffect)
935
- if (record && !record.mounted) mountRecord(record, marker)
936
- }
937
- }
938
- function unmountOwned(root) {
939
- if (!active) return
940
- for (const marker of matching(root)) {
941
- const rowRecords = registrations.get(marker)
942
- if (rowRecords) {
943
- for (const record of rowRecords) unmountRecord(record, true)
944
- registrations.delete(marker)
945
- continue
946
- }
947
- const record = owners.get(marker.dataset.kEffect)
948
- if (record?.marker === marker) unmountRecord(record)
949
- }
950
- }
951
- function matching(root) {
952
- const selector = "template[data-k-effect],[data-k-effects]"
953
- return [...(root.matches?.(selector) ? [root] : []), ...(root.querySelectorAll?.(selector) ?? [])]
954
- }
955
- function mountRecord(record, marker) {
956
- record.mounted = true
957
- record.marker = marker
958
- const version = ++record.version
959
- const begin = () => {
960
- if (active && record.mounted && record.version === version && marker.isConnected) start(record)
961
- }
962
- if (record.disposal) record.disposal.then(begin)
963
- else begin()
964
- }
965
- function unmountRecord(record, dynamic = false) {
966
- if (!record.mounted) return
967
- record.mounted = false
968
- record.marker = undefined
969
- record.version++
970
- pending.delete(record)
971
- if (dynamic) {
972
- unregisterDependencies(record)
973
- records.delete(record)
974
- }
975
- void cleanup(record)
976
- }
977
- function start(record) {
978
- try {
979
- record.values = readDependencies(record)
980
- invoke(record)
981
- } catch (error) {
982
- console.error(error)
983
- }
984
- }
985
- function schedule() {
986
- if (!pending.size || scheduled || flushing) return
987
- scheduled = true
988
- queueMicrotask(flush)
989
- }
990
- async function flush() {
991
- scheduled = false
992
- if (!active) return pending.clear()
993
- const operation = (async () => {
994
- const changed = []
995
- const selected = [...pending].filter(record => record.mounted).sort((left, right) => left.index - right.index || left.order - right.order)
996
- pending.clear()
997
- for (const record of selected) {
998
- try {
999
- const values = readDependencies(record)
1000
- if (!record.values || values.some((value, index) => !Object.is(value, record.values[index]))) changed.push([record, record.version])
1001
- } catch (error) {
1002
- console.error(error)
1003
- }
1004
- }
1005
- for (const [record] of changed) await cleanup(record)
1006
- if (active) for (const [record, version] of changed) if (record.mounted && record.version === version) {
1007
- try {
1008
- record.values = readDependencies(record)
1009
- invoke(record)
1010
- } catch (error) {
1011
- record.values = undefined
1012
- console.error(error)
1013
- }
1014
- }
1015
- })()
1016
- flushing = operation
1017
- try { await operation } finally {
1018
- if (flushing === operation) flushing = undefined
1019
- if (active) schedule()
1020
- }
1021
- }
1022
- function readDependencies(record) {
1023
- ${hasDependencyExpressions ? printDerivedDependencyRead("__kRuntime.browserState") : ""}
1024
- const values = (record.effect.dependencies ?? []).map(id => {
1025
- const value = __kRuntime.browserState.get(id)
1026
- if (value !== null && typeof value !== "string" && typeof value !== "boolean" && !(typeof value === "number" && Number.isFinite(value) && !Object.is(value, -0))) throw new Error("useEffect() dependency state must remain a JSON-safe primitive")
1027
- return value
1028
- })
1029
- ${hasItemDependencies ? `if (record.effect.itemDependencies) {
1030
- const item = JSON.parse(record.marker.dataset.kEffectItem)
1031
- for (const field of record.effect.itemDependencies) {
1032
- const value = item[field]
1033
- if (value !== null && typeof value !== "string" && typeof value !== "boolean" && !(typeof value === "number" && Number.isFinite(value) && !Object.is(value, -0))) throw new Error(\`useEffect() keyed item dependency "\${field}" must remain a JSON-safe primitive\`)
1034
- values.push(value)
1035
- }
1036
- }` : ""}
1037
- return values
1038
- }
1039
- function invoke(record) {
1040
- const token = { active: true }
1041
- record.token = token
1042
- try {
1043
- const effect = record.effect
1044
- const scope = effect.list
1045
- ? Object.fromEntries(Object.entries(effect.scope).map(([name, value]) => [name, value?.type === "list-item" ? JSON.parse(record.marker.dataset.kEffectItem) : value]))
1046
- : effect.scope
1047
- const result = modules.get(effect.module)[effect.handler](createEffectContext(__kRuntime.browserState, effect.states, __kRuntime.commitDom, scope, () => active && token.active && record.token === token))
1048
- if (effect.cleanup && typeof result === "function") record.cleanup = result
1049
- else if (result && typeof result.then === "function") result.catch(error => console.error(error))
1050
- } catch (error) {
1051
- console.error(error)
1052
- }
1053
- }
1054
- function cleanup(record) {
1055
- if (record.token) record.token.active = false
1056
- record.token = undefined
1057
- if (record.disposal) return record.disposal
1058
- const current = record.cleanup
1059
- record.cleanup = undefined
1060
- if (!current) return Promise.resolve()
1061
- const disposal = (async () => {
1062
- try { await current() } catch (error) { console.error(error) }
1063
- })()
1064
- record.disposal = disposal
1065
- startedCleanups.add(disposal)
1066
- disposal.finally(() => {
1067
- startedCleanups.delete(disposal)
1068
- if (record.disposal === disposal) record.disposal = undefined
1069
- })
1070
- return disposal
1071
- }
1072
- let disposal
1073
- return async function dispose() {
1074
- if (disposal) return disposal
1075
- disposal = (async () => {
1076
- active = false
1077
- unsubscribeCommitter?.()
1078
- ${hasItemDependencies ? "for (const unsubscribe of unsubscribeItems) unsubscribe()\n " : ""}unsubscribeMount()
1079
- unsubscribeUnmount()
1080
- pending.clear()
1081
- for (const record of records) if (record.token) record.token.active = false
1082
- if (flushing) await flushing
1083
- const mounted = [...records].filter(record => record.mounted).sort((left, right) => left.index - right.index || left.order - right.order)
1084
- for (const record of mounted) {
1085
- record.mounted = false
1086
- await cleanup(record)
1087
- }
1088
- await Promise.all([...startedCleanups])
1089
- records.clear()
1090
- })()
1091
- return disposal
1092
- }
1093
- }`
1094
- }
1095
-
1096
545
  function runtimeEffects(effects, lifetimes = false) {
1097
546
  return effects.map(effect => ({
1098
547
  module: effect.module,
@@ -1109,328 +558,6 @@ function runtimeEffects(effects, lifetimes = false) {
1109
558
  }))
1110
559
  }
1111
560
 
1112
- function printDerivedDependencyRead(state) {
1113
- return ` if (record.effect.dependencyExpressions) return record.effect.dependencyExpressions.map(expression => {
1114
- const value = __kEvaluateDependency(expression, undefined, undefined, name => ${state}.get(record.effect.dependencyStates[name]))
1115
- if (value !== null && typeof value !== "string" && typeof value !== "boolean" && !(typeof value === "number" && Number.isFinite(value) && !Object.is(value, -0))) throw new Error("useEffect() derived dependency must remain a JSON-safe primitive")
1116
- return value
1117
- })`
1118
- }
1119
-
1120
- function printOwnedEffectEntry(imports, effects, entries) {
1121
- const hasItemDependencies = effects.some(effect => effect.itemDependencies?.length)
1122
- const hasOrdinaryDependencies = effects.some(effect => effect.dependencies?.length)
1123
- const hasDependencyExpressions = effects.some(effect => effect.dependencyExpressions?.length)
1124
- const hasRowState = effects.some(effect => JSON.stringify([effect.dependencies, effect.dependencyStates, effect.states, effect.scope]).includes("$k"))
1125
- return `${imports.join("\n")}
1126
- const effects = ${inlineJson(effects)}
1127
- const modules = new Map([${entries}])
1128
- ${hasItemDependencies ? "let order = 0\n" : ""}const records = effects.map((effect, index) => effect.list ? undefined : createRecord(effect, index)).filter(Boolean)
1129
- const listTemplates = new Map(effects.map((effect, index) => effect.list ? [effect.owner, { effect, index }] : undefined).filter(Boolean))
1130
- const owners = new Map(records.filter(record => record.effect.owner).map(record => [record.effect.owner, record]))
1131
- const listRegistrations = new WeakMap()
1132
- const mountedRecords = new Set(records.filter(record => record.mounted))
1133
- const dependencies = new Map()
1134
- const pending = new Set()
1135
- let scheduled = false
1136
- let flushing = false
1137
- let active = true
1138
- for (const record of records) registerDependencies(record)
1139
- function createRecord(effect, index${hasRowState ? ", marker" : ""}) {
1140
- return { effect: ${hasRowState ? "marker ? specializeRowEffect(effect, marker) : effect" : "effect"}, index, ${hasItemDependencies ? "order: order++, " : ""}mounted: !effect.owner, marker: undefined, version: 0, values: undefined, cleanup: undefined, disposal: undefined, token: undefined }
1141
- }
1142
- ${hasRowState ? `function specializeRowEffect(effect, marker) {
1143
- const path = marker.dataset.kRowPath
1144
- const id = value => typeof value === "string" ? value.replace("$k", path) : value
1145
- const capture = value => value?.type === "state" || value?.type === "setter" || value?.type === "ref" ? { ...value, id: id(value.id) } : value?.type === "array" ? { ...value, value: value.value.map(capture) } : value?.type === "object" ? { ...value, value: value.value.map(([key, entry]) => [key, capture(entry)]) } : value
1146
- return { ...effect, dependencies: effect.dependencies?.map(id), dependencyStates: effect.dependencyStates && Object.fromEntries(Object.entries(effect.dependencyStates).map(([name, value]) => [name, id(value)])), states: Object.fromEntries(Object.entries(effect.states).map(([name, value]) => [name, id(value)])), scope: Object.fromEntries(Object.entries(effect.scope).map(([name, value]) => [name, capture(value)])) }
1147
- }
1148
- ` : ""}
1149
- function registerDependencies(record) {
1150
- for (const id of record.effect.dependencies ?? []) {
1151
- const subscribers = dependencies.get(id) ?? new Set()
1152
- subscribers.add(record)
1153
- dependencies.set(id, subscribers)
1154
- }
1155
- }
1156
- function unregisterDependencies(record) {
1157
- for (const id of record.effect.dependencies ?? []) {
1158
- const subscribers = dependencies.get(id)
1159
- subscribers?.delete(record)
1160
- if (!subscribers?.size) dependencies.delete(id)
1161
- }
1162
- }
1163
- ${hasItemDependencies ? `if (${hasOrdinaryDependencies}) ` : ""}__kRuntime.registerCommitter(id => {
1164
- if (!active) return
1165
- for (const record of dependencies.get(id) ?? []) if (record.mounted) pending.add(record)
1166
- schedule()
1167
- })
1168
- ${hasItemDependencies ? `for (const listState of new Set(effects.filter(effect => effect.itemDependencies?.length).map(effect => effect.listState))) __kRuntime.registerListItemHook(listState, root => {
1169
- if (!active) return
1170
- for (const record of listRegistrations.get(root) ?? []) if (record.mounted && record.effect.itemDependencies) pending.add(record)
1171
- schedule()
1172
- })
1173
- ` : ""}__kRuntime.registerMountHook(root => {
1174
- if (!active) return
1175
- for (const marker of matching(root)) {
1176
- if (marker.dataset.kEffects) {
1177
- if (listRegistrations.has(marker)) continue
1178
- const rowRecords = JSON.parse(marker.dataset.kEffects).map(owner => {
1179
- const template = listTemplates.get(owner)
1180
- if (!template) throw new Error("Keyed row effect template was not emitted")
1181
- const record = createRecord(template.effect, template.index${hasRowState ? ", marker" : ""})
1182
- registerDependencies(record)
1183
- mount(record, marker)
1184
- return record
1185
- })
1186
- listRegistrations.set(marker, rowRecords)
1187
- continue
1188
- }
1189
- const record = owners.get(marker.dataset.kEffect)
1190
- if (!record?.mounted) mount(record, marker)
1191
- }
1192
- })
1193
- __kRuntime.registerUnmountHook(root => {
1194
- if (root === document) {
1195
- if (!active) return
1196
- active = false
1197
- pending.clear()
1198
- for (const record of [...mountedRecords]) unmount(record, record.effect.list)
1199
- return
1200
- }
1201
- for (const marker of matching(root)) {
1202
- const rowRecords = listRegistrations.get(marker)
1203
- if (rowRecords) {
1204
- for (const record of rowRecords) unmount(record, true)
1205
- listRegistrations.delete(marker)
1206
- continue
1207
- }
1208
- const record = owners.get(marker.dataset.kEffect)
1209
- if (record?.marker === marker) unmount(record)
1210
- }
1211
- })
1212
- for (const record of records) if (record.mounted) start(record)
1213
- __kRuntime.mountDom(document)
1214
- addEventListener("pagehide", event => {
1215
- if (!event.persisted) __kRuntime.unmountDom(document)
1216
- })
1217
- function matching(root) {
1218
- const selector = "template[data-k-effect],[data-k-effects]"
1219
- return [...(root.matches?.(selector) ? [root] : []), ...(root.querySelectorAll?.(selector) ?? [])]
1220
- }
1221
- function mount(record, marker) {
1222
- record.mounted = true
1223
- record.marker = marker
1224
- mountedRecords.add(record)
1225
- const version = ++record.version
1226
- const begin = () => {
1227
- if (!active || !record.mounted || record.version !== version || !marker.isConnected) return
1228
- start(record)
1229
- }
1230
- if (record.disposal) record.disposal.then(begin)
1231
- else begin()
1232
- }
1233
- function unmount(record, dynamic = false) {
1234
- if (!record.mounted) return
1235
- record.mounted = false
1236
- record.marker = undefined
1237
- mountedRecords.delete(record)
1238
- if (dynamic) unregisterDependencies(record)
1239
- record.version++
1240
- pending.delete(record)
1241
- invokeCleanup(record)
1242
- }
1243
- function start(record) {
1244
- try {
1245
- record.values = readDependencies(record)
1246
- invoke(record)
1247
- } catch (error) {
1248
- console.error(error)
1249
- }
1250
- }
1251
- function schedule() {
1252
- if (!pending.size || scheduled || flushing) return
1253
- scheduled = true
1254
- queueMicrotask(flush)
1255
- }
1256
- async function flush() {
1257
- scheduled = false
1258
- if (!active) return pending.clear()
1259
- flushing = true
1260
- try {
1261
- const selected = [...pending].filter(record => record.mounted).sort((left, right) => left.index - right.index${hasItemDependencies ? " || left.order - right.order" : ""})
1262
- pending.clear()
1263
- const changed = []
1264
- for (const record of selected) {
1265
- try {
1266
- const values = readDependencies(record)
1267
- if (!record.values || values.some((value, index) => !Object.is(value, record.values[index]))) changed.push([record, record.version])
1268
- } catch (error) {
1269
- console.error(error)
1270
- }
1271
- }
1272
- for (const [record] of changed) await invokeCleanup(record)
1273
- if (active) for (const [record, version] of changed) if (record.mounted && record.version === version) {
1274
- try {
1275
- record.values = readDependencies(record)
1276
- invoke(record)
1277
- } catch (error) {
1278
- record.values = undefined
1279
- console.error(error)
1280
- }
1281
- }
1282
- } finally {
1283
- flushing = false
1284
- if (active) schedule()
1285
- }
1286
- }
1287
- function readDependencies(record) {
1288
- ${hasDependencyExpressions ? printDerivedDependencyRead("browserState") : ""}
1289
- const values = (record.effect.dependencies ?? []).map(id => {
1290
- const value = browserState.get(id)
1291
- if (value !== null && typeof value !== "string" && typeof value !== "boolean" && !(typeof value === "number" && Number.isFinite(value) && !Object.is(value, -0))) throw new Error("useEffect() dependency state must remain a JSON-safe primitive")
1292
- return value
1293
- })
1294
- ${hasItemDependencies ? `if (record.effect.itemDependencies) {
1295
- const item = JSON.parse(record.marker.dataset.kEffectItem)
1296
- for (const field of record.effect.itemDependencies) {
1297
- const value = item[field]
1298
- if (value !== null && typeof value !== "string" && typeof value !== "boolean" && !(typeof value === "number" && Number.isFinite(value) && !Object.is(value, -0))) throw new Error(\`useEffect() keyed item dependency "\${field}" must remain a JSON-safe primitive\`)
1299
- values.push(value)
1300
- }
1301
- }` : ""}
1302
- return values
1303
- }
1304
- function invoke(record) {
1305
- const token = { active: true }
1306
- record.token = token
1307
- try {
1308
- const effect = record.effect
1309
- const scope = effect.list
1310
- ? Object.fromEntries(Object.entries(effect.scope).map(([name, value]) => [name, value?.type === "list-item" ? JSON.parse(record.marker.dataset.kEffectItem) : value]))
1311
- : effect.scope
1312
- const result = modules.get(effect.module)[effect.handler](createEffectContext(browserState, effect.states, commitDom, scope, () => active && token.active && record.token === token))
1313
- if (effect.cleanup && typeof result === "function") record.cleanup = result
1314
- else if (result && typeof result.then === "function") result.catch(error => console.error(error))
1315
- } catch (error) {
1316
- console.error(error)
1317
- }
1318
- }
1319
- function invokeCleanup(record) {
1320
- if (record.token) record.token.active = false
1321
- record.token = undefined
1322
- if (record.disposal) return record.disposal
1323
- const cleanup = record.cleanup
1324
- record.cleanup = undefined
1325
- if (!cleanup) return Promise.resolve()
1326
- const disposal = (async () => {
1327
- try {
1328
- await cleanup()
1329
- } catch (error) {
1330
- console.error(error)
1331
- }
1332
- })()
1333
- record.disposal = disposal
1334
- disposal.finally(() => {
1335
- if (record.disposal === disposal) record.disposal = undefined
1336
- })
1337
- return disposal
1338
- }`
1339
- }
1340
-
1341
- function printSingleDependencyEffect(imports, effect, hasCleanup) {
1342
- const disposal = hasCleanup ? `
1343
- const dispose = root => {
1344
- if (root !== document || !active) return
1345
- active = false
1346
- pending = false
1347
- invokeCleanup()
1348
- }
1349
- if (__kRuntime.registerUnmountHook) __kRuntime.registerUnmountHook(dispose)
1350
- addEventListener("pagehide", event => {
1351
- if (event.persisted) return
1352
- if (__kRuntime.unmountDom) __kRuntime.unmountDom(document)
1353
- else dispose(document)
1354
- })` : ""
1355
- return `${imports.join("\n")}
1356
- const effect = ${inlineJson(effect)}
1357
- const dependency = effect.dependencies[0]
1358
- let value
1359
- let cleanup
1360
- let token
1361
- let active = true
1362
- let pending = false
1363
- let scheduled = false
1364
- let running = false
1365
- __kRuntime.registerCommitter(id => {
1366
- if (active && id === dependency) {
1367
- pending = true
1368
- schedule()
1369
- }
1370
- })
1371
- try {
1372
- value = readDependency()
1373
- invoke()
1374
- } catch (error) {
1375
- console.error(error)
1376
- }
1377
- function schedule() {
1378
- if (!pending || scheduled || running) return
1379
- scheduled = true
1380
- queueMicrotask(flush)
1381
- }
1382
- async function flush() {
1383
- scheduled = false
1384
- if (!active) return
1385
- let next
1386
- try {
1387
- next = readDependency()
1388
- } catch (error) {
1389
- console.error(error)
1390
- return
1391
- }
1392
- pending = false
1393
- if (Object.is(next, value)) return
1394
- value = next
1395
- running = true
1396
- try {
1397
- await invokeCleanup()
1398
- if (active) invoke()
1399
- } finally {
1400
- running = false
1401
- if (active) schedule()
1402
- }
1403
- }
1404
- function readDependency() {
1405
- const next = browserState.get(dependency)
1406
- if (next !== null && typeof next !== "string" && typeof next !== "boolean" && !(typeof next === "number" && Number.isFinite(next) && !Object.is(next, -0))) throw new Error("useEffect() dependency state must remain a JSON-safe primitive")
1407
- return next
1408
- }
1409
- function invoke() {
1410
- try {
1411
- const current = { active: true }
1412
- token = current
1413
- const result = __kEffectModule0[effect.handler](createEffectContext(browserState, effect.states, commitDom, effect.scope, () => active && current.active && token === current))
1414
- if (effect.cleanup && typeof result === "function") cleanup = result
1415
- else if (result && typeof result.then === "function") result.catch(error => console.error(error))
1416
- } catch (error) {
1417
- console.error(error)
1418
- }
1419
- }
1420
- async function invokeCleanup() {
1421
- if (token) token.active = false
1422
- token = undefined
1423
- const current = cleanup
1424
- cleanup = undefined
1425
- if (!current) return
1426
- try {
1427
- await current()
1428
- } catch (error) {
1429
- console.error(error)
1430
- }
1431
- }${disposal}`
1432
- }
1433
-
1434
561
  function printParamEntry(schema, params, searchParams, searchParamsWritable, output, assetsDirectory, base, runtimeName, navigable) {
1435
562
  const hasSearch = searchParams.length || searchParamsWritable
1436
563
  const signature = hasSearch ? "pathname, search" : "pathname"
@@ -1500,20 +627,6 @@ addEventListener("popstate", () => ${searchParams.length ? "initializeSearch(loc
1500
627
  ${searchInitializer}${prefix}${pathname}${query}${suffix}${writer}`
1501
628
  }
1502
629
 
1503
- function hasCaptureType(value, type) {
1504
- if (!value || typeof value !== "object") return false
1505
- if (value.type === type) return true
1506
- return (Array.isArray(value) ? value : Object.values(value)).some(entry => hasCaptureType(entry, type))
1507
- }
1508
-
1509
- function hasNestedCaptureState(value, insideCapture = false) {
1510
- if (!value || typeof value !== "object") return false
1511
- if (value.type === "state") return insideCapture
1512
- if (value.type === "array") return value.value.some(entry => hasNestedCaptureState(entry, true))
1513
- if (value.type === "object") return value.value.some(([, entry]) => hasNestedCaptureState(entry, true))
1514
- return (Array.isArray(value) ? value : Object.values(value)).some(entry => hasNestedCaptureState(entry, false))
1515
- }
1516
-
1517
630
  export function specializeRuntime(source, events, hasStateSeed) {
1518
631
  const specialized = specializeEvents(source, events)
1519
632
  if (hasStateSeed) return specialized
@@ -1543,225 +656,17 @@ async function writeBundledJavaScript(file, source, minify, define) {
1543
656
  await writeFile(file, result.outputFiles[0].contents)
1544
657
  }
1545
658
 
1546
- export function parseDevPort(value) {
1547
- if (value === undefined || value.trim() === "") return 3000
1548
- if (!/^\d+$/.test(value)) throw new Error(`Invalid dev server port: ${value}`)
1549
- const port = Number(value)
1550
- if (port > 65535) throw new Error(`Invalid dev server port: ${value}`)
1551
- return port
1552
- }
1553
-
1554
- export function parseDevHost(value) {
1555
- return value?.trim() || "127.0.0.1"
1556
- }
1557
-
1558
659
  export async function dev({ port = parseDevPort(process.env.PORT), host = parseDevHost(process.env.HOST) } = {}) {
1559
660
  if (!Number.isInteger(port) || port < 0 || port > 65535) throw new Error(`Invalid dev server port: ${port}`)
1560
661
  if (typeof host !== "string" || !host.trim()) throw new Error(`Invalid dev server host: ${host}`)
1561
662
  const base = normalizeBase((await loadConfig()).base)
1562
-
1563
- let buildError
1564
- let revision = 0
1565
- const session = randomUUID()
1566
- try {
1567
- await build({ minify: false })
1568
- revision++
1569
- } catch (error) {
1570
- buildError = errorText(error)
1571
- console.error(error)
1572
- }
1573
-
1574
- const clients = new Set()
1575
-
1576
- const server = createServer(async (request, response) => {
1577
- try {
1578
- const url = new URL(request.url, "http://localhost")
1579
- const rawPathname = url.pathname
1580
- const pathname = decodeURIComponent(rawPathname)
1581
- if (pathname === "/__kudzu_reload") {
1582
- response.writeHead(200, {
1583
- "content-type": "text/event-stream; charset=utf-8",
1584
- "cache-control": "no-cache, no-transform",
1585
- connection: "keep-alive"
1586
- })
1587
- response.write(": connected\n\n")
1588
- clients.add(response)
1589
- request.on("close", () => clients.delete(response))
1590
- if (buildError) sendEvent(response, "build-error", buildError)
1591
- else if (url.searchParams.get("session") !== session || url.searchParams.get("revision") !== String(revision)) sendEvent(response, "reload")
1592
- return
1593
- }
1594
- if (pathname === "/__kudzu_dev.js") {
1595
- response.writeHead(200, { "content-type": "text/javascript; charset=utf-8", "cache-control": "no-store" })
1596
- response.end(await readFile(new URL("./dev-state.js", import.meta.url)))
1597
- return
1598
- }
1599
-
1600
- const relativePath = stripBaseStrict(pathname, decodeURIComponent(base)).replace(/^\/+/, "")
1601
- let file = resolve(outputDirectory, relativePath)
1602
- if (!file.startsWith(`${outputDirectory}${sep}`) && file !== outputDirectory) throw new Error("Invalid path")
1603
-
1604
- if ((await exists(file)) && (await stat(file)).isDirectory()) file = join(file, "index.html")
1605
- if (!(await exists(file)) && !extname(file)) file = join(file, "index.html")
1606
- let matchedRoute
1607
- if (!(await exists(file)) && !buildError) {
1608
- const plan = JSON.parse(await readFile(join(workDirectory, "kudzu-plan.json"), "utf8"))
1609
- const rewrite = plan.rewrites?.find(entry => runtimePathValues(rawPathname, entry, browserPath(base)))
1610
- if (rewrite) {
1611
- file = resolve(outputDirectory, rewrite.file)
1612
- matchedRoute = rewrite.pattern
1613
- }
1614
- }
1615
- const isHtml = extname(file) === ".html"
1616
- const content = isHtml
1617
- ? injectDevClient(buildError ? errorPage(buildError) : await readFile(file, "utf8"), session, revision, buildError ? [] : await devSchema(withBase(base, stripBaseStrict(pathname, decodeURIComponent(base))), matchedRoute))
1618
- : await readFile(file)
1619
- response.writeHead(200, {
1620
- "content-type": contentType(file),
1621
- "cache-control": "no-store"
1622
- })
1623
- response.end(content)
1624
- } catch {
1625
- response.writeHead(404, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" })
1626
- response.end("Not found")
1627
- }
1628
- })
1629
-
1630
- const listeningPort = await listenDevServer(server, port, host)
1631
- console.log(`Kudzu dev server: http://${host}:${listeningPort}`)
1632
-
1633
- let timer
1634
- let rebuilding = false
1635
- let pending = false
1636
- let changedFile
1637
- const rebuild = async () => {
1638
- if (rebuilding) {
1639
- pending = true
1640
- return
1641
- }
1642
- rebuilding = true
1643
- do {
1644
- pending = false
1645
- try {
1646
- await build({ quiet: true, minify: false })
1647
- buildError = undefined
1648
- revision++
1649
- console.log(`Rebuilt after ${changedFile ?? "source change"}`)
1650
- for (const client of clients) sendEvent(client, "reload")
1651
- } catch (error) {
1652
- buildError = errorText(error)
1653
- console.error(error)
1654
- for (const client of clients) sendEvent(client, "build-error", buildError)
1655
- }
1656
- } while (pending)
1657
- rebuilding = false
1658
- }
1659
- const watcher = watch(sourceDirectory, { recursive: true })
1660
- for await (const event of watcher) {
1661
- changedFile = event.filename
1662
- clearTimeout(timer)
1663
- timer = setTimeout(rebuild, 80)
1664
- }
1665
- }
1666
-
1667
- async function listenDevServer(server, port, host) {
1668
- let candidate = port
1669
- while (true) {
1670
- try {
1671
- await new Promise((resolve, reject) => {
1672
- const onError = error => {
1673
- server.off("listening", onListening)
1674
- reject(error)
1675
- }
1676
- const onListening = () => {
1677
- server.off("error", onError)
1678
- resolve()
1679
- }
1680
- server.once("error", onError)
1681
- server.once("listening", onListening)
1682
- server.listen(candidate, host)
1683
- })
1684
- return server.address().port
1685
- } catch (error) {
1686
- if (error.code !== "EADDRINUSE" || candidate === 0 || candidate === 65535) throw error
1687
- console.log(`Port ${candidate} is in use, trying ${candidate + 1}`)
1688
- candidate++
1689
- }
1690
- }
1691
- }
1692
-
1693
- function injectDevClient(html, session, revision, schema) {
1694
- return `${html}${devClient(session, revision, schema).replace("binding|list|native", "binding|deps|list|native")}`
1695
- }
1696
-
1697
- function stripBaseStrict(path, base) {
1698
- if (!base) return path
1699
- if (path === base) return "/"
1700
- if (path.startsWith(`${base}/`)) return path.slice(base.length)
1701
- throw new Error("Path is outside the configured base")
1702
- }
1703
-
1704
- async function devSchema(pathname, matchedRoute) {
1705
- try {
1706
- const plan = JSON.parse(await readFile(join(workDirectory, "kudzu-plan.json"), "utf8"))
1707
- const route = matchedRoute ?? (pathname.replace(/\/(?:index\.html)?$/, "") || "/")
1708
- return stateSchema(plan.routes.find(entry => entry.route === route)?.states ?? [])
1709
- } catch {
1710
- return []
1711
- }
1712
- }
1713
-
1714
- function runtimePathValues(pathname, rewrite, base) {
1715
- try {
1716
- let path = stripBrowserBase(pathname, base)
1717
- if (path.length > 1 && path.endsWith("/")) path = path.slice(0, -1)
1718
- const rawSegments = path.slice(1).split("/")
1719
- if (rawSegments.length !== rewrite.segments.length) return undefined
1720
- const values = Object.create(null)
1721
- for (let index = 0; index < rewrite.segments.length; index++) {
1722
- const segment = rewrite.segments[index]
1723
- const value = decodeRuntimeSegment(rawSegments[index], Boolean(segment.param))
1724
- if (segment.literal !== undefined && value !== segment.literal) return undefined
1725
- if (segment.param) values[segment.param] = value
1726
- }
1727
- return values
1728
- } catch {
1729
- return undefined
1730
- }
1731
- }
1732
-
1733
- function stripBrowserBase(path, base) {
1734
- if (!base) return path
1735
- const pathSegments = path.slice(1).split("/")
1736
- const baseSegments = base.slice(1).split("/").map(segment => decodeURIComponent(segment))
1737
- if (pathSegments.length < baseSegments.length || baseSegments.some((segment, index) => decodeRuntimeSegment(pathSegments[index], false) !== segment)) throw new Error("Path is outside the configured base")
1738
- return `/${pathSegments.slice(baseSegments.length).join("/")}`
1739
- }
1740
-
1741
- function decodeRuntimeSegment(raw, param) {
1742
- if (param && /%(?:2f|5c)/i.test(raw)) throw new Error("Encoded separator")
1743
- const value = decodeURIComponent(raw)
1744
- const decodedDots = value.replace(/%2e/gi, ".")
1745
- if (param && (!value || value === "." || value === ".." || decodedDots === "." || decodedDots === ".." || /[\\/?#]/.test(value) || [...value].some(character => character.charCodeAt(0) < 32 || character.charCodeAt(0) >= 127 && character.charCodeAt(0) <= 159) || /%(?:2f|5c)/i.test(value))) throw new Error("Invalid runtime parameter")
1746
- return value
663
+ return startDevServer({ build, port, host, base, sourceDirectory, workDirectory, outputDirectory })
1747
664
  }
1748
665
 
1749
666
  function inlineJson(value) {
1750
667
  return JSON.stringify(value).replaceAll("<", "\\u003c").replaceAll("\u2028", "\\u2028").replaceAll("\u2029", "\\u2029")
1751
668
  }
1752
669
 
1753
- function errorPage(error) {
1754
- return `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Kudzu build error</title></head><body><div id="__kudzu_error" role="alert" aria-live="assertive" style="position:fixed;inset:0;overflow:auto;padding:2rem;background:#200;color:#fff;font:16px/1.5 ui-monospace,monospace"><strong>Kudzu build error</strong><pre style="white-space:pre-wrap">${escapeHtml(error)}</pre></div></body></html>`
1755
- }
1756
-
1757
- function errorText(error) {
1758
- return String(error?.message ?? error)
1759
- }
1760
-
1761
- function sendEvent(response, event, data = "") {
1762
- response.write(`event: ${event}\n${String(data).replaceAll("\r", "").split("\n").map(line => `data: ${line}\n`).join("")}\n`)
1763
- }
1764
-
1765
670
  function escapeHtml(value) {
1766
671
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;")
1767
672
  }
@@ -1772,11 +677,7 @@ function escapeAttribute(value) {
1772
677
 
1773
678
  async function compile(file, sourceFiles, sourceIndex, staticFiles, importedAssets, cssModules, base, workerReferences) {
1774
679
  const source = sourceIndex.get(file)
1775
- const nativeHandlers = []
1776
- const effectHandlers = []
1777
- const reactiveBindings = []
1778
- const listExpressions = []
1779
- const clientImports = new Set()
680
+ const semantic = createSemanticArtifact()
1780
681
  const handlerPath = `handlers/${relative(sourceDirectory, file).replaceAll(sep, "/").replace(/\.(?:ts|tsx)$/, ".js")}`
1781
682
  const result = ts.transpileModule(source, {
1782
683
  fileName: file,
@@ -1786,7 +687,7 @@ async function compile(file, sourceFiles, sourceIndex, staticFiles, importedAsse
1786
687
  jsx: ts.JsxEmit.ReactJSX,
1787
688
  jsxImportSource: "@kudzujs/core"
1788
689
  },
1789
- transformers: { before: [createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings, listExpressions, assetPath(base, `assets/${handlerPath}`), file, sourceFiles, sourceIndex, staticFiles, importedAssets, cssModules, base, clientImports, workerReferences)] },
690
+ transformers: { before: [createKudzuTransformer({ semantic, handlerUrl: assetPath(base, `assets/${handlerPath}`), file, sourceFiles, sourceIndex, staticFiles, importedAssets, cssModules, base, workerReferences })] },
1790
691
  reportDiagnostics: true
1791
692
  })
1792
693
 
@@ -1801,14 +702,10 @@ async function compile(file, sourceFiles, sourceIndex, staticFiles, importedAsse
1801
702
  await mkdir(resolve(output, ".."), { recursive: true })
1802
703
  await writeFile(output, result.outputText)
1803
704
 
705
+ const { nativeHandlers, effectHandlers, reactiveBindings, listExpressions, clientImports } = semantic
1804
706
  if (!nativeHandlers.length && !effectHandlers.length && !reactiveBindings.length && !listExpressions.length) return undefined
1805
707
  const callbacks = [...nativeHandlers, ...effectHandlers]
1806
- const moduleSource = [
1807
- printClientImports([...callbacks, ...reactiveBindings].flatMap(handler => handler.imports ?? []), handlerPath),
1808
- ...callbacks.map(handler => printNativeHandler(handler)),
1809
- ...reactiveBindings.map(entry => printReactiveBinding(entry)),
1810
- ...listExpressions.map(entry => printListExpression(entry))
1811
- ].join("\n")
708
+ const moduleSource = printHandlerModule({ callbacks, reactiveBindings, listExpressions, handlerPath })
1812
709
  const moduleResult = ts.transpileModule(moduleSource, {
1813
710
  compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext },
1814
711
  reportDiagnostics: true
@@ -1840,259 +737,18 @@ function reachableSourceFiles(entries, sourceFiles, sourceIndex) {
1840
737
  const sourceFile = parseSourceFile(file, sourceIndex.get(file))
1841
738
  const visit = node => {
1842
739
  const specifier = (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && runtimeModuleReference(node) && node.moduleSpecifier
1843
- if (specifier && ts.isStringLiteral(specifier) && specifier.text.startsWith(".") && !isStaticImport(specifier.text)) {
1844
- try { queue.push(resolveSourceImport(file, specifier.text, sourceFiles)) } catch {}
1845
- }
1846
- const worker = relativeWorkerCandidate(node, sourceFile)
1847
- if (worker && ts.isStringLiteral(worker.url.arguments[0]) && worker.url.arguments[0].text.endsWith(".worker.ts")) {
1848
- try { queue.push(resolveSourceImport(file, worker.url.arguments[0].text, sourceFiles)) } catch {}
1849
- }
1850
- ts.forEachChild(node, visit)
1851
- }
1852
- visit(sourceFile)
1853
- }
1854
- return [...reachable].sort()
1855
- }
1856
-
1857
- function normalizeReactRouterSyntax(sourceFile, factory, context, base) {
1858
- const links = new Set()
1859
- const params = new Set()
1860
- const searchHooks = new Set()
1861
- const navigateHooks = new Set()
1862
- for (const statement of sourceFile.statements) {
1863
- if ((ts.isExportDeclaration(statement) || ts.isImportDeclaration(statement)) && statement.moduleSpecifier && ts.isStringLiteral(statement.moduleSpecifier) && statement.moduleSpecifier.text === "react-router-dom") {
1864
- if (ts.isExportDeclaration(statement)) throw sourceNodeError(statement, sourceFile, "React Router exports are not supported; import Link directly where it renders")
1865
- const clause = statement.importClause
1866
- if (clause?.isTypeOnly) continue
1867
- if (!clause) throw sourceNodeError(statement, sourceFile, "Side-effect React Router imports are not supported")
1868
- if (clause.name) throw sourceNodeError(clause.name, sourceFile, "React Router default imports are not supported; use named Link, useParams, useSearchParams, or useNavigate imports")
1869
- const bindings = clause.namedBindings
1870
- if (!bindings || ts.isNamespaceImport(bindings)) throw sourceNodeError(bindings ?? statement, sourceFile, "React Router namespace imports are not supported; use named Link, useParams, useSearchParams, or useNavigate imports")
1871
- for (const entry of bindings.elements) {
1872
- if (entry.isTypeOnly) continue
1873
- const imported = (entry.propertyName ?? entry.name).text
1874
- if (imported === "NavLink") throw sourceNodeError(entry, sourceFile, "React Router NavLink active-route semantics cannot be erased to a native anchor")
1875
- if (imported === "Link") links.add(entry.name.text)
1876
- else if (imported === "useParams") params.add(entry.name.text)
1877
- else if (imported === "useSearchParams") searchHooks.add(entry.name.text)
1878
- else if (imported === "useNavigate") navigateHooks.add(entry.name.text)
1879
- else throw sourceNodeError(entry, sourceFile, `React Router ${imported} is not supported; only named Link, useParams, useSearchParams, and useNavigate imports can be lowered`)
1880
- }
1881
- }
1882
- }
1883
- if (!links.size && !params.size && !searchHooks.size && !navigateHooks.size) return sourceFile
1884
-
1885
- let searchHelper = "__kUseSearchParam"
1886
- while (sourceFile.text.includes(searchHelper)) searchHelper += "_"
1887
- let searchWriterHelper = "__kUseSearchParamsWriter"
1888
- while (sourceFile.text.includes(searchWriterHelper)) searchWriterHelper += "_"
1889
- const searchDeclarations = new Set()
1890
- const searchReads = new Map()
1891
- const searchWrites = new Map()
1892
- const searchObjects = []
1893
- const collectSearchHooks = node => {
1894
- if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && searchHooks.has(node.expression.text) && !isShadowedIdentifier(node.expression, sourceFile)) {
1895
- const declaration = node.parent
1896
- const statement = declaration?.parent?.parent
1897
- const owner = nearestFunction(node)
1898
- const first = ts.isVariableDeclaration(declaration) && ts.isArrayBindingPattern(declaration.name) ? declaration.name.elements[0] : undefined
1899
- const second = ts.isVariableDeclaration(declaration) && ts.isArrayBindingPattern(declaration.name) ? declaration.name.elements[1] : undefined
1900
- if (node.questionDotToken || node.arguments.length || node.typeArguments?.length || !ts.isVariableDeclaration(declaration) || declaration.initializer !== node || !isLocalConst(declaration) || !owner || statement?.parent !== owner.body || !ts.isBindingElement(first) || !ts.isIdentifier(first.name) || declaration.name.elements.length > 2 || second && (!ts.isBindingElement(second) || !ts.isIdentifier(second.name))) {
1901
- throw sourceNodeError(node, sourceFile, "React Router useSearchParams must initialize one top-level const [params] or [params, setParams] binding")
1902
- }
1903
- const entry = { name: first.name.text, setter: second?.name.text, declaration, statement, owner }
1904
- searchDeclarations.add(declaration)
1905
- searchObjects.push(entry)
1906
- }
1907
- ts.forEachChild(node, collectSearchHooks)
1908
- }
1909
- collectSearchHooks(sourceFile)
1910
- const localBindingShadowed = (node, entry, name = entry.name) => {
1911
- for (let current = node.parent; current && current !== entry.owner; current = current.parent) {
1912
- if (isFunctionLike(current) && (current.parameters.some(parameter => bindingNames(parameter.name).includes(name)) || functionVarDeclaresName(current, name))) return true
1913
- if (ts.isBlock(current) && current.statements.some(statement => statement !== entry.statement && statementDeclaresName(statement, name))) return true
1914
- if (ts.isCaseBlock(current) && current.clauses.some(clause => clause.statements.some(statement => statement !== entry.statement && statementDeclaresName(statement, name)))) return true
1915
- if (ts.isCatchClause(current) && current.variableDeclaration && bindingNames(current.variableDeclaration.name).includes(name)) return true
1916
- if ((ts.isForStatement(current) || ts.isForInStatement(current) || ts.isForOfStatement(current)) && loopDeclaresName(current, name)) return true
1917
- }
1918
- return false
1919
- }
1920
- for (const entry of searchObjects) {
1921
- const collectReads = node => {
1922
- if (ts.isIdentifier(node) && node.text === entry.name && isReferenceIdentifier(node) && !localBindingShadowed(node, entry)) {
1923
- if (entry.setter && ts.isCallExpression(node.parent) && node.parent.arguments.includes(node) && ts.isIdentifier(node.parent.expression) && node.parent.expression.text === entry.setter) return
1924
- const property = node.parent
1925
- const call = property?.parent
1926
- const declaration = call?.parent
1927
- const statement = declaration?.parent?.parent
1928
- if (!ts.isPropertyAccessExpression(property) || property.expression !== node || property.name.text !== "get" || !ts.isCallExpression(call) || call.expression !== property || call.questionDotToken || call.typeArguments?.length || call.arguments.length !== 1 || !ts.isStringLiteral(call.arguments[0])) {
1929
- throw sourceNodeError(node, sourceFile, 'React Router search parameters only support direct get("static-name") reads')
1930
- }
1931
- if (!ts.isVariableDeclaration(declaration) || declaration.initializer !== call || !ts.isIdentifier(declaration.name) || !isLocalConst(declaration) || statement?.parent !== entry.owner.body) {
1932
- throw sourceNodeError(call, sourceFile, "React Router search parameter get() must directly initialize one top-level const identifier")
1933
- }
1934
- searchReads.set(call, call.arguments[0])
1935
- return
1936
- }
1937
- ts.forEachChild(node, collectReads)
1938
- }
1939
- collectReads(entry.owner.body)
1940
- if (!entry.setter) continue
1941
- const collectWrites = node => {
1942
- if (ts.isIdentifier(node) && node.text === entry.setter && isReferenceIdentifier(node) && !localBindingShadowed(node, entry, entry.setter)) {
1943
- const call = node.parent
1944
- if (!ts.isCallExpression(call) || call.expression !== node || call.questionDotToken || call.typeArguments?.length || nearestFunction(call) === entry.owner) throw sourceNodeError(node, sourceFile, "React Router search parameter setters may only be called directly from a nested browser callback")
1945
- if (call.arguments.length < 1 || call.arguments.length > 2) throw sourceNodeError(call, sourceFile, "React Router search parameter setters require one inline updater and optional { replace: true }")
1946
- const updater = unwrapExpression(call.arguments[0])
1947
- if ((!ts.isArrowFunction(updater) && !ts.isFunctionExpression(updater)) || updater.asteriskToken || updater.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword) || updater.parameters.length !== 1 || !ts.isIdentifier(updater.parameters[0].name)) throw sourceNodeError(call.arguments[0], sourceFile, "React Router search parameter setters require one synchronous inline updater with one identifier parameter")
1948
- let replace = false
1949
- if (call.arguments.length === 2) {
1950
- const options = unwrapExpression(call.arguments[1])
1951
- const property = ts.isObjectLiteralExpression(options) && options.properties.length === 1 ? options.properties[0] : undefined
1952
- const name = property && ts.isPropertyAssignment(property) && !ts.isComputedPropertyName(property.name) && (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) ? property.name.text : undefined
1953
- if (name !== "replace" || property.initializer.kind !== ts.SyntaxKind.TrueKeyword) throw sourceNodeError(call.arguments[1], sourceFile, "React Router search parameter setters only support exactly { replace: true } as a second argument")
1954
- replace = true
1955
- }
1956
- searchWrites.set(call, { updater, replace })
1957
- return
1958
- }
1959
- ts.forEachChild(node, collectWrites)
1960
- }
1961
- collectWrites(entry.owner.body)
1962
- }
1963
-
1964
- const navigateDeclarations = new Set()
1965
- const navigateCalls = new Map()
1966
- const navigateFunctions = []
1967
- const collectNavigateHooks = node => {
1968
- if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && navigateHooks.has(node.expression.text) && !isShadowedIdentifier(node.expression, sourceFile)) {
1969
- const declaration = node.parent
1970
- const statement = declaration?.parent?.parent
1971
- const owner = nearestFunction(node)
1972
- if (node.questionDotToken || node.arguments.length || node.typeArguments?.length || !ts.isVariableDeclaration(declaration) || declaration.initializer !== node || !ts.isIdentifier(declaration.name) || !isLocalConst(declaration) || !owner || statement?.parent !== owner.body) {
1973
- throw sourceNodeError(node, sourceFile, "React Router useNavigate must initialize one top-level const identifier in a component")
1974
- }
1975
- const entry = { name: declaration.name.text, declaration, statement, owner }
1976
- navigateDeclarations.add(declaration)
1977
- navigateFunctions.push(entry)
1978
- }
1979
- ts.forEachChild(node, collectNavigateHooks)
1980
- }
1981
- collectNavigateHooks(sourceFile)
1982
- for (const entry of navigateFunctions) {
1983
- const collectCalls = node => {
1984
- if (ts.isIdentifier(node) && node.text === entry.name && isReferenceIdentifier(node) && !localBindingShadowed(node, entry)) {
1985
- const call = node.parent
1986
- if (!ts.isCallExpression(call) || call.expression !== node || call.questionDotToken || call.typeArguments?.length || nearestFunction(call) === entry.owner) {
1987
- throw sourceNodeError(node, sourceFile, "React Router navigate bindings may only be called directly from a nested browser callback")
1988
- }
1989
- if (call.arguments.length < 1 || call.arguments.length > 2 || !ts.isStringLiteral(call.arguments[0])) {
1990
- throw sourceNodeError(call, sourceFile, 'React Router useNavigate requires a static root-relative navigate("/path") destination')
1991
- }
1992
- const destination = call.arguments[0].text
1993
- const pathname = destination.match(/^[^?#]*/)[0]
1994
- let decoded
1995
- try { decoded = decodeURIComponent(pathname) } catch { throw sourceNodeError(call.arguments[0], sourceFile, 'React Router useNavigate requires a safe static root-relative navigate("/path") destination') }
1996
- if (!destination.startsWith("/") || destination.startsWith("//") || /%(?:2f|5c)/i.test(pathname) || /[\\\0]/.test(decoded) || decoded.split("/").includes("..") || [...decoded].some(character => character.charCodeAt(0) < 32 || character.charCodeAt(0) >= 127 && character.charCodeAt(0) <= 159)) throw sourceNodeError(call.arguments[0], sourceFile, 'React Router useNavigate requires a safe static root-relative navigate("/path") destination')
1997
- let method = "assign"
1998
- if (call.arguments.length === 2) {
1999
- const options = unwrapExpression(call.arguments[1])
2000
- const property = ts.isObjectLiteralExpression(options) && options.properties.length === 1 ? options.properties[0] : undefined
2001
- const name = property && ts.isPropertyAssignment(property) && !ts.isComputedPropertyName(property.name) && (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) ? property.name.text : undefined
2002
- if (name !== "replace" || property.initializer.kind !== ts.SyntaxKind.TrueKeyword) throw sourceNodeError(call.arguments[1], sourceFile, 'React Router useNavigate only supports exactly { replace: true } as a second argument')
2003
- method = "replace"
2004
- }
2005
- navigateCalls.set(call, { method, destination: withBase(base, destination) })
2006
- return
2007
- }
2008
- ts.forEachChild(node, collectCalls)
2009
- }
2010
- collectCalls(entry.owner.body)
2011
- }
2012
-
2013
- const routerProps = new Set(["discover", "end", "prefetch", "preventScrollReset", "relative", "reloadDocument", "replace", "state", "viewTransition"])
2014
- const attributes = attributesNode => {
2015
- const output = []
2016
- let destination
2017
- for (const property of attributesNode.properties) {
2018
- if (ts.isJsxSpreadAttribute(property)) throw sourceNodeError(property, sourceFile, "React Router Link does not support spread attributes during native anchor lowering")
2019
- const name = property.name.text
2020
- if (name === "href") throw sourceNodeError(property, sourceFile, "React Router Link must not declare href; Kudzu derives it from to")
2021
- if (routerProps.has(name)) throw sourceNodeError(property, sourceFile, `React Router Link prop ${JSON.stringify(name)} cannot be erased to a native anchor`)
2022
- if (name !== "to") {
2023
- output.push(ts.visitEachChild(property, visitor, context))
2024
- continue
740
+ if (specifier && ts.isStringLiteral(specifier) && specifier.text.startsWith(".") && !isStaticImport(specifier.text)) {
741
+ try { queue.push(resolveSourceImport(file, specifier.text, sourceFiles)) } catch {}
2025
742
  }
2026
- if (destination !== undefined) throw sourceNodeError(property, sourceFile, "React Router Link requires exactly one to attribute")
2027
- if (!property.initializer || !ts.isStringLiteral(property.initializer)) throw sourceNodeError(property, sourceFile, 'React Router Link requires a static root-relative to="/path"')
2028
- destination = property.initializer.text
2029
- const pathname = destination.match(/^[^?#]*/)[0]
2030
- let decoded
2031
- try { decoded = decodeURIComponent(pathname) } catch { throw sourceNodeError(property.initializer, sourceFile, 'React Router Link requires a safe static root-relative to="/path"') }
2032
- if (!destination.startsWith("/") || destination.startsWith("//") || /%(?:2f|5c)/i.test(pathname) || /[\\\0]/.test(decoded) || decoded.split("/").includes("..") || [...decoded].some(character => character.charCodeAt(0) < 32 || character.charCodeAt(0) >= 127 && character.charCodeAt(0) <= 159)) throw sourceNodeError(property.initializer, sourceFile, 'React Router Link requires a safe static root-relative to="/path"')
2033
- output.push(factory.createJsxAttribute(factory.createIdentifier("href"), factory.createStringLiteral(withBase(base, destination))))
2034
- }
2035
- if (destination === undefined) throw sourceNodeError(attributesNode.parent, sourceFile, "React Router Link requires exactly one static root-relative to attribute")
2036
- return factory.updateJsxAttributes(attributesNode, output)
2037
- }
2038
- const importedLink = tag => ts.isIdentifier(tag) && links.has(tag.text) && !isShadowedIdentifier(tag, sourceFile)
2039
- const visitor = node => {
2040
- if (ts.isVariableStatement(node) && node.declarationList.declarations.some(declaration => searchDeclarations.has(declaration) || navigateDeclarations.has(declaration))) {
2041
- const declarations = node.declarationList.declarations.flatMap(declaration => {
2042
- if (navigateDeclarations.has(declaration)) return []
2043
- if (!searchDeclarations.has(declaration)) return [ts.visitEachChild(declaration, visitor, context)]
2044
- const entry = searchObjects.find(candidate => candidate.declaration === declaration)
2045
- if (!entry?.setter) return []
2046
- return [factory.updateVariableDeclaration(declaration, declaration.name, declaration.exclamationToken, declaration.type, factory.createCallExpression(factory.createIdentifier(searchWriterHelper), undefined, []))]
2047
- })
2048
- if (!declarations.length) return undefined
2049
- return factory.updateVariableStatement(node, node.modifiers, factory.updateVariableDeclarationList(node.declarationList, declarations))
2050
- }
2051
- if (ts.isCallExpression(node) && searchReads.has(node)) return factory.createCallExpression(factory.createIdentifier(searchHelper), undefined, [searchReads.get(node)])
2052
- if (ts.isCallExpression(node) && searchWrites.has(node)) {
2053
- const { updater, replace } = searchWrites.get(node)
2054
- return factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("globalThis"), "__kSetSearchParams"), undefined, [ts.visitNode(updater, visitor), replace ? factory.createTrue() : factory.createFalse()])
2055
- }
2056
- if (ts.isCallExpression(node) && navigateCalls.has(node)) {
2057
- const { method, destination } = navigateCalls.get(node)
2058
- return factory.createCallExpression(factory.createPropertyAccessExpression(factory.createPropertyAccessExpression(factory.createIdentifier("globalThis"), "location"), method), undefined, [factory.createStringLiteral(destination)])
2059
- }
2060
- if (ts.isJsxElement(node) && importedLink(node.openingElement.tagName)) {
2061
- const opening = factory.updateJsxOpeningElement(node.openingElement, factory.createIdentifier("a"), node.openingElement.typeArguments, attributes(node.openingElement.attributes))
2062
- const closing = factory.updateJsxClosingElement(node.closingElement, factory.createIdentifier("a"))
2063
- return factory.updateJsxElement(node, opening, ts.visitNodes(node.children, visitor), closing)
2064
- }
2065
- if (ts.isJsxSelfClosingElement(node) && importedLink(node.tagName)) return factory.updateJsxSelfClosingElement(node, factory.createIdentifier("a"), node.typeArguments, attributes(node.attributes))
2066
- if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && params.has(node.expression.text) && !isShadowedIdentifier(node.expression, sourceFile)) {
2067
- if (node.questionDotToken || node.arguments.length || (node.typeArguments?.length ?? 0) > 1) throw sourceNodeError(node, sourceFile, "React Router useParams must be called directly without runtime arguments and with at most one type argument")
2068
- return node
2069
- }
2070
- if (ts.isIdentifier(node) && links.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, sourceFile)) throw sourceNodeError(node, sourceFile, "React Router Link imports may only be used as direct JSX elements")
2071
- if (ts.isIdentifier(node) && params.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, sourceFile)) throw sourceNodeError(node, sourceFile, "React Router useParams imports may only be called directly")
2072
- if (ts.isIdentifier(node) && searchHooks.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, sourceFile)) throw sourceNodeError(node, sourceFile, "React Router useSearchParams imports may only initialize the supported top-level tuple binding")
2073
- if (ts.isIdentifier(node) && navigateHooks.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, sourceFile)) throw sourceNodeError(node, sourceFile, "React Router useNavigate imports may only initialize the supported top-level navigate binding")
2074
- if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text === "react-router-dom") {
2075
- const clause = node.importClause
2076
- if (!clause || clause.isTypeOnly) return node
2077
- const bindings = clause.namedBindings
2078
- if (!bindings || !ts.isNamedImports(bindings)) return node
2079
- const elements = bindings.elements.filter(entry => entry.isTypeOnly || !["Link", "useParams", "useSearchParams", "useNavigate"].includes((entry.propertyName ?? entry.name).text))
2080
- if (!elements.length) return undefined
2081
- return factory.updateImportDeclaration(node, node.modifiers, factory.updateImportClause(clause, clause.isTypeOnly, undefined, factory.updateNamedImports(bindings, elements)), node.moduleSpecifier, node.attributes)
743
+ const worker = workerCompiler.candidate(node, sourceFile)
744
+ if (worker && ts.isStringLiteral(worker.url.arguments[0]) && worker.url.arguments[0].text.endsWith(".worker.ts")) {
745
+ try { queue.push(resolveSourceImport(file, worker.url.arguments[0].text, sourceFiles)) } catch {}
746
+ }
747
+ ts.forEachChild(node, visit)
2082
748
  }
2083
- return ts.visitEachChild(node, visitor, context)
749
+ visit(sourceFile)
2084
750
  }
2085
- const normalized = ts.visitNode(sourceFile, visitor)
2086
- if (!params.size && !searchHooks.size) return normalized
2087
- const imports = [
2088
- ...[...params].map(name => factory.createImportSpecifier(false, name === "useParams" ? undefined : factory.createIdentifier("useParams"), factory.createIdentifier(name))),
2089
- ...(searchReads.size ? [factory.createImportSpecifier(false, factory.createIdentifier("useSearchParam"), factory.createIdentifier(searchHelper))] : []),
2090
- ...(searchObjects.some(entry => entry.setter) ? [factory.createImportSpecifier(false, factory.createIdentifier("useSearchParamsWriter"), factory.createIdentifier(searchWriterHelper))] : [])
2091
- ]
2092
- const declaration = factory.createImportDeclaration(undefined, factory.createImportClause(false, undefined, factory.createNamedImports(imports)), factory.createStringLiteral("@kudzujs/core"))
2093
- const statements = [...normalized.statements]
2094
- statements.splice(statements.findLastIndex(statement => ts.isImportDeclaration(statement)) + 1, 0, declaration)
2095
- return factory.updateSourceFile(normalized, statements)
751
+ return [...reachable].sort()
2096
752
  }
2097
753
 
2098
754
  function normalizeClsxSyntax(sourceFile, factory, context) {
@@ -2142,360 +798,6 @@ function normalizeClsxSyntax(sourceFile, factory, context) {
2142
798
  return ts.visitNode(sourceFile, visitor)
2143
799
  }
2144
800
 
2145
- function analyzeZustandStores(sourceFile) {
2146
- const createNames = new Set()
2147
- for (const statement of sourceFile.statements) {
2148
- if (!ts.isImportDeclaration(statement) || statement.importClause?.isTypeOnly || !ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text !== "zustand") continue
2149
- const bindings = statement.importClause?.namedBindings
2150
- if (statement.importClause?.name || !bindings || !ts.isNamedImports(bindings)) throw sourceNodeError(statement, sourceFile, "Zustand migration input requires a named create import")
2151
- for (const entry of bindings.elements) {
2152
- if (entry.isTypeOnly) continue
2153
- if ((entry.propertyName ?? entry.name).text !== "create") throw sourceNodeError(entry, sourceFile, "Only Zustand create is supported")
2154
- createNames.add(entry.name.text)
2155
- }
2156
- }
2157
- const stores = new Map()
2158
- if (!createNames.size) return stores
2159
- for (const statement of sourceFile.statements) {
2160
- if (!ts.isVariableStatement(statement) || !statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword)) continue
2161
- for (const declaration of statement.declarationList.declarations) {
2162
- if (!ts.isIdentifier(declaration.name) || !declaration.initializer || !ts.isCallExpression(declaration.initializer) || !ts.isIdentifier(declaration.initializer.expression) || !createNames.has(declaration.initializer.expression.text)) continue
2163
- const callback = declaration.initializer.arguments[0]
2164
- if (declaration.initializer.arguments.length !== 1 || !callback || (!ts.isArrowFunction(callback) && !ts.isFunctionExpression(callback)) || callback.parameters.length !== 1 || !ts.isIdentifier(callback.parameters[0].name) || callback.asteriskToken || callback.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) throw sourceNodeError(declaration.initializer, sourceFile, "Zustand create() requires one synchronous initializer with one set parameter")
2165
- const body = unwrapExpression(callback.body)
2166
- if (!ts.isObjectLiteralExpression(body)) throw sourceNodeError(callback.body, sourceFile, "Zustand create() initializer must return one object literal")
2167
- const data = []
2168
- const actions = new Map()
2169
- for (const property of body.properties) {
2170
- if (!ts.isPropertyAssignment(property) || !property.name || !(ts.isIdentifier(property.name) || ts.isStringLiteral(property.name))) throw sourceNodeError(property, sourceFile, "Zustand store entries must be ordinary properties")
2171
- const name = property.name.text
2172
- const value = unwrapExpression(property.initializer)
2173
- if (ts.isArrowFunction(value) || ts.isFunctionExpression(value)) actions.set(name, value)
2174
- else data.push({ name, value })
2175
- }
2176
- if (data.length !== 1 || !isSerializableStateLiteral(data[0].value)) throw sourceNodeError(body, sourceFile, "Zustand migration stores require exactly one directly serializable data property")
2177
- if (!actions.size) throw sourceNodeError(body, sourceFile, "Zustand migration stores require at least one action")
2178
- for (const [name, action] of actions) {
2179
- if (action.asteriskToken || action.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) throw sourceNodeError(action, sourceFile, `Zustand action ${JSON.stringify(name)} must be synchronous`)
2180
- const capture = [...nativeCaptureNames(action, new Map())].find(entry => entry !== callback.parameters[0].name.text)
2181
- if (capture) throw sourceNodeError(action, sourceFile, `Zustand action ${JSON.stringify(name)} cannot capture ${JSON.stringify(capture)}`)
2182
- const validateAction = node => {
2183
- if (ts.isAwaitExpression(node) || ts.isYieldExpression(node) || ts.isNewExpression(node)) throw sourceNodeError(node, sourceFile, `Zustand action ${JSON.stringify(name)} must be synchronous`)
2184
- if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && ["then", "catch", "finally"].includes(node.expression.name.text)) throw sourceNodeError(node, sourceFile, `Zustand action ${JSON.stringify(name)} cannot schedule asynchronous updates`)
2185
- if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === callback.parameters[0].name.text && !isShadowedIdentifier(node.expression, action)) {
2186
- if (nearestFunction(node) !== action) throw sourceNodeError(node, sourceFile, `Zustand action ${JSON.stringify(name)} must call set directly`)
2187
- if (node.arguments.length !== 1) throw sourceNodeError(node, sourceFile, `Zustand action ${JSON.stringify(name)} set() requires exactly one partial update`)
2188
- }
2189
- ts.forEachChild(node, validateAction)
2190
- }
2191
- validateAction(action.body)
2192
- }
2193
- stores.set(declaration.name.text, { name: declaration.name.text, setName: callback.parameters[0].name.text, field: data[0].name, initialValue: data[0].value, actions, declaration })
2194
- }
2195
- }
2196
- const visit = node => {
2197
- const recognized = ts.isIdentifier(node) && ts.isCallExpression(node.parent) && node.parent.expression === node && [...stores.values()].some(store => store.declaration.initializer === node.parent)
2198
- if (ts.isIdentifier(node) && createNames.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, sourceFile) && !recognized) throw sourceNodeError(node, sourceFile, "Zustand create must directly initialize an exported const store")
2199
- ts.forEachChild(node, visit)
2200
- }
2201
- visit(sourceFile)
2202
- return stores
2203
- }
2204
-
2205
- function normalizeZustandMigrationSyntax(sourceFile, factory, context) {
2206
- const stores = analyzeZustandStores(sourceFile)
2207
- if (!stores.size) {
2208
- const declaration = sourceFile.statements.find(statement => ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier) && statement.moduleSpecifier.text === "zustand" && !statement.importClause?.isTypeOnly)
2209
- if (declaration) throw sourceNodeError(declaration, sourceFile, "Zustand create must directly initialize an exported const store")
2210
- return sourceFile
2211
- }
2212
- const identity = name => `${relative(sourceDirectory, sourceFile.fileName).replaceAll(sep, "/")}#${name}`
2213
- const visitor = node => {
2214
- if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && stores.has(node.name.text)) {
2215
- const store = stores.get(node.name.text)
2216
- return factory.updateVariableDeclaration(node, node.name, node.exclamationToken, node.type, factory.createCallExpression(factory.createIdentifier("__kCreateStore"), undefined, [
2217
- factory.createStringLiteral(identity(store.name)),
2218
- factory.createStringLiteral(store.field),
2219
- store.initialValue,
2220
- factory.createArrayLiteralExpression([...store.actions.keys()].map(name => factory.createStringLiteral(name)))
2221
- ]))
2222
- }
2223
- if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text === "zustand") return undefined
2224
- return ts.visitEachChild(node, visitor, context)
2225
- }
2226
- const normalized = ts.visitNode(sourceFile, visitor)
2227
- const declaration = factory.createImportDeclaration(undefined, factory.createImportClause(false, undefined, factory.createNamedImports([factory.createImportSpecifier(false, undefined, factory.createIdentifier("__kCreateStore"))])), factory.createStringLiteral("@kudzujs/core"))
2228
- const statements = [...normalized.statements]
2229
- statements.splice(statements.findLastIndex(statement => ts.isImportDeclaration(statement)) + 1, 0, declaration)
2230
- return factory.updateSourceFile(normalized, statements)
2231
- }
2232
-
2233
- function normalizeReactMigrationSyntax(sourceFile, factory, context, importedCollections = new Set()) {
2234
- const supported = new Set(["createContext", "useContext", "useEffect", "useId", "useReducer", "useRef", "useState"])
2235
- const erased = new Set(["forwardRef", "memo", "useCallback", "useMemo"])
2236
- const aliases = new Map()
2237
- const reactObjects = new Set()
2238
- for (const statement of sourceFile.statements) {
2239
- if (!ts.isImportDeclaration(statement) || statement.importClause?.isTypeOnly || !ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text !== "react") continue
2240
- if (statement.importClause?.name) reactObjects.add(statement.importClause.name.text)
2241
- const bindings = statement.importClause?.namedBindings
2242
- if (bindings && ts.isNamespaceImport(bindings)) reactObjects.add(bindings.name.text)
2243
- if (bindings && ts.isNamedImports(bindings)) for (const entry of bindings.elements) {
2244
- const imported = (entry.propertyName ?? entry.name).text
2245
- if (!entry.isTypeOnly && (supported.has(imported) || erased.has(imported))) aliases.set(entry.name.text, imported)
2246
- else if (!entry.isTypeOnly && /^use[A-Z]/.test(imported)) throw sourceNodeError(entry, sourceFile, `React ${imported} is not supported by Kudzu migration input`)
2247
- }
2248
- }
2249
- if (!aliases.size && !reactObjects.size) return sourceFile
2250
-
2251
- const migrationCallName = call => {
2252
- if (ts.isIdentifier(call.expression) && aliases.has(call.expression.text) && !isShadowedIdentifier(call.expression, sourceFile)) return aliases.get(call.expression.text)
2253
- if (ts.isPropertyAccessExpression(call.expression) && ts.isIdentifier(call.expression.expression) && reactObjects.has(call.expression.expression.text) && !isShadowedIdentifier(call.expression.expression, sourceFile)) return call.expression.name.text
2254
- return undefined
2255
- }
2256
- const ownerStateNames = owner => {
2257
- const names = new Set()
2258
- const collect = node => {
2259
- if (node !== owner && isFunctionLike(node)) return
2260
- if (ts.isVariableDeclaration(node) && ts.isArrayBindingPattern(node.name) && node.initializer && ts.isCallExpression(node.initializer) && ["useReducer", "useState"].includes(migrationCallName(node.initializer))) {
2261
- const state = node.name.elements[0]
2262
- if (state && ts.isBindingElement(state) && ts.isIdentifier(state.name)) names.add(state.name.text)
2263
- }
2264
- ts.forEachChild(node, collect)
2265
- }
2266
- collect(owner)
2267
- return names
2268
- }
2269
-
2270
- const validate = node => {
2271
- if (ts.isTypeNode(node)) return
2272
- if (ts.isIdentifier(node) && aliases.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, sourceFile) && !(ts.isCallExpression(node.parent) && node.parent.expression === node)) throw sourceNodeError(node, sourceFile, `Aliased React ${aliases.get(node.text)} must be called directly`)
2273
- if (ts.isIdentifier(node) && reactObjects.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, sourceFile) && !(ts.isPropertyAccessExpression(node.parent) && node.parent.expression === node)) throw sourceNodeError(node, sourceFile, "React default or namespace imports may only be used for direct supported members or React.Fragment")
2274
- if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.expression) && reactObjects.has(node.expression.text) && !isShadowedIdentifier(node.expression, sourceFile)) {
2275
- const name = node.name.text
2276
- if (name !== "Fragment" && !(ts.isCallExpression(node.parent) && node.parent.expression === node && (supported.has(name) || erased.has(name)))) throw sourceNodeError(node, sourceFile, `React.${name} is not supported; use a directly supported hook call or React.Fragment`)
2277
- }
2278
- ts.forEachChild(node, validate)
2279
- }
2280
- validate(sourceFile)
2281
-
2282
- const memoLocals = new Map()
2283
- const collectMemoLocals = node => {
2284
- if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isCallExpression(node.initializer) && migrationCallName(node.initializer) === "useMemo") {
2285
- if (!isLocalConst(node)) throw sourceNodeError(node, sourceFile, "React useMemo() local values must use const declarations")
2286
- const callback = node.initializer.arguments[0]
2287
- if (callback && (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback))) {
2288
- const expression = lowerReactMemoCollectionExpression(reactMemoExpression(callback), factory)
2289
- const owner = nearestFunction(node)
2290
- if (owner && expression) {
2291
- const entries = memoLocals.get(owner) ?? new Map()
2292
- if (entries.has(node.name.text)) throw sourceNodeError(node.name, sourceFile, `React useMemo() local ${JSON.stringify(node.name.text)} must be unique within its component`)
2293
- entries.set(node.name.text, { declaration: node, expression })
2294
- memoLocals.set(owner, entries)
2295
- }
2296
- }
2297
- }
2298
- ts.forEachChild(node, collectMemoLocals)
2299
- }
2300
- collectMemoLocals(sourceFile)
2301
- const memoLocalIsShadowed = (node, owner, entry) => {
2302
- if (isShadowedByParameter(node, owner)) return true
2303
- const declarationStatement = entry.declaration.parent?.parent
2304
- for (let current = node.parent; current && current !== owner; current = current.parent) {
2305
- if (ts.isFunctionExpression(current) && current.name?.text === node.text) return true
2306
- if (ts.isBlock(current) && current.statements.some(statement => statement !== declarationStatement && statementDeclaresName(statement, node.text))) return true
2307
- if (ts.isCaseBlock(current) && current.clauses.some(clause => clause.statements.some(statement => statement !== declarationStatement && statementDeclaresName(statement, node.text)))) return true
2308
- if (ts.isCatchClause(current) && current.variableDeclaration && bindingNames(current.variableDeclaration.name).includes(node.text)) return true
2309
- if ((ts.isForStatement(current) || ts.isForInStatement(current) || ts.isForOfStatement(current)) && loopDeclaresName(current, node.text)) return true
2310
- }
2311
- return false
2312
- }
2313
- for (const [owner, entries] of memoLocals) for (const [name, entry] of entries) {
2314
- const visit = node => {
2315
- if (ts.isIdentifier(node) && node.text === name && isReferenceIdentifier(node) && nearestFunctionLike(node) !== owner && !memoLocalIsShadowed(node, owner, entry)) throw sourceNodeError(node, sourceFile, `React useMemo() local ${JSON.stringify(name)} cannot be captured by a nested function`)
2316
- ts.forEachChild(node, visit)
2317
- }
2318
- visit(owner.body)
2319
- }
2320
-
2321
- const required = new Set()
2322
- const imported = new Set()
2323
- const visitor = node => {
2324
- if (ts.isVariableStatement(node)) {
2325
- const entries = memoLocals.get(nearestFunction(node))
2326
- if (entries) {
2327
- for (const declaration of node.declarationList.declarations) if (ts.isIdentifier(declaration.name) && entries.has(declaration.name.text) && declaration.initializer) ts.visitNode(declaration.initializer, visitor)
2328
- const declarations = node.declarationList.declarations.filter(declaration => !ts.isIdentifier(declaration.name) || !entries.has(declaration.name.text))
2329
- if (!declarations.length) return undefined
2330
- if (declarations.length !== node.declarationList.declarations.length) return factory.updateVariableStatement(node, node.modifiers, factory.updateVariableDeclarationList(node.declarationList, declarations.map(declaration => ts.visitEachChild(declaration, visitor, context))))
2331
- }
2332
- }
2333
- if (ts.isIdentifier(node) && isReferenceIdentifier(node)) {
2334
- const owner = nearestFunctionLike(node)
2335
- const entry = memoLocals.get(owner)?.get(node.text)
2336
- if (entry && !memoLocalIsShadowed(node, owner, entry)) return ts.visitNode(cloneAst(entry.expression, factory, context), visitor)
2337
- }
2338
- if (ts.isCallExpression(node)) {
2339
- const name = migrationCallName(node)
2340
- if (name === "forwardRef") return ts.visitNode(lowerReactForwardRef(node, sourceFile, factory), visitor)
2341
- if (name === "memo") {
2342
- if (node.arguments.length !== 1 || !(ts.isArrowFunction(node.arguments[0]) || ts.isFunctionExpression(node.arguments[0]) || ts.isIdentifier(node.arguments[0]))) throw sourceNodeError(node, sourceFile, "React memo() requires exactly one function component or component identifier")
2343
- if (ts.isIdentifier(node.arguments[0]) && isShadowedIdentifier(node.arguments[0], sourceFile)) throw sourceNodeError(node.arguments[0], sourceFile, "React memo() component identifiers must resolve to an unshadowed same-file top-level function")
2344
- const component = ts.isIdentifier(node.arguments[0]) ? reactMemoComponentExpression(node.arguments[0], sourceFile, factory, context) : node.arguments[0]
2345
- if (!component) throw sourceNodeError(node.arguments[0], sourceFile, "React memo() identifiers must name a same-file top-level function component")
2346
- return ts.visitNode(component, visitor)
2347
- }
2348
- if (name === "useCallback") {
2349
- if (node.arguments.length !== 2 || !ts.isArrayLiteralExpression(node.arguments[1]) || !(ts.isArrowFunction(node.arguments[0]) || ts.isFunctionExpression(node.arguments[0]))) throw sourceNodeError(node, sourceFile, "React useCallback() requires an inline function and a literal dependency array")
2350
- const dependency = node.arguments[1].elements.find(entry => !isReactCallbackDependency(entry))
2351
- if (dependency) throw sourceNodeError(dependency, sourceFile, "React useCallback() dependencies must be identifiers or primitive literals")
2352
- const dependencies = new Set(node.arguments[1].elements.map(unwrapExpression).filter(ts.isIdentifier).map(entry => entry.text))
2353
- const owner = nearestFunction(node)
2354
- const stale = owner && [...ownerStateNames(owner)].find(state => referenceIdentifiers(node.arguments[0], state).length && !dependencies.has(state))
2355
- if (stale) throw sourceNodeError(node.arguments[1], sourceFile, `React useCallback() must list captured state ${JSON.stringify(stale)} as a dependency`)
2356
- return ts.visitNode(node.arguments[0], visitor)
2357
- }
2358
- if (name === "useMemo") {
2359
- if (node.arguments.length !== 2 || !ts.isArrayLiteralExpression(node.arguments[1]) || !(ts.isArrowFunction(node.arguments[0]) || ts.isFunctionExpression(node.arguments[0]))) throw sourceNodeError(node, sourceFile, "React useMemo() requires an inline function and a literal dependency array")
2360
- const callback = node.arguments[0]
2361
- if (callback.parameters.length || callback.asteriskToken || callback.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) throw sourceNodeError(callback, sourceFile, "React useMemo() callback must be synchronous and parameterless")
2362
- const dependency = node.arguments[1].elements.find(entry => !isReactCallbackDependency(entry))
2363
- if (dependency) throw sourceNodeError(dependency, sourceFile, "React useMemo() dependencies must be identifiers or primitive literals")
2364
- const expression = lowerReactMemoCollectionExpression(reactMemoExpression(callback), factory)
2365
- const dependencies = new Set(node.arguments[1].elements.map(unwrapExpression).filter(ts.isIdentifier).map(entry => entry.text))
2366
- const owner = nearestFunction(node)
2367
- const states = owner ? ownerStateNames(owner) : new Set()
2368
- const collection = expression && reactMemoCollection(expression, states, importedCollections, sourceFile)
2369
- if (!expression || !collection && !isPureReactMemoExpression(expression)) throw sourceNodeError(callback.body, sourceFile, "React useMemo() callback must return one pure expression or analyzable collection pipeline")
2370
- if (!collection) {
2371
- const unsupported = [...reactMemoReferenceNames(expression)].find(reference => !states.has(reference))
2372
- if (unsupported) throw sourceNodeError(expression, sourceFile, `React useMemo() pure expressions may only reference direct local state; found ${JSON.stringify(unsupported)}`)
2373
- }
2374
- const collectionDependencies = collection ? new Set([...collection.selectorStates, ...(collection.static ? [] : [collection.state.text])]) : undefined
2375
- const stale = collection
2376
- ? [...collectionDependencies].find(state => !dependencies.has(state))
2377
- : [...states].find(state => referenceIdentifiers(expression, state).length && !dependencies.has(state))
2378
- if (stale) throw sourceNodeError(node.arguments[1], sourceFile, `React useMemo() must list captured state ${JSON.stringify(stale)} as a dependency`)
2379
- return ts.visitNode(expression, visitor)
2380
- }
2381
- if (name && supported.has(name)) {
2382
- required.add(name)
2383
- return factory.updateCallExpression(node, factory.createIdentifier(name), node.typeArguments, ts.visitNodes(node.arguments, visitor))
2384
- }
2385
- }
2386
- if (ts.isImportDeclaration(node) && !node.importClause?.isTypeOnly && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text === "react") {
2387
- const clause = node.importClause
2388
- if (!clause) return node
2389
- let bindings = clause.namedBindings
2390
- if (bindings && ts.isNamedImports(bindings)) {
2391
- const entries = []
2392
- for (const entry of bindings.elements) {
2393
- const name = (entry.propertyName ?? entry.name).text
2394
- if (entry.isTypeOnly) continue
2395
- if (!entry.isTypeOnly && erased.has(name)) continue
2396
- if (!entry.isTypeOnly && supported.has(name)) {
2397
- if (imported.has(name)) continue
2398
- imported.add(name)
2399
- required.add(name)
2400
- entries.push(factory.createImportSpecifier(false, undefined, factory.createIdentifier(name)))
2401
- } else {
2402
- entries.push(entry)
2403
- }
2404
- }
2405
- bindings = entries.length ? factory.updateNamedImports(bindings, entries) : undefined
2406
- }
2407
- if (!clause.name && !bindings) return undefined
2408
- return factory.updateImportDeclaration(node, node.modifiers, factory.updateImportClause(clause, clause.isTypeOnly, clause.name, bindings), node.moduleSpecifier, node.attributes)
2409
- }
2410
- return ts.visitEachChild(node, visitor, context)
2411
- }
2412
- let normalized = ts.visitNode(sourceFile, visitor)
2413
- const missing = [...required].filter(name => !imported.has(name)).sort()
2414
- if (!missing.length) return normalized
2415
- for (const name of missing) {
2416
- const collision = sourceFile.statements.some(statement => statementDeclaresName(statement, name) || ts.isImportDeclaration(statement) && importDeclarationNames(statement).includes(name) && statement.moduleSpecifier.text !== "react")
2417
- if (collision) throw sourceNodeError(sourceFile, sourceFile, `React.${name} cannot be normalized because ${JSON.stringify(name)} is already declared`)
2418
- }
2419
- const declaration = factory.createImportDeclaration(undefined, factory.createImportClause(false, undefined, factory.createNamedImports(missing.map(name => factory.createImportSpecifier(false, undefined, factory.createIdentifier(name))))), factory.createStringLiteral("react"))
2420
- const statements = [...normalized.statements]
2421
- const lastImport = statements.findLastIndex(statement => ts.isImportDeclaration(statement))
2422
- statements.splice(lastImport + 1, 0, declaration)
2423
- normalized = factory.updateSourceFile(normalized, statements)
2424
- return normalized
2425
- }
2426
-
2427
- function lowerReactForwardRef(call, sourceFile, factory) {
2428
- const declaration = call.parent
2429
- const statement = declaration?.parent?.parent
2430
- if (!ts.isVariableDeclaration(declaration) || declaration.initializer !== call || !ts.isIdentifier(declaration.name) || !statement || !ts.isVariableStatement(statement) || (statement.declarationList.flags & ts.NodeFlags.Const) === 0 || statement.parent !== sourceFile) {
2431
- throw sourceNodeError(call, sourceFile, "React forwardRef() must directly initialize one top-level const component")
2432
- }
2433
- if (call.arguments.length !== 1 || !ts.isArrowFunction(call.arguments[0]) && !ts.isFunctionExpression(call.arguments[0])) throw sourceNodeError(call, sourceFile, "React forwardRef() requires exactly one inline render function")
2434
- const callback = call.arguments[0]
2435
- if (callback.asteriskToken || callback.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) throw sourceNodeError(callback, sourceFile, "React forwardRef() render function must be synchronous and cannot be a generator")
2436
- if (callback.parameters.length !== 2) throw sourceNodeError(callback, sourceFile, "React forwardRef() render function must declare exactly (props, ref)")
2437
- const [props, ref] = callback.parameters
2438
- if (props.dotDotDotToken || props.initializer || !ts.isIdentifier(props.name) && !ts.isObjectBindingPattern(props.name)) throw sourceNodeError(props, sourceFile, "React forwardRef() props must use one identifier or a flat object binding")
2439
- if (ref.dotDotDotToken || ref.initializer || !ts.isIdentifier(ref.name)) throw sourceNodeError(ref, sourceFile, "React forwardRef() ref parameter must be one identifier")
2440
-
2441
- let elements
2442
- if (ts.isIdentifier(props.name)) {
2443
- elements = [
2444
- factory.createBindingElement(undefined, factory.createIdentifier("ref"), factory.createIdentifier(ref.name.text)),
2445
- factory.createBindingElement(factory.createToken(ts.SyntaxKind.DotDotDotToken), undefined, factory.createIdentifier(props.name.text))
2446
- ]
2447
- } else {
2448
- for (const element of props.name.elements) {
2449
- const property = (element.propertyName ?? element.name)
2450
- if (!ts.isIdentifier(element.name) || property.text === "ref") throw sourceNodeError(element, sourceFile, property.text === "ref" ? "React forwardRef() props must not declare ref; Kudzu supplies ref through the second parameter" : "React forwardRef() props must use one identifier or a flat object binding")
2451
- }
2452
- const rest = props.name.elements.findIndex(element => Boolean(element.dotDotDotToken))
2453
- elements = [...props.name.elements]
2454
- elements.splice(rest < 0 ? elements.length : rest, 0, factory.createBindingElement(undefined, factory.createIdentifier("ref"), factory.createIdentifier(ref.name.text)))
2455
- }
2456
-
2457
- const last = ts.isBlock(callback.body) ? callback.body.statements.at(-1) : undefined
2458
- let returnCount = 0
2459
- const countReturns = node => {
2460
- if (node !== callback.body && isFunctionLike(node)) return
2461
- if (ts.isReturnStatement(node)) returnCount++
2462
- ts.forEachChild(node, countReturns)
2463
- }
2464
- countReturns(callback.body)
2465
- const returned = ts.isBlock(callback.body)
2466
- ? last && ts.isReturnStatement(last) ? last.expression : undefined
2467
- : callback.body
2468
- const root = returned && unwrapExpression(returned)
2469
- const tag = root && jsxTagName(root)
2470
- if ((ts.isBlock(callback.body) && returnCount !== 1) || !root || !ts.isJsxElement(root) && !ts.isJsxSelfClosingElement(root) || !ts.isIdentifier(tag) || tag.text[0] !== tag.text[0].toLowerCase()) throw sourceNodeError(callback.body, sourceFile, "React forwardRef() render function must directly return one intrinsic JSX element")
2471
- const attributes = ts.isJsxElement(root) ? root.openingElement.attributes : root.attributes
2472
- const forwarded = attributes.properties.filter(attribute => ts.isJsxAttribute(attribute) && attribute.name.text === "ref" && ts.isJsxExpression(attribute.initializer) && ts.isIdentifier(attribute.initializer.expression) && attribute.initializer.expression.text === ref.name.text)
2473
- if (forwarded.length !== 1 || referenceIdentifiers(callback.body, ref.name.text).length !== 1) throw sourceNodeError(ref, sourceFile, "React forwardRef() ref must be forwarded exactly once as ref={ref} on the direct intrinsic root")
2474
-
2475
- const parameter = factory.updateParameterDeclaration(props, props.modifiers, undefined, factory.createObjectBindingPattern(elements), props.questionToken, props.type, undefined)
2476
- return ts.isArrowFunction(callback)
2477
- ? factory.updateArrowFunction(callback, callback.modifiers, callback.typeParameters, [parameter], callback.type, callback.equalsGreaterThanToken, callback.body)
2478
- : factory.updateFunctionExpression(callback, callback.modifiers, undefined, callback.name, callback.typeParameters, [parameter], callback.type, callback.body)
2479
- }
2480
-
2481
- function validateUseIdSyntax(sourceFile) {
2482
- const imported = 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 === "useId"))
2483
- if (!imported) return
2484
- const visit = node => {
2485
- if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "useId" && !isShadowedIdentifier(node.expression, sourceFile)) {
2486
- if (node.arguments.length) throw sourceNodeError(node, sourceFile, "useId() does not accept arguments")
2487
- const declaration = node.parent
2488
- const statement = declaration?.parent?.parent
2489
- const owner = nearestFunction(node)
2490
- if (!ts.isVariableDeclaration(declaration) || declaration.initializer !== node || !ts.isIdentifier(declaration.name) || !statement || !ts.isVariableStatement(statement) || (statement.declarationList.flags & ts.NodeFlags.Const) === 0 || !owner || !ts.isBlock(owner.body) || statement.parent !== owner.body) {
2491
- throw sourceNodeError(node, sourceFile, "useId() must be assigned to one top-level const identifier in a component")
2492
- }
2493
- }
2494
- ts.forEachChild(node, visit)
2495
- }
2496
- visit(sourceFile)
2497
- }
2498
-
2499
801
  function normalizeLazyStateInitializers(sourceFile, factory, context, file, sourceFiles, sourceIndex) {
2500
802
  const bindings = new Set()
2501
803
  for (const statement of sourceFile.statements) {
@@ -2544,519 +846,56 @@ function normalizeLazyStateInitializers(sourceFile, factory, context, file, sour
2544
846
  return ts.visitNode(sourceFile, visitor)
2545
847
  }
2546
848
 
2547
- function importDeclarationNames(statement) {
2548
- const names = []
2549
- if (statement.importClause?.name) names.push(statement.importClause.name.text)
2550
- const bindings = statement.importClause?.namedBindings
2551
- if (bindings && ts.isNamespaceImport(bindings)) names.push(bindings.name.text)
2552
- if (bindings && ts.isNamedImports(bindings)) for (const entry of bindings.elements) names.push(entry.name.text)
2553
- return names
2554
- }
2555
-
2556
- function isReactCallbackDependency(node) {
2557
- node = unwrapExpression(node)
2558
- return ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node) || node.kind === ts.SyntaxKind.TrueKeyword || node.kind === ts.SyntaxKind.FalseKeyword || node.kind === ts.SyntaxKind.NullKeyword
2559
- }
2560
-
2561
- function reactMemoExpression(callback) {
2562
- if (!ts.isBlock(callback.body)) return callback.body
2563
- if (callback.body.statements.length !== 1 || !ts.isReturnStatement(callback.body.statements[0])) return undefined
2564
- return callback.body.statements[0].expression
2565
- }
2566
-
2567
- function lowerReactMemoCollectionExpression(expression, factory) {
2568
- if (!expression) return undefined
2569
- const visit = node => {
2570
- if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "map" && node.arguments.length === 1) {
2571
- return factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("Array"), "from"), undefined, [visit(node.expression.expression), node.arguments[0]])
2572
- }
2573
- if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && ["filter", "flatMap", "slice", "toSorted"].includes(node.expression.name.text)) {
2574
- return factory.updateCallExpression(node, factory.updatePropertyAccessExpression(node.expression, visit(node.expression.expression), node.expression.name), node.typeArguments, node.arguments)
2575
- }
2576
- if (isArrayFromCall(node)) return factory.updateCallExpression(node, node.expression, node.typeArguments, [visit(node.arguments[0]), ...node.arguments.slice(1)])
2577
- return node
2578
- }
2579
- return visit(expression)
2580
- }
2581
-
2582
- function reactMemoCollection(expression, states, importedCollections, sourceFile) {
2583
- const setters = new Map([...states].map(state => [state, state]))
2584
- const fail = (node, message) => { throw sourceNodeError(node, sourceFile, message) }
2585
- return renderedCollectionSource(expression, setters, undefined, fail, new Set(), importedCollections, states)
2586
- }
2587
-
2588
- function reactMemoComponentExpression(identifier, sourceFile, factory, context) {
2589
- for (const statement of sourceFile.statements) {
2590
- if (ts.isFunctionDeclaration(statement) && statement.name?.text === identifier.text && statement.body) {
2591
- const clone = cloneAst(statement, factory, context)
2592
- return factory.createFunctionExpression(clone.modifiers?.filter(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword), clone.asteriskToken, clone.name, clone.typeParameters, clone.parameters, clone.type, clone.body)
2593
- }
2594
- if (!ts.isVariableStatement(statement)) continue
2595
- const declaration = statement.declarationList.declarations.find(entry => ts.isIdentifier(entry.name) && entry.name.text === identifier.text)
2596
- if (declaration?.initializer && (ts.isArrowFunction(declaration.initializer) || ts.isFunctionExpression(declaration.initializer))) return cloneAst(declaration.initializer, factory, context)
2597
- }
2598
- return undefined
2599
- }
2600
-
2601
- function isPureReactMemoExpression(node) {
2602
- node = unwrapExpression(node)
2603
- if (ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) || node.kind === ts.SyntaxKind.TrueKeyword || node.kind === ts.SyntaxKind.FalseKeyword || node.kind === ts.SyntaxKind.NullKeyword) return true
2604
- if (ts.isParenthesizedExpression(node)) return isPureReactMemoExpression(node.expression)
2605
- if (ts.isPrefixUnaryExpression(node)) return ![ts.SyntaxKind.PlusPlusToken, ts.SyntaxKind.MinusMinusToken].includes(node.operator) && isPureReactMemoExpression(node.operand)
2606
- if (ts.isBinaryExpression(node)) return node.operatorToken.kind < ts.SyntaxKind.FirstAssignment && isPureReactMemoExpression(node.left) && isPureReactMemoExpression(node.right)
2607
- if (ts.isConditionalExpression(node)) return isPureReactMemoExpression(node.condition) && isPureReactMemoExpression(node.whenTrue) && isPureReactMemoExpression(node.whenFalse)
2608
- if (ts.isTemplateExpression(node)) return node.templateSpans.every(span => isPureReactMemoExpression(span.expression))
2609
- return false
2610
- }
2611
-
2612
- function reactMemoReferenceNames(root) {
2613
- const names = new Set()
2614
- const visit = node => {
2615
- if (ts.isIdentifier(node) && isReferenceIdentifier(node)) names.add(node.text)
2616
- ts.forEachChild(node, visit)
2617
- }
2618
- visit(root)
2619
- return names
2620
- }
2621
-
2622
- const customHookTimerStatePrefix = "__kTimerState_"
2623
- const customHookTimerSetterPrefix = "__kSetTimerState_"
2624
- const customHookTimerStatesBySource = new WeakMap()
2625
-
2626
- function normalizeCustomHookTimerRefs(sourceFile, factory, context) {
2627
- const timerCall = (node, name) => ts.isCallExpression(node) && (
2628
- ts.isIdentifier(node.expression) && node.expression.text === name ||
2629
- ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.expression) && node.expression.expression.text === "window" && node.expression.name.text === name
2630
- )
2631
- const currentAccess = (node, name) => ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === name && node.name.text === "current"
2632
- const clearStatement = (node, name) => {
2633
- if (!ts.isIfStatement(node) || node.elseStatement || !currentAccess(unwrapExpression(node.expression), name)) return undefined
2634
- const statement = ts.isBlock(node.thenStatement) && node.thenStatement.statements.length === 1 ? node.thenStatement.statements[0] : node.thenStatement
2635
- if (!ts.isExpressionStatement(statement) || !timerCall(statement.expression, "clearTimeout") || statement.expression.arguments.length !== 1 || !currentAccess(unwrapExpression(statement.expression.arguments[0]), name)) return undefined
2636
- return { condition: unwrapExpression(node.expression), argument: unwrapExpression(statement.expression.arguments[0]) }
2637
- }
2638
- const analyze = (hook, hookName) => {
2639
- if (!/^use[A-Z]/.test(hookName) || hook.parameters.length || !hook.body || !ts.isBlock(hook.body)) return undefined
2640
- const returnedStatement = hook.body.statements.at(-1)
2641
- const returned = returnedStatement && ts.isReturnStatement(returnedStatement) && returnedStatement.expression ? unwrapExpression(returnedStatement.expression) : undefined
2642
- if (!returned || !ts.isObjectLiteralExpression(returned)) return undefined
2643
- const returnedNames = new Set(returned.properties.filter(ts.isShorthandPropertyAssignment).map(property => property.name.text))
2644
- const callbacks = new Map()
2645
- const refs = []
2646
- for (const statement of hook.body.statements) {
2647
- if (!ts.isVariableStatement(statement) || !(statement.declarationList.flags & ts.NodeFlags.Const)) continue
2648
- for (const declaration of statement.declarationList.declarations) {
2649
- if (ts.isIdentifier(declaration.name) && declaration.initializer && (ts.isArrowFunction(declaration.initializer) || ts.isFunctionExpression(declaration.initializer))) callbacks.set(declaration.name.text, declaration.initializer)
2650
- if (ts.isIdentifier(declaration.name) && declaration.initializer && ts.isCallExpression(declaration.initializer) && ts.isIdentifier(declaration.initializer.expression) && declaration.initializer.expression.text === "useRef" && declaration.initializer.arguments.length === 1 && declaration.initializer.arguments[0].kind === ts.SyntaxKind.NullKeyword) refs.push({ declaration, name: declaration.name.text })
2651
- }
2652
- }
2653
- const candidates = []
2654
- for (const ref of refs) {
2655
- const assignments = []
2656
- const accesses = []
2657
- const clearStatements = []
2658
- const collect = node => {
2659
- if (currentAccess(node, ref.name)) accesses.push(node)
2660
- if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken && currentAccess(unwrapExpression(node.left), ref.name)) assignments.push(node)
2661
- const clear = clearStatement(node, ref.name)
2662
- if (clear) clearStatements.push({ node, ...clear })
2663
- ts.forEachChild(node, collect)
2664
- }
2665
- collect(hook.body)
2666
- if (assignments.some(assignment => timerCall(unwrapExpression(assignment.right), "setTimeout"))) candidates.push({ ...ref, assignments, accesses, clearStatements })
2667
- }
2668
- if (!candidates.length) return undefined
2669
- if (candidates.length !== 1) throw sourceNodeError(hook, sourceFile, "Relative custom hooks may own only one private timeout ref")
2670
- const timer = candidates[0]
2671
- if (timer.assignments.length !== 1) throw sourceNodeError(timer.declaration, sourceFile, "Private timeout refs require one direct timer.current = setTimeout(...) assignment")
2672
- const assignment = timer.assignments[0]
2673
- const timeout = unwrapExpression(assignment.right)
2674
- const timeoutCallback = timeout.arguments[0]
2675
- const delay = timeout.arguments[1]
2676
- if (!timerCall(timeout, "setTimeout") || timeout.arguments.length !== 2 || !timeoutCallback || !(ts.isArrowFunction(timeoutCallback) || ts.isFunctionExpression(timeoutCallback)) || timeoutCallback.parameters.length || !delay || !ts.isNumericLiteral(unwrapExpression(delay))) throw sourceNodeError(assignment, sourceFile, "Private timeout refs require setTimeout() with one zero-argument callback and a numeric literal delay")
2677
- const callback = nearestFunction(assignment)
2678
- const callbackName = [...callbacks].find(([, value]) => value === callback)?.[0]
2679
- if (!callbackName || !returnedNames.has(callbackName) || !ts.isBlock(callback.body) || !ts.isExpressionStatement(assignment.parent) || assignment.parent.parent !== callback.body) throw sourceNodeError(assignment, sourceFile, "Private timeout refs must be assigned directly inside one returned custom-hook callback")
2680
- const callbackClear = timer.clearStatements.find(entry => nearestFunction(entry.node) === callback)
2681
- if (!callbackClear || callbackClear.node.parent !== callback.body || callback.body.statements.indexOf(callbackClear.node) >= callback.body.statements.indexOf(assignment.parent)) throw sourceNodeError(callback, sourceFile, "Private timeout callbacks must directly clear the previous timer before assigning its replacement")
2682
- const effectCalls = hook.body.statements.flatMap(statement => {
2683
- if (!ts.isExpressionStatement(statement) || !ts.isCallExpression(statement.expression) || !ts.isIdentifier(statement.expression.expression) || statement.expression.expression.text !== "useEffect") return []
2684
- return [statement.expression]
2685
- })
2686
- let cleanupClear
2687
- for (const effect of effectCalls) {
2688
- const [setup, dependencies] = effect.arguments
2689
- if (!(ts.isArrowFunction(setup) || ts.isFunctionExpression(setup)) || !ts.isArrayLiteralExpression(dependencies) || dependencies.elements.length) continue
2690
- const returns = effectReturns(setup)
2691
- if (returns.cleanups.length !== 1) continue
2692
- const cleanup = returns.cleanups[0]
2693
- const entry = timer.clearStatements.find(candidate => nearestFunction(candidate.node) === cleanup)
2694
- if (entry && ts.isBlock(cleanup.body) && cleanup.body.statements.length === 1 && cleanup.body.statements[0] === entry.node) cleanupClear = entry
2695
- }
2696
- if (!cleanupClear) throw sourceNodeError(timer.declaration, sourceFile, "Private timeout refs require one empty-dependency effect that directly clears the timer on cleanup")
2697
- const accepted = new Set([assignment.left, callbackClear.condition, callbackClear.argument, cleanupClear.condition, cleanupClear.argument].map(unwrapExpression))
2698
- const unsupported = timer.accesses.find(access => !accepted.has(access))
2699
- if (unsupported) throw sourceNodeError(unsupported, sourceFile, "Private timeout refs may only be read by their direct replacement and cleanup guards")
2700
- const identity = createHash("sha256").update(`${sourceFile.fileName}:${hook.pos}:${timer.name}`).digest("hex").slice(0, 10)
2701
- const stateName = `${customHookTimerStatePrefix}${identity}`
2702
- const setterName = `${customHookTimerSetterPrefix}${identity}`
2703
- if (referencesIdentifier(hook.body, stateName) || referencesIdentifier(hook.body, setterName)) throw sourceNodeError(timer.declaration, sourceFile, "Private timeout ref conflicts with compiler-owned bindings")
2704
- return { assignment, declaration: timer.declaration, refName: timer.name, returned, stateName, setterName }
2705
- }
2706
- const timerStates = new Set()
2707
- const transform = (hook, hookName) => {
2708
- const timer = analyze(hook, hookName)
2709
- if (!timer) return undefined
2710
- timerStates.add(timer.stateName)
2711
- const timerVisitor = current => {
2712
- if (current === timer.declaration) {
2713
- const binding = factory.createArrayBindingPattern([
2714
- factory.createBindingElement(undefined, undefined, timer.stateName),
2715
- factory.createBindingElement(undefined, undefined, timer.setterName)
2716
- ])
2717
- const initializer = factory.updateCallExpression(current.initializer, factory.createIdentifier("useState"), current.initializer.typeArguments, current.initializer.arguments)
2718
- return factory.updateVariableDeclaration(current, binding, current.exclamationToken, undefined, initializer)
2719
- }
2720
- if (current === timer.assignment) return factory.createCallExpression(factory.createIdentifier(timer.setterName), undefined, [ts.visitNode(current.right, timerVisitor)])
2721
- if (currentAccess(current, timer.refName)) return factory.createIdentifier(timer.stateName)
2722
- if (current === timer.returned) return factory.updateObjectLiteralExpression(current, [
2723
- ...current.properties,
2724
- factory.createShorthandPropertyAssignment(timer.stateName),
2725
- factory.createShorthandPropertyAssignment(timer.setterName)
2726
- ])
2727
- return ts.visitEachChild(current, timerVisitor, context)
2728
- }
2729
- return ts.visitEachChild(hook, timerVisitor, context)
2730
- }
2731
- const visitor = node => {
2732
- if (ts.isFunctionDeclaration(node)) {
2733
- const hookName = node.name?.text ?? (node.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.DefaultKeyword) ? "useDefault" : "")
2734
- const transformed = transform(node, hookName)
2735
- if (transformed) return transformed
2736
- }
2737
- if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && (ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer))) {
2738
- const transformed = transform(node.initializer, node.name.text)
2739
- if (transformed) return factory.updateVariableDeclaration(node, node.name, node.exclamationToken, node.type, transformed)
2740
- }
2741
- return ts.visitEachChild(node, visitor, context)
2742
- }
2743
- const normalized = ts.visitNode(sourceFile, visitor)
2744
- customHookTimerStatesBySource.set(normalized, timerStates)
2745
- return normalized
2746
- }
2747
-
2748
- function normalizeMediaQueryExternalStores(sourceFile, factory, context) {
2749
- const imports = sourceFile.statements.filter(statement => ts.isImportDeclaration(statement) && !statement.importClause?.isTypeOnly && ts.isStringLiteral(statement.moduleSpecifier) && statement.moduleSpecifier.text === "react" && statement.importClause?.namedBindings && ts.isNamedImports(statement.importClause.namedBindings))
2750
- const externalStoreImport = imports.flatMap(statement => statement.importClause.namedBindings.elements.map(entry => ({ entry, statement }))).find(({ entry }) => !entry.isTypeOnly && !entry.propertyName && entry.name.text === "useSyncExternalStore")
2751
- if (!externalStoreImport) return sourceFile
2752
- const returnedExpression = callback => {
2753
- if (!(ts.isArrowFunction(callback) || ts.isFunctionExpression(callback)) || callback.parameters.length) return undefined
2754
- if (!ts.isBlock(callback.body)) return unwrapExpression(callback.body)
2755
- if (callback.body.statements.length !== 1 || !ts.isReturnStatement(callback.body.statements[0]) || !callback.body.statements[0].expression) return undefined
2756
- return unwrapExpression(callback.body.statements[0].expression)
2757
- }
2758
- const matchMediaQuery = expression => {
2759
- expression = unwrapExpression(expression)
2760
- if (!ts.isCallExpression(expression) || expression.arguments.length !== 1 || !ts.isStringLiteral(unwrapExpression(expression.arguments[0])) || !ts.isPropertyAccessExpression(expression.expression) || !ts.isIdentifier(expression.expression.expression) || expression.expression.expression.text !== "window" || expression.expression.name.text !== "matchMedia" || !isUnshadowedGlobal(expression.expression.expression, sourceFile)) return undefined
2761
- return unwrapExpression(expression.arguments[0]).text
2762
- }
2763
- const mediaListener = (statement, method, media, callback) => ts.isExpressionStatement(statement) && ts.isCallExpression(statement.expression) && statement.expression.arguments.length === 2 && ts.isPropertyAccessExpression(statement.expression.expression) && ts.isIdentifier(statement.expression.expression.expression) && statement.expression.expression.expression.text === media && statement.expression.expression.name.text === method && ts.isStringLiteral(unwrapExpression(statement.expression.arguments[0])) && unwrapExpression(statement.expression.arguments[0]).text === "change" && ts.isIdentifier(unwrapExpression(statement.expression.arguments[1])) && unwrapExpression(statement.expression.arguments[1]).text === callback
2764
- const candidates = new Map()
2765
- let index = 0
2766
- const inspect = node => {
2767
- if (!ts.isVariableStatement(node) || !(node.declarationList.flags & ts.NodeFlags.Const) || node.declarationList.declarations.length !== 1) {
2768
- ts.forEachChild(node, inspect)
2769
- return
2770
- }
2771
- const declaration = node.declarationList.declarations[0]
2772
- const call = declaration.initializer && unwrapExpression(declaration.initializer)
2773
- if (!ts.isIdentifier(declaration.name) || !call || !ts.isCallExpression(call) || !ts.isIdentifier(call.expression) || call.expression.text !== "useSyncExternalStore" || isShadowedIdentifier(call.expression, sourceFile)) {
2774
- ts.forEachChild(node, inspect)
2775
- return
2776
- }
2777
- if (call.arguments.length !== 3) throw sourceNodeError(call, sourceFile, "Media query useSyncExternalStore() requires subscribe, browser snapshot, and false server snapshot callbacks")
2778
- const [subscribe, snapshot, serverSnapshot] = call.arguments.map(unwrapExpression)
2779
- if (!(ts.isArrowFunction(subscribe) || ts.isFunctionExpression(subscribe)) || subscribe.parameters.length !== 1 || !ts.isIdentifier(subscribe.parameters[0].name) || !ts.isBlock(subscribe.body)) throw sourceNodeError(subscribe, sourceFile, "Media query subscriptions require one inline callback parameter and block body")
2780
- if (subscribe.body.statements.length !== 3) throw sourceNodeError(subscribe, sourceFile, "Media query subscriptions must add and remove one matching change listener")
2781
- const callback = subscribe.parameters[0].name.text
2782
- const [mediaStatement, addStatement, returnStatement] = subscribe.body.statements
2783
- const mediaDeclaration = ts.isVariableStatement(mediaStatement) && (mediaStatement.declarationList.flags & ts.NodeFlags.Const) && mediaStatement.declarationList.declarations.length === 1 ? mediaStatement.declarationList.declarations[0] : undefined
2784
- const media = mediaDeclaration && ts.isIdentifier(mediaDeclaration.name) ? mediaDeclaration.name.text : undefined
2785
- const query = mediaDeclaration?.initializer && matchMediaQuery(mediaDeclaration.initializer)
2786
- const cleanup = ts.isReturnStatement(returnStatement) && returnStatement.expression ? unwrapExpression(returnStatement.expression) : undefined
2787
- const cleanupStatement = cleanup && (ts.isArrowFunction(cleanup) || ts.isFunctionExpression(cleanup)) && !cleanup.parameters.length
2788
- ? ts.isBlock(cleanup.body) ? cleanup.body.statements.length === 1 ? cleanup.body.statements[0] : undefined : factory.createExpressionStatement(cleanup.body)
2789
- : undefined
2790
- if (!media || !query || !mediaListener(addStatement, "addEventListener", media, callback) || !cleanupStatement || !mediaListener(cleanupStatement, "removeEventListener", media, callback)) throw sourceNodeError(subscribe, sourceFile, "Media query subscriptions must add and remove one matching change listener")
2791
- const snapshotValue = returnedExpression(snapshot)
2792
- const snapshotQuery = snapshotValue && ts.isPropertyAccessExpression(snapshotValue) && snapshotValue.name.text === "matches" ? matchMediaQuery(snapshotValue.expression) : undefined
2793
- const serverValue = returnedExpression(serverSnapshot)
2794
- if (snapshotQuery !== query || !serverValue || serverValue.kind !== ts.SyntaxKind.FalseKeyword) throw sourceNodeError(call, sourceFile, "Media query external stores require matching static snapshots and a false server fallback")
2795
- const owner = nearestFunction(node)
2796
- const topLevelOwner = owner && (owner.parent === sourceFile || ts.isVariableDeclaration(owner.parent) && owner.parent.parent?.parent?.parent === sourceFile)
2797
- if (!topLevelOwner || !owner.body || !ts.isBlock(owner.body) || node.parent !== owner.body) throw sourceNodeError(declaration, sourceFile, "Media query external stores must initialize one top-level component const")
2798
- for (const name of ["useEffect", "useState"]) if (owner.parameters.some(parameter => bindingNames(parameter.name).includes(name)) || functionVarDeclaresName(owner, name) || owner.body.statements.some(statement => statementDeclaresName(statement, name))) throw sourceNodeError(declaration, sourceFile, `Media query external stores conflict with component-local ${name}`)
2799
- let setter = `__kSetMediaQuery${index++}`
2800
- while (sourceFile.text.includes(setter)) setter = `__kSetMediaQuery${index++}`
2801
- candidates.set(node, { declaration, query, setter })
2802
- }
2803
- inspect(sourceFile)
2804
- const references = referenceIdentifiers(sourceFile, "useSyncExternalStore")
2805
- if (references.length !== candidates.size) throw sourceNodeError(references.find(reference => ![...candidates.values()].some(candidate => insideNode(reference, candidate.declaration.initializer))) ?? externalStoreImport.entry, sourceFile, "useSyncExternalStore is supported only for direct static media query declarations")
2806
- const directHooks = new Set(imports.flatMap(statement => statement.importClause.namedBindings.elements.filter(entry => !entry.propertyName).map(entry => entry.name.text)))
2807
- const missingHooks = ["useEffect", "useState"].filter(name => !directHooks.has(name))
2808
- for (const name of missingHooks) {
2809
- const collision = sourceFile.statements.some(statement => statementDeclaresName(statement, name) || ts.isImportDeclaration(statement) && importDeclarationNames(statement).includes(name))
2810
- if (collision) throw sourceNodeError([...candidates.values()][0].declaration, sourceFile, `Media query external stores conflict with local ${name}`)
2811
- }
2812
-
2813
- const visitor = node => {
2814
- const candidate = ts.isVariableStatement(node) ? candidates.get(node) : undefined
2815
- if (candidate) {
2816
- const state = factory.createVariableStatement(node.modifiers, factory.createVariableDeclarationList([
2817
- factory.createVariableDeclaration(factory.createArrayBindingPattern([
2818
- factory.createBindingElement(undefined, undefined, candidate.declaration.name),
2819
- factory.createBindingElement(undefined, undefined, candidate.setter)
2820
- ]), undefined, undefined, factory.createCallExpression(factory.createIdentifier("useState"), undefined, [factory.createFalse()]))
2821
- ], ts.NodeFlags.Const))
2822
- const media = factory.createIdentifier("media")
2823
- const update = factory.createIdentifier("update")
2824
- const mediaCall = factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("window"), "matchMedia"), undefined, [factory.createStringLiteral(candidate.query)])
2825
- const updateCallback = factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), factory.createCallExpression(factory.createIdentifier(candidate.setter), undefined, [factory.createPropertyAccessExpression(media, "matches")]))
2826
- const listener = method => factory.createCallExpression(factory.createPropertyAccessExpression(media, method), undefined, [factory.createStringLiteral("change"), update])
2827
- const cleanup = factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), listener("removeEventListener"))
2828
- const setup = factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), factory.createBlock([
2829
- factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(media, undefined, undefined, mediaCall)], ts.NodeFlags.Const)),
2830
- factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(update, undefined, undefined, updateCallback)], ts.NodeFlags.Const)),
2831
- factory.createExpressionStatement(factory.createCallExpression(update, undefined, [])),
2832
- factory.createExpressionStatement(listener("addEventListener")),
2833
- factory.createReturnStatement(cleanup)
2834
- ], true))
2835
- const effectCall = factory.createCallExpression(factory.createIdentifier("useEffect"), undefined, [setup, factory.createArrayLiteralExpression()])
2836
- ts.setOriginalNode(effectCall, candidate.declaration.initializer)
2837
- ts.setTextRange(effectCall, candidate.declaration.initializer)
2838
- return [state, factory.createExpressionStatement(effectCall)]
2839
- }
2840
- if (node === externalStoreImport.statement) {
2841
- const clause = node.importClause
2842
- const bindings = clause.namedBindings
2843
- const elements = bindings.elements.filter(entry => entry !== externalStoreImport.entry)
2844
- for (const name of missingHooks) elements.push(factory.createImportSpecifier(false, undefined, factory.createIdentifier(name)))
2845
- return factory.updateImportDeclaration(node, node.modifiers, factory.updateImportClause(clause, false, clause.name, factory.updateNamedImports(bindings, elements)), node.moduleSpecifier, node.attributes)
2846
- }
2847
- return ts.visitEachChild(node, visitor, context)
2848
- }
2849
- return ts.visitNode(sourceFile, visitor)
2850
- }
2851
-
2852
- function insideNode(node, root) {
2853
- for (let current = node; current; current = current.parent) if (current === root) return true
2854
- return false
2855
- }
2856
-
2857
- function normalizeNavigatorCapabilityConditions(sourceFile, factory, context) {
2858
- const candidates = new Map()
2859
- let index = 0
2860
- const inspect = node => {
2861
- if (!ts.isVariableStatement(node) || !(node.declarationList.flags & ts.NodeFlags.Const) || node.declarationList.declarations.length !== 1) {
2862
- ts.forEachChild(node, inspect)
2863
- return
2864
- }
2865
- const declaration = node.declarationList.declarations[0]
2866
- const value = declaration.initializer && unwrapExpression(declaration.initializer)
2867
- if (!ts.isIdentifier(declaration.name) || !value || !ts.isBinaryExpression(value) || value.operatorToken.kind !== ts.SyntaxKind.InKeyword || !ts.isStringLiteral(unwrapExpression(value.left)) || !ts.isIdentifier(unwrapExpression(value.right)) || unwrapExpression(value.right).text !== "navigator" || !isUnshadowedGlobal(unwrapExpression(value.right), sourceFile)) {
2868
- ts.forEachChild(node, inspect)
2869
- return
2870
- }
2871
- const owner = nearestFunction(node)
2872
- const topLevelOwner = owner && (owner.parent === sourceFile || ts.isVariableDeclaration(owner.parent) && owner.parent.parent?.parent?.parent === sourceFile)
2873
- if (!topLevelOwner || !owner.body || !ts.isBlock(owner.body) || node.parent !== owner.body) throw sourceNodeError(declaration, sourceFile, "Navigator capability conditions must be top-level component const declarations")
2874
- const references = referenceIdentifiers(owner.body, declaration.name.text)
2875
- const condition = references.length === 1 ? references[0] : undefined
2876
- const structural = condition && ts.isBinaryExpression(condition.parent) && condition.parent.left === condition && condition.parent.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken && containsJsx(condition.parent.right) && ts.isJsxExpression(condition.parent.parent) && condition.parent.parent.expression === condition.parent
2877
- if (!structural) throw sourceNodeError(declaration, sourceFile, "Navigator capability values may only control one direct JSX && branch")
2878
- for (const name of ["useEffect", "useState"]) if (owner.parameters.some(parameter => bindingNames(parameter.name).includes(name)) || functionVarDeclaresName(owner, name) || owner.body.statements.some(statement => statementDeclaresName(statement, name))) throw sourceNodeError(declaration, sourceFile, `Navigator capability conditions conflict with component-local ${name}`)
2879
- let setter = `__kSetNavigatorCapability${index++}`
2880
- while (sourceFile.text.includes(setter)) setter = `__kSetNavigatorCapability${index++}`
2881
- candidates.set(node, { declaration, property: unwrapExpression(value.left).text, setter })
2882
- }
2883
- inspect(sourceFile)
2884
- if (!candidates.size) return sourceFile
2885
-
2886
- const visitor = node => {
2887
- const candidate = ts.isVariableStatement(node) ? candidates.get(node) : undefined
2888
- if (candidate) {
2889
- const state = factory.createVariableStatement(node.modifiers, factory.createVariableDeclarationList([
2890
- factory.createVariableDeclaration(factory.createArrayBindingPattern([
2891
- factory.createBindingElement(undefined, undefined, candidate.declaration.name),
2892
- factory.createBindingElement(undefined, undefined, candidate.setter)
2893
- ]), undefined, undefined, factory.createCallExpression(factory.createIdentifier("useState"), undefined, [factory.createFalse()]))
2894
- ], ts.NodeFlags.Const))
2895
- const capability = factory.createBinaryExpression(factory.createStringLiteral(candidate.property), factory.createToken(ts.SyntaxKind.InKeyword), factory.createIdentifier("navigator"))
2896
- const setup = factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), factory.createBlock([
2897
- factory.createExpressionStatement(factory.createCallExpression(factory.createIdentifier(candidate.setter), undefined, [capability]))
2898
- ], true))
2899
- const effectCall = factory.createCallExpression(factory.createIdentifier("useEffect"), undefined, [setup, factory.createArrayLiteralExpression()])
2900
- ts.setOriginalNode(effectCall, candidate.declaration.initializer)
2901
- ts.setTextRange(effectCall, candidate.declaration.initializer)
2902
- const effect = factory.createExpressionStatement(effectCall)
2903
- return [state, effect]
2904
- }
2905
- return ts.visitEachChild(node, visitor, context)
2906
- }
2907
- let normalized = ts.visitNode(sourceFile, visitor)
2908
- const hookImports = normalized.statements.filter(statement => ts.isImportDeclaration(statement) && !statement.importClause?.isTypeOnly && ts.isStringLiteral(statement.moduleSpecifier) && ["react", "@kudzujs/core"].includes(statement.moduleSpecifier.text) && statement.importClause?.namedBindings && ts.isNamedImports(statement.importClause.namedBindings))
2909
- const hookImport = hookImports[0]
2910
- const bindings = hookImport?.importClause.namedBindings
2911
- const imported = new Set(hookImports.flatMap(statement => statement.importClause.namedBindings.elements.map(entry => entry.name.text)))
2912
- const missing = ["useEffect", "useState"].filter(name => !imported.has(name))
2913
- if (!missing.length) return normalized
2914
- for (const name of missing) if (normalized.statements.some(statement => !hookImports.includes(statement) && (statementDeclaresName(statement, name) || ts.isImportDeclaration(statement) && importDeclarationNames(statement).includes(name)))) throw sourceNodeError([...candidates.values()][0].declaration, sourceFile, `Navigator capability conditions conflict with local ${name}`)
2915
- if (hookImport && bindings && ts.isNamedImports(bindings)) {
2916
- const statements = normalized.statements.map(statement => statement === hookImport ? factory.updateImportDeclaration(statement, statement.modifiers, factory.updateImportClause(statement.importClause, false, statement.importClause.name, factory.updateNamedImports(bindings, [
2917
- ...bindings.elements,
2918
- ...missing.map(name => factory.createImportSpecifier(false, undefined, factory.createIdentifier(name)))
2919
- ])), statement.moduleSpecifier, statement.attributes) : statement)
2920
- return factory.updateSourceFile(normalized, statements)
2921
- }
2922
- const declaration = factory.createImportDeclaration(undefined, factory.createImportClause(false, undefined, factory.createNamedImports(missing.map(name => factory.createImportSpecifier(false, undefined, factory.createIdentifier(name))))), factory.createStringLiteral("@kudzujs/core"))
2923
- const statements = [...normalized.statements]
2924
- statements.splice(statements.findLastIndex(statement => ts.isImportDeclaration(statement)) + 1, 0, declaration)
2925
- return factory.updateSourceFile(normalized, statements)
2926
- }
2927
-
2928
- function normalizeEffectAnimationFrameRefs(sourceFile, factory, context) {
2929
- const frameCall = (node, name) => ts.isCallExpression(node) && (
2930
- ts.isIdentifier(node.expression) && node.expression.text === name ||
2931
- ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.expression) && node.expression.expression.text === "window" && node.expression.name.text === name
2932
- )
2933
- const unshadowedFrameCall = (node, owner) => {
2934
- const name = ts.isIdentifier(node.expression) ? node.expression : ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.expression) ? node.expression.expression : undefined
2935
- return name && !isShadowedIdentifier(name, owner) && !sourceFile.statements.some(statement => statementDeclaresName(statement, name.text) || ts.isImportDeclaration(statement) && importDeclarationNames(statement).includes(name.text))
2936
- }
2937
- const currentAccess = (node, name) => ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === name && node.name.text === "current"
2938
- const inside = (node, root) => {
2939
- for (let current = node; current; current = current.parent) if (current === root) return true
2940
- return false
2941
- }
2942
- const directOrGuarded = (statement, body, name, negated) => {
2943
- if (statement.parent === body) return true
2944
- let branch = statement
2945
- if (ts.isBlock(statement.parent) && statement.parent.statements.length === 1) branch = statement.parent
2946
- const conditional = branch.parent
2947
- if (!ts.isIfStatement(conditional) || conditional.thenStatement !== branch || conditional.parent !== body || conditional.elseStatement) return false
2948
- let condition = unwrapExpression(conditional.expression)
2949
- const isNegated = ts.isPrefixUnaryExpression(condition) && condition.operator === ts.SyntaxKind.ExclamationToken
2950
- if (isNegated) condition = unwrapExpression(condition.operand)
2951
- return isNegated === negated && currentAccess(condition, name)
2952
- }
2953
- const hasUseRefImport = sourceFile.statements.some(statement => ts.isImportDeclaration(statement) && !statement.importClause?.isTypeOnly && ts.isStringLiteral(statement.moduleSpecifier) && ["react", "@kudzujs/core"].includes(statement.moduleSpecifier.text) && statement.importClause?.namedBindings && ts.isNamedImports(statement.importClause.namedBindings) && statement.importClause.namedBindings.elements.some(entry => !entry.propertyName && entry.name.text === "useRef"))
2954
- const hasUseEffectImport = sourceFile.statements.some(statement => ts.isImportDeclaration(statement) && !statement.importClause?.isTypeOnly && ts.isStringLiteral(statement.moduleSpecifier) && ["react", "@kudzujs/core"].includes(statement.moduleSpecifier.text) && statement.importClause?.namedBindings && ts.isNamedImports(statement.importClause.namedBindings) && statement.importClause.namedBindings.elements.some(entry => !entry.propertyName && entry.name.text === "useEffect"))
2955
- const replacements = new Set()
2956
- const inspect = node => {
2957
- if (!hasUseRefImport || !ts.isVariableDeclaration(node) || !ts.isIdentifier(node.name) || !node.initializer || !ts.isCallExpression(node.initializer) || !ts.isIdentifier(node.initializer.expression) || node.initializer.expression.text !== "useRef" || isShadowedIdentifier(node.initializer.expression, sourceFile) || node.initializer.arguments.length !== 1 || !ts.isNumericLiteral(node.initializer.arguments[0]) || Number(node.initializer.arguments[0].text) !== 0) {
2958
- ts.forEachChild(node, inspect)
2959
- return
2960
- }
2961
- const owner = nearestFunction(node)
2962
- if (!owner?.body || !ts.isBlock(owner.body)) return
2963
- const accesses = []
2964
- const collect = current => {
2965
- if (currentAccess(current, node.name.text) && !isShadowedIdentifier(current.expression, owner.body)) accesses.push(current)
2966
- ts.forEachChild(current, collect)
2967
- }
2968
- collect(owner.body)
2969
- const frameAssignments = accesses.filter(access => ts.isBinaryExpression(access.parent) && unwrapExpression(access.parent.left) === access && access.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken && frameCall(unwrapExpression(access.parent.right), "requestAnimationFrame") && unshadowedFrameCall(unwrapExpression(access.parent.right), owner))
2970
- if (!frameAssignments.length) return
2971
- const invalidReference = referenceIdentifiers(owner.body, node.name.text).find(reference => !ts.isPropertyAccessExpression(reference.parent) || reference.parent.expression !== reference || reference.parent.name.text !== "current")
2972
- if (invalidReference) throw sourceNodeError(invalidReference, sourceFile, "Animation frame refs may only use direct .current reads and assignments")
2973
- const statement = node.parent?.parent
2974
- const topLevelOwner = owner.parent === sourceFile || ts.isVariableDeclaration(owner.parent) && owner.parent.parent?.parent?.parent === sourceFile
2975
- if (!topLevelOwner || !ts.isVariableStatement(statement) || !(statement.declarationList.flags & ts.NodeFlags.Const) || statement.declarationList.declarations.length !== 1 || statement.parent !== owner.body) throw sourceNodeError(node, sourceFile, "Animation frame refs must be one top-level component const")
2976
- if (frameAssignments.length !== 1) throw sourceNodeError(node, sourceFile, "Animation frame refs require one direct ref.current = requestAnimationFrame(callback) assignment")
2977
- const frame = unwrapExpression(frameAssignments[0].parent.right)
2978
- const frameCallback = frame.arguments.length === 1 && ts.isIdentifier(unwrapExpression(frame.arguments[0])) ? unwrapExpression(frame.arguments[0]) : undefined
2979
- const effectCalls = owner.body.statements.flatMap(statement => hasUseEffectImport && ts.isExpressionStatement(statement) && ts.isCallExpression(statement.expression) && ts.isIdentifier(statement.expression.expression) && statement.expression.expression.text === "useEffect" && !isShadowedIdentifier(statement.expression.expression, sourceFile) ? [statement.expression] : [])
2980
- const effects = effectCalls.filter(effect => effect.arguments[0] && inside(frameAssignments[0], effect.arguments[0]))
2981
- if (effects.length !== 1) throw sourceNodeError(node, sourceFile, "Animation frame refs must belong to one inline component effect")
2982
- const effect = effects[0]
2983
- const callback = effect.arguments[0]
2984
- if (!(ts.isArrowFunction(callback) || ts.isFunctionExpression(callback)) || !ts.isBlock(callback.body)) throw sourceNodeError(callback, sourceFile, "Animation frame refs require one inline block-bodied effect")
2985
- if (accesses.some(access => !inside(access, callback))) throw sourceNodeError(node, sourceFile, "Animation frame refs may only be used inside their owning effect")
2986
- const callbacks = new Map()
2987
- for (const statement of callback.body.statements) {
2988
- if (ts.isVariableStatement(statement)) for (const declaration of statement.declarationList.declarations) {
2989
- if (ts.isIdentifier(declaration.name) && declaration.initializer && (ts.isArrowFunction(declaration.initializer) || ts.isFunctionExpression(declaration.initializer))) callbacks.set(declaration.name.text, declaration.initializer)
2990
- }
2991
- if (ts.isFunctionDeclaration(statement) && statement.name && statement.body) callbacks.set(statement.name.text, statement)
2992
- }
2993
- const frameOwner = nearestFunction(frameAssignments[0])
2994
- const shadowedCallback = frameCallback && isShadowedIdentifier(frameCallback, callback.body)
2995
- const update = frameCallback && !shadowedCallback && callbacks.get(frameCallback.text)
2996
- if (!update) throw sourceNodeError(frame, sourceFile, "Animation frame refs require a direct local callback")
2997
- const frameStatement = frameAssignments[0].parent.parent
2998
- if (!frameOwner || ![...callbacks.values()].includes(frameOwner) || !ts.isBlock(frameOwner.body) || !ts.isExpressionStatement(frameStatement) || !directOrGuarded(frameStatement, frameOwner.body, node.name.text, true)) throw sourceNodeError(frameAssignments[0], sourceFile, "Animation frame requests must be assigned directly inside one local scheduler")
2999
- const resetAssignments = accesses.filter(access => ts.isBinaryExpression(access.parent) && unwrapExpression(access.parent.left) === access && access.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken && ts.isNumericLiteral(unwrapExpression(access.parent.right)) && Number(unwrapExpression(access.parent.right).text) === 0)
3000
- const resetStatement = resetAssignments[0]?.parent.parent
3001
- if (resetAssignments.length !== 1 || nearestFunction(resetAssignments[0]) !== update || !ts.isBlock(update.body) || !ts.isExpressionStatement(resetStatement) || resetStatement.parent !== update.body) throw sourceNodeError(update, sourceFile, "Animation frame callbacks must directly reset their ref to 0")
3002
- const writes = accesses.filter(access => ts.isBinaryExpression(access.parent) && unwrapExpression(access.parent.left) === access && access.parent.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && access.parent.operatorToken.kind <= ts.SyntaxKind.LastAssignment)
3003
- if (writes.length !== 2) throw sourceNodeError(node, sourceFile, "Animation frame refs may only be assigned by their request and reset operations")
3004
- const returns = effectReturns(callback)
3005
- const cancellations = []
3006
- const collectCancellations = current => {
3007
- const argument = current.arguments?.[0] && unwrapExpression(current.arguments[0])
3008
- if (frameCall(current, "cancelAnimationFrame") && unshadowedFrameCall(current, owner) && current.arguments.length === 1 && currentAccess(argument, node.name.text) && !isShadowedIdentifier(argument.expression, owner.body)) cancellations.push(current)
3009
- ts.forEachChild(current, collectCancellations)
849
+ function normalizeCompilerSource(sourceFile, { base, context, file, importedCollections, importedStaticCollections, sourceFiles, sourceIndex }) {
850
+ const factory = context.factory
851
+ let customHookTimerStates = new Set()
852
+ sourceFile = applyNormalizationPasses(sourceFile, [
853
+ ...(importedStaticCollections ? [source => normalizeImportedStaticCollections(source, importedStaticCollections, factory, context)] : []),
854
+ source => normalizeReactRouterSyntax(source, factory, context, base),
855
+ source => normalizeClsxSyntax(source, factory, context),
856
+ source => normalizeMediaQueryExternalStores(source, factory, context),
857
+ source => normalizeReactMigrationSyntax(source, factory, context, importedCollections ?? importedSerializableCollectionNames(source, file, sourceFiles, sourceIndex)),
858
+ source => normalizeNavigatorCapabilityConditions(source, factory, context),
859
+ source => normalizeEffectAnimationFrameRefs(source, factory, context),
860
+ source => {
861
+ const result = normalizeCustomHookTimerRefs(source, factory, context)
862
+ customHookTimerStates = result.timerStates
863
+ return result.sourceFile
864
+ },
865
+ source => {
866
+ validateUseIdSyntax(source)
867
+ return source
868
+ },
869
+ source => normalizeLazyStateInitializers(source, factory, context, file, sourceFiles, sourceIndex),
870
+ source => normalizeZustandMigrationSyntax(source, factory, context),
871
+ source => normalizeRenderControlFlow(source, factory, context),
872
+ source => {
873
+ workerCompiler.rejectOrdinaryImports(source, file, sourceFiles)
874
+ return source
3010
875
  }
3011
- collectCancellations(callback.body)
3012
- const cancellation = cancellations[0]
3013
- const cleanup = cancellation && returns.cleanups.find(candidate => nearestFunction(cancellation) === candidate)
3014
- const cancellationStatement = cancellation?.parent
3015
- if (cancellations.length !== 1 || !cleanup || !ts.isBlock(cleanup.body) || !ts.isExpressionStatement(cancellationStatement) || !directOrGuarded(cancellationStatement, cleanup.body, node.name.text, false)) throw sourceNodeError(node, sourceFile, "Animation frame refs require direct cancellation in effect cleanup")
3016
- replacements.add(node)
3017
- }
3018
- inspect(sourceFile)
3019
- if (!replacements.size) return sourceFile
3020
- const visitor = node => {
3021
- if (replacements.has(node)) return factory.updateVariableDeclaration(node, node.name, node.exclamationToken, node.type, factory.createObjectLiteralExpression([
3022
- factory.createPropertyAssignment("current", factory.createNumericLiteral(0))
3023
- ]))
3024
- return ts.visitEachChild(node, visitor, context)
3025
- }
3026
- return ts.visitNode(sourceFile, visitor)
876
+ ])
877
+ return { sourceFile, customHookTimerStates }
3027
878
  }
3028
879
 
3029
- function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings, listExpressions, handlerUrl, file, sourceFiles, sourceIndex, staticFiles, importedAssets, cssModules, base, clientImports, workerReferences) {
880
+ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourceIndex, staticFiles, importedAssets, cssModules, base, workerReferences }) {
881
+ const { nativeHandlers, effectHandlers } = semantic
3030
882
  return context => sourceFile => {
3031
- const factory = context.factory
3032
883
  const hasLinkElements = /<link/i.test(sourceFile.text)
3033
884
  const importedStaticCollections = importedSerializableCollections(sourceFile, file, sourceFiles, sourceIndex)
3034
885
  const importedCollections = new Set(importedStaticCollections.keys())
3035
- sourceFile = normalizeImportedStaticCollections(sourceFile, importedStaticCollections, factory, context)
3036
- ts.setParentRecursive(sourceFile, false)
3037
- sourceFile = normalizeReactRouterSyntax(sourceFile, factory, context, base)
3038
- ts.setParentRecursive(sourceFile, false)
3039
- sourceFile = normalizeClsxSyntax(sourceFile, factory, context)
3040
- ts.setParentRecursive(sourceFile, false)
3041
- sourceFile = normalizeMediaQueryExternalStores(sourceFile, factory, context)
3042
- ts.setParentRecursive(sourceFile, false)
3043
- sourceFile = normalizeReactMigrationSyntax(sourceFile, factory, context, importedCollections)
3044
- ts.setParentRecursive(sourceFile, false)
3045
- sourceFile = normalizeNavigatorCapabilityConditions(sourceFile, factory, context)
3046
- ts.setParentRecursive(sourceFile, false)
3047
- sourceFile = normalizeEffectAnimationFrameRefs(sourceFile, factory, context)
3048
- ts.setParentRecursive(sourceFile, false)
3049
- sourceFile = normalizeCustomHookTimerRefs(sourceFile, factory, context)
3050
- const customHookTimerStates = customHookTimerStatesBySource.get(sourceFile) ?? new Set()
3051
- ts.setParentRecursive(sourceFile, false)
3052
- validateUseIdSyntax(sourceFile)
3053
- sourceFile = normalizeLazyStateInitializers(sourceFile, factory, context, file, sourceFiles, sourceIndex)
3054
- ts.setParentRecursive(sourceFile, false)
3055
- sourceFile = normalizeZustandMigrationSyntax(sourceFile, factory, context)
3056
- ts.setParentRecursive(sourceFile, false)
3057
- sourceFile = normalizeRenderControlFlow(sourceFile, factory, context)
3058
- ts.setParentRecursive(sourceFile, false)
3059
- rejectOrdinaryWorkerImports(sourceFile, file, sourceFiles)
886
+ const normalized = normalizeCompilerSource(sourceFile, { base, context, file, importedCollections, importedStaticCollections, sourceFiles, sourceIndex })
887
+ sourceFile = normalized.sourceFile
888
+ const { customHookTimerStates } = normalized
889
+ const factory = context.factory
890
+ const descriptors = createDescriptorSession({
891
+ semantic,
892
+ handlerUrl,
893
+ factory,
894
+ context,
895
+ compileEventCommand,
896
+ isPrimitiveLiteral: isPrimitiveDefaultLiteral,
897
+ rejectWorkerConstructions: expression => workerCompiler.rejectConstructions(expression, expression.getSourceFile(), "Relative TypeScript Worker construction is only supported directly inside an inline useEffect() callback")
898
+ })
3060
899
  const importBindings = clientImportBindings(sourceFile, file, sourceFiles)
3061
900
  const packageBindings = packageImportBindings(sourceFile)
3062
901
  for (const [name] of packageBindings) {
@@ -3065,36 +904,14 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
3065
904
  if (invalid) throw sourceNodeError(invalid, sourceFile, `Package import ${JSON.stringify(name)} may only be referenced directly inside JSX event handlers`)
3066
905
  }
3067
906
  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"))
3068
- const importedSources = new Map()
3069
- const importedTimerStates = new Map()
907
+ const importedSourceCache = new Map()
3070
908
  const importedSource = target => {
3071
- let imported = importedSources.get(target)
3072
- if (!imported) {
3073
- imported = normalizeReactRouterSyntax(parseSourceFile(target, sourceIndex.get(target)), factory, context, base)
3074
- ts.setParentRecursive(imported, false)
3075
- imported = normalizeClsxSyntax(imported, factory, context)
3076
- ts.setParentRecursive(imported, false)
3077
- imported = normalizeMediaQueryExternalStores(imported, factory, context)
3078
- ts.setParentRecursive(imported, false)
3079
- imported = normalizeReactMigrationSyntax(imported, factory, context, importedSerializableCollectionNames(imported, target, sourceFiles, sourceIndex))
3080
- ts.setParentRecursive(imported, false)
3081
- imported = normalizeNavigatorCapabilityConditions(imported, factory, context)
3082
- ts.setParentRecursive(imported, false)
3083
- imported = normalizeEffectAnimationFrameRefs(imported, factory, context)
3084
- ts.setParentRecursive(imported, false)
3085
- imported = normalizeCustomHookTimerRefs(imported, factory, context)
3086
- importedTimerStates.set(target, customHookTimerStatesBySource.get(imported) ?? new Set())
3087
- ts.setParentRecursive(imported, false)
3088
- validateUseIdSyntax(imported)
3089
- imported = normalizeLazyStateInitializers(imported, factory, context, target, sourceFiles, sourceIndex)
3090
- ts.setParentRecursive(imported, false)
3091
- imported = normalizeZustandMigrationSyntax(imported, factory, context)
3092
- ts.setParentRecursive(imported, false)
3093
- imported = normalizeRenderControlFlow(imported, factory, context)
3094
- ts.setParentRecursive(imported, false)
3095
- importedSources.set(target, imported)
909
+ let result = importedSourceCache.get(target)
910
+ if (!result) {
911
+ result = normalizeCompilerSource(parseSourceFile(target, sourceIndex.get(target)), { base, context, file: target, sourceFiles, sourceIndex })
912
+ importedSourceCache.set(target, result)
3096
913
  }
3097
- return imported
914
+ return result.sourceFile
3098
915
  }
3099
916
  const importedCollectionTransforms = new Map()
3100
917
  const importedCalculationFunctions = new Map()
@@ -3255,7 +1072,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
3255
1072
  const capture = nativeCaptureNames(callback, states).values().next().value
3256
1073
  if (capture) throw sourceNodeError(callback, hookSource, `Relative custom hook callback ${JSON.stringify(name)} cannot capture private binding ${JSON.stringify(capture)}`)
3257
1074
  }
3258
- const privateStates = new Set([...states.values()].filter(state => importedTimerStates.get(hookSource.fileName)?.has(state)))
1075
+ const privateStates = new Set([...states.values()].filter(state => importedSourceCache.get(hookSource.fileName)?.customHookTimerStates.has(state)))
3259
1076
  const analysis = { callbacks, fields, privateStates, states }
3260
1077
  customHooks.set(key, analysis)
3261
1078
  return analysis
@@ -3492,14 +1309,14 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
3492
1309
  const call = unwrapExpression(current.expression)
3493
1310
  if (ts.isIdentifier(call.expression) && importBindings.has(call.expression.text) && importBindings.get(call.expression.text).kind !== "namespace") {
3494
1311
  validateImportedCalculation(call, current.name.text)
3495
- for (const argument of call.arguments) collectionExpression(argument, {}, (target, message) => fail(target, message.replace("Rendered collection", "Reactive imported calculation")), allowedNames)
1312
+ for (const argument of call.arguments) collectionExpression(argument, { fail: (target, message) => fail(target, message.replace("Rendered collection", "Reactive imported calculation")), stateNames: allowedNames })
3496
1313
  return factory.createNumericLiteral(0)
3497
1314
  }
3498
1315
  }
3499
1316
  return ts.visitEachChild(current, validate, context)
3500
1317
  }
3501
1318
  const normalized = ts.visitNode(value, validate)
3502
- collectionExpression(normalized, {}, (node, message) => fail(node, message.replace("Rendered collection", "Reactive JSX local")), allowedNames)
1319
+ collectionExpression(normalized, { fail: (node, message) => fail(node, message.replace("Rendered collection", "Reactive JSX local")), stateNames: allowedNames })
3503
1320
  return
3504
1321
  }
3505
1322
  const intl = constructor.expression.expression
@@ -3509,7 +1326,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
3509
1326
  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
3510
1327
  if (!roundAccess) fail(value, "Reactive JSX Intl.NumberFormat format() requires exactly Math.round(expression)")
3511
1328
  if (!isUnshadowedGlobal(roundAccess.expression, sourceFile)) fail(roundAccess.expression, "Reactive JSX Intl.NumberFormat requires the unshadowed global Math object")
3512
- collectionExpression(rounded.arguments[0], {}, (node, message) => fail(node, message.replace("Rendered collection", "Reactive JSX local")), allowedNames)
1329
+ collectionExpression(rounded.arguments[0], { fail: (node, message) => fail(node, message.replace("Rendered collection", "Reactive JSX local")), stateNames: allowedNames })
3513
1330
  }
3514
1331
  const resolveReactiveJsxExpression = (expression, owner, setters) => {
3515
1332
  const declarations = jsxLocalDeclarations.get(owner)
@@ -4081,16 +1898,12 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
4081
1898
  if (!usedStates.size && !captures.size) return ts.visitEachChild(expression, visitor, context)
4082
1899
  usesBehavior = true
4083
1900
  usesConditional = true
4084
- return compileConditional(
1901
+ return descriptors.compileConditional(
4085
1902
  parts.kind,
4086
1903
  parts.condition,
4087
1904
  compileRenderExpression(parts.truthy, anchor),
4088
1905
  compileRenderExpression(parts.falsy, anchor),
4089
- setters,
4090
- factory,
4091
- context,
4092
- reactiveBindings,
4093
- handlerUrl
1906
+ setters
4094
1907
  )
4095
1908
  }
4096
1909
 
@@ -4190,7 +2003,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
4190
2003
  const derivedStates = initializer && !directAlias ? referencedStateNames(initializer, setters) : new Set()
4191
2004
  if (derivedStates.size) {
4192
2005
  const usedStates = new Set()
4193
- const expression = collectionExpression(initializer, {}, effectFail, stateNames, usedStates)
2006
+ const expression = collectionExpression(initializer, { fail: effectFail, stateNames, selectorStates: usedStates })
4194
2007
  if (!usedStates.size) effectFail(dependency, `useEffect() derived dependency "${dependency.text}" must read direct primitive state`)
4195
2008
  dependencyExpressions.push(expression)
4196
2009
  for (const name of usedStates) {
@@ -4242,11 +2055,19 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
4242
2055
  }
4243
2056
  if (listEffect && callbackFile !== file) {
4244
2057
  const originalCallback = listEffect.source.arguments[0]
4245
- rejectWorkerConstructions(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")
2058
+ 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")
4246
2059
  } else {
4247
- compiledCallback = rewriteEffectWorkers(compiledCallback, callbackFile, callbackSource, sourceFiles, workerReferences, factory, context)
4248
- }
4249
- const descriptor = compileNativeCallback(compiledCallback, setters, reducersForNode(node, reducersByFunction), factory, effectHandlers, specializedEffect?.imports ?? importBindings, clientImports, "effect", dependencyItem, true, returns.cleanup, customHookTimerStates)
2060
+ compiledCallback = workerCompiler.rewriteEffect(compiledCallback, callbackFile, callbackSource, sourceFiles, workerReferences, factory, context)
2061
+ }
2062
+ const descriptor = descriptors.compileEffectCallback(compiledCallback, {
2063
+ setters,
2064
+ reducers: reducersForNode(node, reducersByFunction),
2065
+ importBindings: specializedEffect?.imports ?? importBindings,
2066
+ listItem: dependencyItem,
2067
+ deferValues: true,
2068
+ snapshotNested: returns.cleanup,
2069
+ liveStates: customHookTimerStates
2070
+ })
4250
2071
  for (const reference of workerReferences.slice(workerStart)) Object.assign(reference, { module: handlerUrl, handler: descriptor.exportName })
4251
2072
  usesListItem ||= Boolean(itemDependencies.length && !listEffect)
4252
2073
  usesBehavior = true
@@ -4302,19 +2123,19 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
4302
2123
 
4303
2124
  if (ts.isJsxExpression(node) && node.expression && listConditions.has(node.expression)) {
4304
2125
  const entry = listConditions.get(node.expression)
4305
- return factory.updateJsxExpression(node, compileListConditional({
2126
+ return factory.updateJsxExpression(node, descriptors.compileListConditional({
4306
2127
  ...entry,
4307
2128
  truthy: ts.visitNode(entry.truthy, visitor),
4308
2129
  falsy: ts.visitNode(entry.falsy, visitor)
4309
- }, factory, listExpressions, handlerUrl))
2130
+ }))
4310
2131
  }
4311
2132
 
4312
2133
  if (ts.isJsxExpression(node) && node.expression && listValues.has(node.expression)) {
4313
- return factory.updateJsxExpression(node, compileListValue(node.expression, listValues.get(node.expression), factory, context, listExpressions, handlerUrl))
2134
+ return factory.updateJsxExpression(node, descriptors.compileListValue(node.expression, listValues.get(node.expression)))
4314
2135
  }
4315
2136
 
4316
2137
  if (ts.isJsxAttribute(node) && node.initializer && ts.isJsxExpression(node.initializer) && node.initializer.expression && listValues.has(node.initializer.expression)) {
4317
- return factory.updateJsxAttribute(node, node.name, factory.createJsxExpression(undefined, compileListValue(node.initializer.expression, listValues.get(node.initializer.expression), factory, context, listExpressions, handlerUrl)))
2138
+ return factory.updateJsxAttribute(node, node.name, factory.createJsxExpression(undefined, descriptors.compileListValue(node.initializer.expression, listValues.get(node.initializer.expression))))
4318
2139
  }
4319
2140
 
4320
2141
  if (ts.isJsxExpression(node) && node.initializer === undefined && node.expression && (ts.isJsxElement(node.parent) || ts.isJsxFragment(node.parent))) {
@@ -4326,7 +2147,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
4326
2147
  let listSource = listParts.state
4327
2148
  if (listParts.calculation) {
4328
2149
  usesBinding = true
4329
- listSource = compileReactiveBinding(listParts.calculation, settersForNode(node, settersByFunction), factory, context, reactiveBindings, handlerUrl, importBindings, clientImports)
2150
+ listSource = descriptors.compileReactiveBinding(listParts.calculation, { setters: settersForNode(node, settersByFunction), importBindings })
4330
2151
  }
4331
2152
  const arguments_ = [
4332
2153
  listSource,
@@ -4352,7 +2173,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
4352
2173
  if ((usedStates.size || captures.size) && !ts.isIdentifier(expression) && !containsJsx(expression)) {
4353
2174
  usesBehavior = true
4354
2175
  usesBinding = true
4355
- return factory.updateJsxExpression(node, compileReactiveBinding(expression, setters, factory, context, reactiveBindings, handlerUrl, importBindings, clientImports))
2176
+ return factory.updateJsxExpression(node, descriptors.compileReactiveBinding(expression, { setters, importBindings }))
4356
2177
  }
4357
2178
  }
4358
2179
 
@@ -4365,14 +2186,20 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
4365
2186
  if ((usedStates.size || captures.size) && !ts.isIdentifier(expression)) {
4366
2187
  usesBehavior = true
4367
2188
  usesBinding = true
4368
- const compiled = compileReactiveBinding(expression, setters, factory, context, reactiveBindings, handlerUrl, importBindings, clientImports)
2189
+ const compiled = descriptors.compileReactiveBinding(expression, { setters, importBindings })
4369
2190
  return factory.updateJsxAttribute(node, node.name, factory.createJsxExpression(undefined, compiled))
4370
2191
  }
4371
2192
  }
4372
2193
 
4373
2194
  if (ts.isJsxAttribute(node) && node.initializer && ts.isJsxExpression(node.initializer) && node.initializer.expression && /^on[A-Z]/.test(node.name.text)) {
4374
2195
  const setters = settersForNode(node, settersByFunction)
4375
- const event = compileEvent(node.initializer.expression, setters, reducersForNode(node, reducersByFunction), functionsForNode(node), factory, nativeHandlers, handlerUrl, listEventItems.get(node), new Map([...importBindings, ...packageBindings]), clientImports)
2196
+ const event = descriptors.compileEvent(node.initializer.expression, {
2197
+ setters,
2198
+ reducers: reducersForNode(node, reducersByFunction),
2199
+ functions: functionsForNode(node),
2200
+ listItem: listEventItems.get(node),
2201
+ importBindings: new Map([...importBindings, ...packageBindings])
2202
+ })
4376
2203
  if (event) {
4377
2204
  usesBehavior = true
4378
2205
  return factory.updateJsxAttribute(node, node.name, factory.createJsxExpression(undefined, event))
@@ -4416,106 +2243,12 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
4416
2243
  if (usesComponentEffects) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("useEffect"), factory.createIdentifier("__kComponentUseEffect")))
4417
2244
  if (usesBinding || usesConditional) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("bindingValue"), factory.createIdentifier("__kBindingValue")))
4418
2245
  const behaviorImport = factory.createImportDeclaration(
4419
- undefined,
4420
- factory.createImportClause(false, undefined, factory.createNamedImports(behaviorImports)),
4421
- factory.createStringLiteral("@kudzujs/core")
4422
- )
4423
- return factory.updateSourceFile(transformed, [behaviorImport, ...transformed.statements])
4424
- }
4425
- }
4426
-
4427
- function normalizeRenderControlFlow(sourceFile, factory, context) {
4428
- const normalizeStatements = statements => {
4429
- const nested = statements.map(statement => ts.visitEachChild(statement, visitNested, context))
4430
- const assigned = []
4431
- for (let index = 0; index < nested.length; index++) {
4432
- const statement = nested[index]
4433
- const next = nested[index + 1]
4434
- const declaration = singleUninitializedLet(statement)
4435
- const assignment = declaration && next && assignmentConditional(next, declaration.name.text, factory)
4436
- if (declaration && assignment) {
4437
- const updated = factory.updateVariableDeclaration(declaration, declaration.name, declaration.exclamationToken, declaration.type, assignment)
4438
- const list = factory.createVariableDeclarationList([updated], ts.NodeFlags.Const)
4439
- assigned.push(factory.updateVariableStatement(statement, statement.modifiers, list))
4440
- index++
4441
- } else {
4442
- assigned.push(statement)
4443
- }
4444
- }
4445
-
4446
- if (!assigned.length) return assigned
4447
- const finalIf = returnConditional(assigned.at(-1), factory)
4448
- if (finalIf) return [...assigned.slice(0, -1), factory.createReturnStatement(finalIf)]
4449
- if (!ts.isReturnStatement(assigned.at(-1)) || !assigned.at(-1).expression) return assigned
4450
- let expression = assigned.at(-1).expression
4451
- let start = assigned.length - 1
4452
- while (start > 0) {
4453
- const previous = assigned[start - 1]
4454
- if (!ts.isIfStatement(previous) || previous.elseStatement) break
4455
- const truthy = returnOnlyExpression(previous.thenStatement)
4456
- if (!truthy) break
4457
- expression = factory.createConditionalExpression(previous.expression, factory.createToken(ts.SyntaxKind.QuestionToken), truthy, factory.createToken(ts.SyntaxKind.ColonToken), expression)
4458
- start--
4459
- }
4460
- return start === assigned.length - 1 ? assigned : [...assigned.slice(0, start), factory.createReturnStatement(expression)]
4461
- }
4462
-
4463
- const visitNested = node => {
4464
- if (ts.isBlock(node)) return factory.updateBlock(node, normalizeStatements([...node.statements]))
4465
- if (isFunctionLike(node) && ts.isBlock(node.body)) {
4466
- if (!isRenderFunction(node)) return node
4467
- const body = factory.updateBlock(node.body, normalizeStatements([...node.body.statements]))
4468
- if (ts.isFunctionDeclaration(node)) return factory.updateFunctionDeclaration(node, node.modifiers, node.asteriskToken, node.name, node.typeParameters, node.parameters, node.type, body)
4469
- if (ts.isFunctionExpression(node)) return factory.updateFunctionExpression(node, node.modifiers, node.asteriskToken, node.name, node.typeParameters, node.parameters, node.type, body)
4470
- if (ts.isArrowFunction(node)) return factory.updateArrowFunction(node, node.modifiers, node.typeParameters, node.parameters, node.type, node.equalsGreaterThanToken, body)
4471
- }
4472
- return ts.visitEachChild(node, visitNested, context)
4473
- }
4474
-
4475
- return ts.visitEachChild(sourceFile, visitNested, context)
4476
- }
4477
-
4478
- function isRenderFunction(node) {
4479
- if (ts.isFunctionDeclaration(node)) return node.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.DefaultKeyword) || Boolean(node.name && /^[A-Z]/.test(node.name.text))
4480
- const declaration = node.parent
4481
- return ts.isVariableDeclaration(declaration) && ts.isIdentifier(declaration.name) && /^[A-Z]/.test(declaration.name.text)
4482
- }
4483
-
4484
- function singleUninitializedLet(statement) {
4485
- if (!ts.isVariableStatement(statement) || (statement.declarationList.flags & ts.NodeFlags.Let) === 0 || statement.declarationList.declarations.length !== 1) return undefined
4486
- const declaration = statement.declarationList.declarations[0]
4487
- return ts.isIdentifier(declaration.name) && !declaration.initializer ? declaration : undefined
4488
- }
4489
-
4490
- function assignmentConditional(statement, name, factory) {
4491
- if (!ts.isIfStatement(statement) || !statement.elseStatement) return undefined
4492
- const truthy = assignmentOnlyExpression(statement.thenStatement, name)
4493
- const falsy = ts.isIfStatement(statement.elseStatement)
4494
- ? assignmentConditional(statement.elseStatement, name, factory)
4495
- : assignmentOnlyExpression(statement.elseStatement, name)
4496
- if (!truthy || !falsy) return undefined
4497
- return factory.createConditionalExpression(statement.expression, factory.createToken(ts.SyntaxKind.QuestionToken), truthy, factory.createToken(ts.SyntaxKind.ColonToken), falsy)
4498
- }
4499
-
4500
- function assignmentOnlyExpression(statement, name) {
4501
- const candidate = ts.isBlock(statement) && statement.statements.length === 1 ? statement.statements[0] : statement
4502
- if (!ts.isExpressionStatement(candidate) || !ts.isBinaryExpression(candidate.expression) || candidate.expression.operatorToken.kind !== ts.SyntaxKind.EqualsToken || !ts.isIdentifier(candidate.expression.left) || candidate.expression.left.text !== name) return undefined
4503
- return candidate.expression.right
4504
- }
4505
-
4506
- function returnConditional(statement, factory) {
4507
- if (!ts.isIfStatement(statement) || !statement.elseStatement) return undefined
4508
- const truthy = returnOnlyExpression(statement.thenStatement)
4509
- const falsy = ts.isIfStatement(statement.elseStatement)
4510
- ? returnConditional(statement.elseStatement, factory)
4511
- : returnOnlyExpression(statement.elseStatement)
4512
- if (!truthy || !falsy) return undefined
4513
- return factory.createConditionalExpression(statement.expression, factory.createToken(ts.SyntaxKind.QuestionToken), truthy, factory.createToken(ts.SyntaxKind.ColonToken), falsy)
4514
- }
4515
-
4516
- function returnOnlyExpression(statement) {
4517
- const candidate = ts.isBlock(statement) && statement.statements.length === 1 ? statement.statements[0] : statement
4518
- return ts.isReturnStatement(candidate) && candidate.expression ? candidate.expression : undefined
2246
+ undefined,
2247
+ factory.createImportClause(false, undefined, factory.createNamedImports(behaviorImports)),
2248
+ factory.createStringLiteral("@kudzujs/core")
2249
+ )
2250
+ return factory.updateSourceFile(transformed, [behaviorImport, ...transformed.statements])
2251
+ }
4519
2252
  }
4520
2253
 
4521
2254
  function containsRenderControl(root, knownLocals) {
@@ -4534,7 +2267,9 @@ function keyedListParts(expression, setters, declarations, fail, aliases = new S
4534
2267
  const value = unwrapExpression(expression)
4535
2268
  const directFrom = isArrayFromCall(value) && value.arguments.length === 2 && containsJsx(value.arguments[1])
4536
2269
  if (!directFrom && (!ts.isCallExpression(value) || value.arguments.length !== 1 || !ts.isPropertyAccessExpression(value.expression) || value.expression.name.text !== "map")) return undefined
4537
- let collection = renderedCollectionSource(directFrom ? value.arguments[0] : value.expression.expression, setters, declarations, fail, aliases, importedCollections, new Set(setters.values()), importedCollectionTransforms, factory, context, calculatedCollection, staticCollection)
2270
+ let collection = analyzeCollectionPipeline(directFrom ? value.arguments[0] : value.expression.expression, {
2271
+ setters, declarations, fail, aliases, importedCollections, stateNames: new Set(setters.values()), importedCollectionTransforms, calculatedCollection, staticCollection
2272
+ })
4538
2273
  if (!collection?.state && !collection?.calculation) return undefined
4539
2274
  if (directFrom) collection.selector.push(["from", undefined])
4540
2275
  let callback = directFrom ? value.arguments[1] : value.arguments[0]
@@ -4544,7 +2279,7 @@ function keyedListParts(expression, setters, declarations, fail, aliases = new S
4544
2279
  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")
4545
2280
  const declaration = root.statements[0].declarationList.declarations[0]
4546
2281
  if (!ts.isIdentifier(declaration.name) || !declaration.initializer) fail(declaration, "Computed child collections must initialize one const identifier")
4547
- const computed = renderedCollectionSource(declaration.initializer, new Map(), undefined, fail, new Set(), new Set(), new Set(), importedCollectionTransforms, factory, context)
2282
+ const computed = analyzeCollectionPipeline(declaration.initializer, { fail, importedCollectionTransforms })
4548
2283
  if (!computed?.ownerField || computed.parentItem !== parameters.item) fail(declaration.initializer, `Computed child collections must start from ${parameters.item}.<field>`)
4549
2284
  const returned = root.statements[1].expression
4550
2285
  if (identifierReferenceCount(returned, declaration.name.text) !== 1) fail(declaration.name, `Computed child collection alias "${declaration.name.text}" must be used exactly once`)
@@ -4568,7 +2303,7 @@ function keyedListParts(expression, setters, declarations, fail, aliases = new S
4568
2303
  function nestedKeyedListParts(expression, parentItem, fail) {
4569
2304
  const value = unwrapExpression(expression)
4570
2305
  if (!ts.isCallExpression(value) || value.arguments.length !== 1 || !ts.isPropertyAccessExpression(value.expression) || value.expression.name.text !== "map") return undefined
4571
- let collection = renderedCollectionSource(value.expression.expression, new Map(), undefined, fail, new Set())
2306
+ let collection = analyzeCollectionPipeline(value.expression.expression, { fail })
4572
2307
  if (!collection?.ownerField || collection.parentItem !== parentItem) return undefined
4573
2308
  let callback = value.arguments[0]
4574
2309
  const parameters = collectionParameters(callback, "Nested keyed list map", fail)
@@ -4600,175 +2335,13 @@ function conditionalKeyedMapRoot(callback, root, parameters, collection, fail, s
4600
2335
  }
4601
2336
  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")
4602
2337
  const selectorStates = new Set(collection.selectorStates)
4603
- const selector = collectionExpression(condition, parameters, fail, stateNames, selectorStates)
2338
+ const selector = collectionExpression(condition, { parameters, fail, stateNames, selectorStates })
4604
2339
  const normalized = factory.updateArrowFunction(callback, callback.modifiers, callback.typeParameters, callback.parameters, callback.type, callback.equalsGreaterThanToken, rendered)
4605
2340
  ts.setParentRecursive(normalized, false)
4606
2341
  normalized.parent = parent
4607
2342
  return { callback: normalized, root: rendered, collection: { ...collection, selector: [...collection.selector, ["filter", selector]], selectorStates } }
4608
2343
  }
4609
2344
 
4610
- function renderedCollectionSource(expression, setters, declarations, fail, aliases, importedCollections = new Set(), stateNames = new Set(), importedCollectionTransforms = new Map(), factory = ts.factory, context, calculatedCollection, staticCollection) {
4611
- const value = unwrapExpression(expression)
4612
- if (ts.isIdentifier(value)) {
4613
- if ([...setters.values()].includes(value.text)) {
4614
- const localStatic = staticCollection?.(value.text)
4615
- return { state: value, static: localStatic, localStatic, selector: [], selectorStates: new Set() }
4616
- }
4617
- if (importedCollections.has(value.text)) return { state: value, static: true, selector: [], selectorStates: new Set() }
4618
- const entries = declarations?.get(value.text)
4619
- if (!entries) return undefined
4620
- if (entries.length !== 1 || aliases.has(value.text) || entries[0].node.parent?.parent?.parent !== nearestFunction(entries[0].node)?.body) fail(value, `Rendered collection alias "${value.text}" must be one top-level immutable local`)
4621
- aliases.add(value.text)
4622
- const source = renderedCollectionSource(entries[0].initializer, setters, declarations, fail, aliases, importedCollections, stateNames, importedCollectionTransforms, factory, context, calculatedCollection, staticCollection)
4623
- aliases.delete(value.text)
4624
- return source && { ...source, aliasDeclarations: [...(source.aliasDeclarations ?? []), entries[0].node], aliasUses: [...(source.aliasUses ?? []), value] }
4625
- }
4626
- if (ts.isPropertyAccessExpression(value) && ts.isIdentifier(value.expression)) {
4627
- const calculation = calculatedCollection?.(value)
4628
- if (calculation) return { calculation, selector: [], selectorStates: new Set() }
4629
- return { state: undefined, ownerField: value.name.text, selector: [], parentItem: value.expression.text }
4630
- }
4631
- if (ts.isCallExpression(value) && ts.isIdentifier(value.expression) && importedCollectionTransforms.has(value.expression.text)) {
4632
- const transform = importedCollectionTransforms.get(value.expression.text)
4633
- const parameter = transform.parameters[0]
4634
- if (value.arguments.length !== 1 || transform.parameters.length !== 1 || transform.asteriskToken || transform.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword) || !parameter || !ts.isIdentifier(parameter.name) || parameter.dotDotDotToken || parameter.initializer || parameter.questionToken) fail(value, `Imported collection transform "${value.expression.text}" must be synchronous with exactly one identifier parameter and one argument`)
4635
- const returned = ts.isBlock(transform.body)
4636
- ? transform.body.statements.length === 1 && ts.isReturnStatement(transform.body.statements[0]) ? transform.body.statements[0].expression : undefined
4637
- : transform.body
4638
- if (!returned) fail(value, `Imported collection transform "${value.expression.text}" must contain only one returned collection expression`)
4639
- const transformSource = renderedCollectionSource(returned, new Map([[parameter.name.text, parameter.name.text]]), undefined, fail, new Set(), new Set(), new Set([parameter.name.text]))
4640
- if (!transformSource?.state || transformSource.state.text !== parameter.name.text || transformSource.selectorStates.size) fail(value, `Imported collection transform "${value.expression.text}" must return a supported pure pipeline rooted only in its parameter`)
4641
- const source = renderedCollectionSource(value.arguments[0], setters, declarations, fail, aliases, importedCollections, stateNames, importedCollectionTransforms, factory, context, calculatedCollection, staticCollection)
4642
- if (!source) fail(value.arguments[0], `Imported collection transform "${value.expression.text}" requires a supported collection argument`)
4643
- return { ...source, selector: [...source.selector, ...transformSource.selector] }
4644
- }
4645
- if (ts.isCallExpression(value) && ts.isPropertyAccessExpression(value.expression)) {
4646
- const method = value.expression.name.text
4647
- if (method === "filter") {
4648
- if (value.arguments.length !== 1) fail(value, "Rendered collection filter() requires one inline predicate")
4649
- const source = renderedCollectionSource(value.expression.expression, setters, declarations, fail, aliases, importedCollections, stateNames, importedCollectionTransforms, factory, context, calculatedCollection, staticCollection)
4650
- if (!source) return undefined
4651
- const parameters = collectionParameters(value.arguments[0], "Rendered collection filter()", fail)
4652
- const selectorStates = new Set(source.selectorStates)
4653
- return { ...source, selector: [...source.selector, ["filter", collectionExpression(unwrapExpression(value.arguments[0].body), parameters, fail, stateNames, selectorStates)]], selectorStates }
4654
- }
4655
- if (method === "flatMap") {
4656
- if (value.arguments.length !== 1) fail(value, "Rendered collection flatMap() requires one inline projector")
4657
- const source = renderedCollectionSource(value.expression.expression, setters, declarations, fail, aliases, importedCollections, stateNames, importedCollectionTransforms, factory, context, calculatedCollection, staticCollection)
4658
- if (!source) return undefined
4659
- const parameters = collectionParameters(value.arguments[0], "Rendered collection flatMap()", fail)
4660
- const field = directProperty(value.arguments[0].body, parameters.item)
4661
- if (!field) fail(value.arguments[0].body, `Rendered collection flatMap() projector must be ${parameters.item}.<field>`)
4662
- if (["__proto__", "constructor", "prototype"].includes(field)) fail(value.arguments[0].body, `Rendered collection property "${field}" is not supported`)
4663
- return { ...source, selector: [...source.selector, ["flatMap", field]] }
4664
- }
4665
- if (method === "slice") {
4666
- if (value.arguments.length < 1 || value.arguments.length > 2) fail(value, "Rendered collection slice() requires a start and optional end")
4667
- const source = renderedCollectionSource(value.expression.expression, setters, declarations, fail, aliases, importedCollections, stateNames, importedCollectionTransforms, factory, context, calculatedCollection, staticCollection)
4668
- if (!source) return undefined
4669
- const selectorStates = new Set(source.selectorStates)
4670
- const start = collectionExpression(value.arguments[0], {}, fail, stateNames, selectorStates)
4671
- const end = value.arguments[1] && collectionExpression(value.arguments[1], {}, fail, stateNames, selectorStates)
4672
- return { ...source, selector: [...source.selector, ["slice", start, end]], selectorStates }
4673
- }
4674
- if (method === "toSorted") {
4675
- if (value.arguments.length !== 1) fail(value, "Rendered collection toSorted() requires one inline comparator")
4676
- const source = renderedCollectionSource(value.expression.expression, setters, declarations, fail, aliases, importedCollections, stateNames, importedCollectionTransforms, factory, context, calculatedCollection, staticCollection)
4677
- if (!source) return undefined
4678
- const comparator = value.arguments[0]
4679
- const parameters = collectionParameters(comparator, "Rendered collection toSorted()", fail)
4680
- if (comparator.parameters.length !== 2 || ts.isBlock(comparator.body)) fail(comparator, "Rendered collection toSorted() comparator must be a synchronous expression arrow with (left, right) identifier parameters")
4681
- const selectorStates = new Set(source.selectorStates)
4682
- const expression = collectionExpression(comparator.body, parameters, fail, stateNames, selectorStates)
4683
- return { ...source, selector: [...source.selector, ["sort", expression]], selectorStates }
4684
- }
4685
- if (method === "sort") fail(value, "Rendered collections cannot use mutating sort(); use toSorted()")
4686
- }
4687
- if (isArrayFromCall(value)) {
4688
- if (value.arguments.length < 1 || value.arguments.length > 2) fail(value, "Rendered Array.from() requires an anchor and optional inline mapper")
4689
- const source = renderedCollectionSource(value.arguments[0], setters, declarations, fail, aliases, importedCollections, stateNames, importedCollectionTransforms, factory, context, calculatedCollection, staticCollection)
4690
- if (!source) return undefined
4691
- let mapper
4692
- if (value.arguments[1]) {
4693
- const parameters = collectionParameters(value.arguments[1], "Rendered Array.from() mapper", fail)
4694
- const selectorStates = new Set(source.selectorStates)
4695
- mapper = collectionExpression(unwrapExpression(value.arguments[1].body), parameters, fail, stateNames, selectorStates)
4696
- source.selectorStates = selectorStates
4697
- }
4698
- return { ...source, selector: [...source.selector, ["from", mapper]] }
4699
- }
4700
- }
4701
-
4702
- function isArrayFromCall(value) {
4703
- return ts.isCallExpression(value) && ts.isPropertyAccessExpression(value.expression) && ts.isIdentifier(value.expression.expression) && value.expression.expression.text === "Array" && value.expression.name.text === "from"
4704
- }
4705
-
4706
- function collectionParameters(callback, label, fail) {
4707
- if (!ts.isArrowFunction(callback) || callback.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword) || callback.parameters.length < 1 || callback.parameters.length > 2 || callback.parameters.some(parameter => !ts.isIdentifier(parameter.name) || parameter.dotDotDotToken || parameter.initializer || parameter.questionToken)) fail(callback, `${label} callback must be a synchronous arrow function with (item) or (item, index) identifier parameters`)
4708
- return { item: callback.parameters[0].name.text, index: callback.parameters[1]?.name.text }
4709
- }
4710
-
4711
- function collectionExpression(expression, parameters, fail, stateNames = new Set(), selectorStates = new Set()) {
4712
- const encode = node => {
4713
- node = unwrapExpression(node)
4714
- if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) || ts.isNumericLiteral(node)) return ["value", ts.isNumericLiteral(node) ? Number(node.text) : node.text]
4715
- if (node.kind === ts.SyntaxKind.TrueKeyword) return ["value", true]
4716
- if (node.kind === ts.SyntaxKind.FalseKeyword) return ["value", false]
4717
- if (node.kind === ts.SyntaxKind.NullKeyword) return ["value", null]
4718
- if (ts.isIdentifier(node)) {
4719
- if (node.text === parameters.item) return ["item"]
4720
- if (node.text === parameters.index) return ["index"]
4721
- if (node.text === "undefined") return ["undefined"]
4722
- if (stateNames.has(node.text)) {
4723
- selectorStates.add(node.text)
4724
- return ["state", node.text]
4725
- }
4726
- fail(node, `Rendered collection expression identifier "${node.text}" is not allowed`)
4727
- }
4728
- if (ts.isPropertyAccessExpression(node)) {
4729
- if (["__proto__", "constructor", "prototype"].includes(node.name.text)) fail(node, `Rendered collection property "${node.name.text}" is not supported`)
4730
- return ["get", encode(node.expression), node.name.text, Boolean(node.questionDotToken)]
4731
- }
4732
- if (ts.isElementAccessExpression(node)) {
4733
- const key = node.argumentExpression
4734
- if (!ts.isStringLiteral(key) && !ts.isNumericLiteral(key)) fail(node, "Rendered collection computed properties require a direct string or numeric literal key")
4735
- if (ts.isStringLiteral(key) && ["__proto__", "constructor", "prototype"].includes(key.text)) fail(node, `Rendered collection property "${key.text}" is not supported`)
4736
- return ["get", encode(node.expression), ts.isNumericLiteral(key) ? Number(key.text) : key.text, Boolean(node.questionDotToken)]
4737
- }
4738
- if (ts.isPrefixUnaryExpression(node)) {
4739
- const operator = node.operator === ts.SyntaxKind.ExclamationToken ? "!" : node.operator === ts.SyntaxKind.PlusToken ? "+" : node.operator === ts.SyntaxKind.MinusToken ? "-" : undefined
4740
- if (!operator) fail(node, "Rendered collection expression uses an unsupported unary operator")
4741
- return ["unary", operator, encode(node.operand)]
4742
- }
4743
- if (ts.isTypeOfExpression(node)) return ["unary", "typeof", encode(node.expression)]
4744
- if (ts.isBinaryExpression(node)) {
4745
- const operator = node.operatorToken.getText()
4746
- if (!new Set(["&&", "||", "??", "===", "!==", "==", "!=", "<", "<=", ">", ">=", "+", "-", "*", "/", "%"]).has(operator)) fail(node, `Rendered collection expression operator "${operator}" is not supported`)
4747
- return ["binary", operator, encode(node.left), encode(node.right)]
4748
- }
4749
- if (ts.isConditionalExpression(node)) return ["conditional", encode(node.condition), encode(node.whenTrue), encode(node.whenFalse)]
4750
- if (ts.isArrayLiteralExpression(node) && !node.elements.some(ts.isSpreadElement)) return ["array", ...node.elements.map(encode)]
4751
- if (ts.isObjectLiteralExpression(node)) return ["object", ...node.properties.map(property => {
4752
- if (!ts.isPropertyAssignment(property) || !ts.isIdentifier(property.name) && !ts.isStringLiteral(property.name) && !ts.isNumericLiteral(property.name)) fail(property, "Rendered collection mapper objects require direct properties")
4753
- return [property.name.text, encode(property.initializer)]
4754
- })]
4755
- if (ts.isTemplateExpression(node)) return ["template", [node.head.text, ...node.templateSpans.map(span => span.literal.text)], node.templateSpans.map(span => encode(span.expression))]
4756
- if (ts.isCallExpression(node)) {
4757
- if (ts.isIdentifier(node.expression) && ["Boolean", "Number", "String"].includes(node.expression.text)) return ["global", node.expression.text, ...node.arguments.map(encode)]
4758
- if (ts.isPropertyAccessExpression(node.expression)) {
4759
- const method = node.expression.name.text
4760
- if (ts.isIdentifier(node.expression.expression) && node.expression.expression.text === "Math" && pureMathMethods.has(method)) return ["math", method, ...node.arguments.map(encode)]
4761
- if (pureListMethods.has(method)) return ["call", encode(node.expression.expression), method, ...node.arguments.map(encode)]
4762
- if (mutatingListMethods.has(method)) fail(node, `Rendered collection expressions cannot call mutating method "${method}"`)
4763
- }
4764
- fail(node, "Rendered collection expressions cannot call arbitrary functions")
4765
- }
4766
- if (ts.isArrowFunction(node) || ts.isFunctionExpression(node) || ts.isAwaitExpression(node) || ts.isNewExpression(node) || ts.isYieldExpression(node) || ts.isDeleteExpression(node) || ts.isPostfixUnaryExpression(node)) fail(node, "Rendered collection expressions must be pure and synchronous")
4767
- fail(node, "Rendered collection expression is not supported")
4768
- }
4769
- return encode(expression)
4770
- }
4771
-
4772
2345
  function jsonExpression(value, factory) {
4773
2346
  return factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("JSON"), "parse"), undefined, [factory.createStringLiteral(JSON.stringify(value))])
4774
2347
  }
@@ -4851,16 +2424,6 @@ function runtimeImportNames(sourceFile, relative) {
4851
2424
  return names
4852
2425
  }
4853
2426
 
4854
- function referenceIdentifiers(root, name) {
4855
- const references = []
4856
- const visit = node => {
4857
- if (ts.isIdentifier(node) && node.text === name && isReferenceIdentifier(node) && !isShadowedIdentifier(node, root)) references.push(node)
4858
- ts.forEachChild(node, visit)
4859
- }
4860
- visit(root)
4861
- return references
4862
- }
4863
-
4864
2427
  function insideJsxEventHandler(node, root) {
4865
2428
  for (let current = node.parent; current && current !== root.parent; current = current.parent) {
4866
2429
  if (ts.isJsxAttribute(current) && /^on[A-Z]/.test(current.name.text)) return true
@@ -5385,564 +2948,126 @@ function isJsxSyntaxIdentifier(node) {
5385
2948
  const parent = node.parent
5386
2949
  return (ts.isJsxOpeningElement(parent) || ts.isJsxClosingElement(parent) || ts.isJsxSelfClosingElement(parent)) && parent.tagName === node || ts.isJsxAttribute(parent) && parent.name === node
5387
2950
  }
5388
-
5389
- function isFunctionLike(node) {
5390
- return ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node) || ts.isMethodDeclaration(node) || ts.isGetAccessorDeclaration(node) || ts.isSetAccessorDeclaration(node) || ts.isConstructorDeclaration(node)
5391
- }
5392
-
5393
- function isDestructuredParameter(identifier, fn) {
5394
- return fn?.parameters.some(parameter => ts.isObjectBindingPattern(parameter.name) && parameter.name.elements.some(element => ts.isIdentifier(element.name) && element.name.text === identifier.text)) ?? false
5395
- }
5396
-
5397
- function isExportedDeclaration(node) {
5398
- const statement = ts.isVariableDeclaration(node) ? node.parent?.parent : node
5399
- return statement?.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword || modifier.kind === ts.SyntaxKind.DefaultKeyword) ?? false
5400
- }
5401
-
5402
- function jsxTagUses(root, name) {
5403
- const uses = []
5404
- const visit = node => {
5405
- const tag = ts.isJsxElement(node) ? node.openingElement.tagName : ts.isJsxSelfClosingElement(node) ? node.tagName : undefined
5406
- if (tag && ts.isIdentifier(tag) && tag.text === name) uses.push(node)
5407
- ts.forEachChild(node, visit)
5408
- }
5409
- visit(root)
5410
- return uses
5411
- }
5412
-
5413
- const pureListMethods = new Set(["at", "charAt", "charCodeAt", "concat", "endsWith", "includes", "indexOf", "join", "lastIndexOf", "localeCompare", "padEnd", "padStart", "repeat", "replace", "replaceAll", "slice", "startsWith", "substring", "toLowerCase", "toUpperCase", "trim", "trimEnd", "trimStart"])
5414
- const mutatingListMethods = new Set(["copyWithin", "fill", "pop", "push", "reverse", "shift", "sort", "splice", "unshift"])
5415
- const pureMathMethods = new Set(["abs", "ceil", "floor", "max", "min", "pow", "round", "sign", "sqrt", "trunc"])
5416
- const pureListGlobals = new Set(["Boolean", "Infinity", "Math", "NaN", "Number", "String", "undefined"])
5417
- const assignmentOperators = new Set([
5418
- ts.SyntaxKind.EqualsToken, ts.SyntaxKind.PlusEqualsToken, ts.SyntaxKind.MinusEqualsToken,
5419
- ts.SyntaxKind.AsteriskEqualsToken, ts.SyntaxKind.AsteriskAsteriskEqualsToken, ts.SyntaxKind.SlashEqualsToken,
5420
- ts.SyntaxKind.PercentEqualsToken, ts.SyntaxKind.LessThanLessThanEqualsToken, ts.SyntaxKind.GreaterThanGreaterThanEqualsToken,
5421
- ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken, ts.SyntaxKind.AmpersandEqualsToken, ts.SyntaxKind.BarEqualsToken,
5422
- ts.SyntaxKind.CaretEqualsToken, ts.SyntaxKind.BarBarEqualsToken, ts.SyntaxKind.AmpersandAmpersandEqualsToken,
5423
- ts.SyntaxKind.QuestionQuestionEqualsToken
5424
- ])
5425
-
5426
- function validateListExpression(expression, item, source, fail, index, states = new Set()) {
5427
- const visit = node => {
5428
- if (ts.isTypeNode(node)) return
5429
- if (ts.isElementAccessExpression(node) && referencesIdentifier(node.expression, item)) {
5430
- const key = node.argumentExpression
5431
- if (!ts.isStringLiteral(key) && !ts.isNumericLiteral(key)) fail(source, "Derived keyed list item computed properties require a direct string or numeric literal key")
5432
- if (ts.isStringLiteral(key) && ["__proto__", "constructor", "prototype"].includes(key.text)) fail(source, `Derived keyed list item property "${key.text}" is not supported`)
5433
- }
5434
- 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)) {
5435
- fail(source, "Derived keyed list item expressions cannot read __proto__, prototype, or constructor")
5436
- }
5437
- if (ts.isBinaryExpression(node) && assignmentOperators.has(node.operatorToken.kind) || ts.isPostfixUnaryExpression(node) || ts.isPrefixUnaryExpression(node) && [ts.SyntaxKind.PlusPlusToken, ts.SyntaxKind.MinusMinusToken].includes(node.operator)) {
5438
- fail(source, "Derived keyed list item expressions must be pure; assignments and updates are not supported")
5439
- }
5440
- if (ts.isDeleteExpression(node) || ts.isAwaitExpression(node) || ts.isNewExpression(node) || ts.isYieldExpression(node)) {
5441
- fail(source, "Derived keyed list item expressions must be synchronous and side-effect free; delete, await, yield, and new are not supported")
5442
- }
5443
- if (ts.isArrowFunction(node) || ts.isFunctionExpression(node) || ts.isTaggedTemplateExpression(node)) {
5444
- fail(source, "Derived keyed list item expressions cannot create or invoke arbitrary functions")
5445
- }
5446
- if (ts.isCallExpression(node)) {
5447
- if (ts.isPropertyAccessExpression(node.expression)) {
5448
- const method = node.expression.name.text
5449
- if (mutatingListMethods.has(method)) fail(source, `Derived keyed list item expressions cannot call mutating method "${method}"`)
5450
- const receiver = node.expression.expression
5451
- const mathCall = ts.isIdentifier(receiver) && receiver.text === "Math" && pureMathMethods.has(method)
5452
- if (!mathCall && !pureListMethods.has(method)) fail(source, `Derived keyed list item expressions cannot call arbitrary method "${method}"`)
5453
- } else if (!ts.isIdentifier(node.expression) || !["Boolean", "Number", "String"].includes(node.expression.text)) {
5454
- fail(source, "Derived keyed list item expressions cannot call arbitrary functions")
5455
- }
5456
- }
5457
- if (ts.isIdentifier(node) && isReferenceIdentifier(node) && !isJsxSyntaxIdentifier(node) && node.text !== item && node.text !== index && !states.has(node.text) && !pureListGlobals.has(node.text)) {
5458
- fail(source, `Derived keyed list item expression identifier "${node.text}" is not allowed`)
5459
- }
5460
- ts.forEachChild(node, visit)
5461
- }
5462
- visit(expression)
5463
- }
5464
-
5465
- function containsJsx(root) {
5466
- let found = false
5467
- const visit = node => {
5468
- if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node)) found = true
5469
- if (!found) ts.forEachChild(node, visit)
5470
- }
5471
- visit(root)
5472
- return found
5473
- }
5474
-
5475
- function compileListExpression(read, expression, item, factory, listExpressions, handlerUrl, index, states = new Set()) {
5476
- const exportName = `listExpression${listExpressions.length}`
5477
- listExpressions.push({ exportName, expression, item, index, states })
5478
- const arguments_ = [read, factory.createStringLiteral(handlerUrl), factory.createStringLiteral(exportName)]
5479
- if (states.size) arguments_.push(factory.createArrayLiteralExpression([...states].map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), factory.createIdentifier(name)]))))
5480
- return factory.createCallExpression(factory.createIdentifier("__kListExpression"), undefined, arguments_)
5481
- }
5482
-
5483
- function compileListConditional(entry, factory, listExpressions, handlerUrl) {
5484
- const exportName = `listExpression${listExpressions.length}`
5485
- listExpressions.push({ exportName, expression: entry.condition, item: entry.item, index: entry.index })
5486
- const read = factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), entry.condition)
5487
- const thunk = branch => factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), branch)
5488
- return factory.createCallExpression(factory.createIdentifier("__kListConditional"), undefined, [
5489
- factory.createStringLiteral(entry.kind), read, thunk(entry.truthy), thunk(entry.falsy), factory.createStringLiteral(handlerUrl), factory.createStringLiteral(exportName)
5490
- ])
5491
- }
5492
-
5493
- function compileListValue(expression, entry, factory, context, listExpressions, handlerUrl) {
5494
- const rewrite = node => {
5495
- if (ts.isShorthandPropertyAssignment(node) && entry.states?.has(node.name.text)) return factory.createPropertyAssignment(node.name, factory.createPropertyAccessExpression(node.name, "value"))
5496
- if (ts.isIdentifier(node) && entry.states?.has(node.text) && isReferenceIdentifier(node)) return factory.createPropertyAccessExpression(node, "value")
5497
- return ts.visitEachChild(node, rewrite, context)
5498
- }
5499
- const initial = entry.states?.size ? ts.visitNode(expression, rewrite) : expression
5500
- const read = factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), initial)
5501
- return entry.field
5502
- ? factory.createCallExpression(factory.createIdentifier("__kListField"), undefined, [read, factory.createStringLiteral(entry.field)])
5503
- : compileListExpression(read, expression, entry.item, factory, listExpressions, handlerUrl, entry.index, entry.states)
5504
- }
5505
-
5506
- function directProperty(expression, objectName) {
5507
- const value = unwrapExpression(expression)
5508
- if (!ts.isPropertyAccessExpression(value) || !ts.isIdentifier(value.expression)) return undefined
5509
- if (objectName !== undefined && value.expression.text !== objectName) return undefined
5510
- return value.name.text
5511
- }
5512
-
5513
- function keyedListParentTag(node) {
5514
- for (let current = node.parent; current; current = current.parent) {
5515
- if (ts.isJsxElement(current)) return current.openingElement.tagName.getText().toLowerCase()
5516
- }
5517
- return undefined
5518
- }
5519
-
5520
- function referencesIdentifier(root, name) {
5521
- let found = false
5522
- const visit = node => {
5523
- if (ts.isIdentifier(node) && node.text === name && isReferenceIdentifier(node)) found = true
5524
- if (!found) ts.forEachChild(node, visit)
5525
- }
5526
- visit(root)
5527
- return found
5528
- }
5529
-
5530
- function identifierReferenceCount(root, name) {
5531
- return identifierReferences(root, name).length
5532
- }
5533
-
5534
- function identifierReferences(root, name) {
5535
- const references = []
5536
- const visit = node => {
5537
- if (ts.isIdentifier(node) && node.text === name && isReferenceIdentifier(node) && !ts.isJsxClosingElement(node.parent)) references.push(node)
5538
- ts.forEachChild(node, visit)
5539
- }
5540
- visit(root)
5541
- return references
5542
- }
5543
-
5544
- function unwrapExpression(node) {
5545
- return ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isTypeAssertionExpression(node) || ts.isNonNullExpression(node) || ts.isSatisfiesExpression(node) ? unwrapExpression(node.expression) : node
5546
- }
5547
-
5548
- function isLocalConst(node) {
5549
- const list = node.parent
5550
- const statement = list?.parent
5551
- return ts.isVariableDeclarationList(list) && (list.flags & ts.NodeFlags.Const) !== 0 && ts.isVariableStatement(statement)
5552
- }
5553
-
5554
- function isJsxLocalValue(expression, known) {
5555
- const value = unwrapExpression(expression)
5556
- if (ts.isJsxElement(value) || ts.isJsxSelfClosingElement(value) || ts.isJsxFragment(value)) return true
5557
- if (ts.isIdentifier(value)) return known.has(value.text)
5558
- const parts = conditionalParts(value)
5559
- return Boolean(parts && (isJsxLocalValue(parts.truthy, known) || isJsxLocalValue(parts.falsy, known)))
5560
- }
5561
-
5562
- function compileReactiveBinding(expression, setters, factory, context, reactiveBindings, handlerUrl, importBindings = new Map(), clientImports = new Set()) {
5563
- const parts = conditionalParts(expression)
5564
- const state = parts && directStateIdentifier(parts.condition, setters)
5565
- if (state && isPrimitiveDefaultLiteral(parts.truthy) && isPrimitiveDefaultLiteral(parts.falsy)) {
5566
- return factory.createCallExpression(factory.createIdentifier("__kSelect"), undefined, [state, parts.truthy, parts.falsy])
5567
- }
5568
- return factory.createCallExpression(factory.createIdentifier("__kBinding"), undefined, compileReactiveExpression(expression, setters, factory, context, reactiveBindings, handlerUrl, importBindings, clientImports))
5569
- }
5570
-
5571
- function compileConditional(kind, expression, truthy, falsy, setters, factory, context, reactiveBindings, handlerUrl) {
5572
- const state = directStateIdentifier(expression, setters)
5573
- const thunk = branch => factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), branch)
5574
- if (state) return factory.createCallExpression(factory.createIdentifier("__kStateConditional"), undefined, [factory.createStringLiteral(kind), state, thunk(truthy), thunk(falsy)])
5575
- const [initial, ...descriptor] = compileReactiveExpression(expression, setters, factory, context, reactiveBindings, handlerUrl)
5576
- return factory.createCallExpression(factory.createIdentifier("__kConditional"), undefined, [factory.createStringLiteral(kind), initial, thunk(truthy), thunk(falsy), ...descriptor])
5577
- }
5578
-
5579
- function directStateIdentifier(expression, setters) {
5580
- const value = unwrapExpression(expression)
5581
- return ts.isIdentifier(value) && new Set(setters.values()).has(value.text) ? value : undefined
5582
- }
5583
-
5584
- function compileReactiveExpression(expression, setters, factory, context, reactiveBindings, handlerUrl, importBindings = new Map(), clientImports = new Set()) {
5585
- const usedStates = referencedStateNames(expression, setters)
5586
- const importedNames = referencedImportedBindings(expression, importBindings)
5587
- const imports = [...importedNames].map(name => importBindings.get(name))
5588
- for (const entry of imports) if (!entry.package) clientImports.add(entry.target)
5589
- const captures = new Set([...captureNames(expression, expression, setters)].filter(name => !importedNames.has(name)))
5590
- const exportName = `binding${reactiveBindings.length}`
5591
- reactiveBindings.push({ exportName, expression, captures, states: usedStates, imports })
5592
- const states = [...usedStates].map(name => factory.createArrayLiteralExpression([
5593
- factory.createStringLiteral(name),
5594
- factory.createIdentifier(name)
5595
- ]))
5596
- const scope = [...captures].map(name => factory.createArrayLiteralExpression([
5597
- factory.createStringLiteral(name),
5598
- factory.createIdentifier(name)
5599
- ]))
5600
- const stateNames = new Set(usedStates)
5601
- const rewriteInitial = node => {
5602
- if (ts.isShorthandPropertyAssignment(node) && stateNames.has(node.name.text)) {
5603
- return factory.createPropertyAssignment(node.name, factory.createPropertyAccessExpression(node.name, "value"))
5604
- }
5605
- if (ts.isIdentifier(node) && stateNames.has(node.text) && isReferenceIdentifier(node) && !isShadowedByParameter(node, expression)) {
5606
- return factory.createPropertyAccessExpression(node, "value")
5607
- }
5608
- if (ts.isShorthandPropertyAssignment(node) && captures.has(node.name.text)) {
5609
- return factory.createPropertyAssignment(node.name, factory.createCallExpression(factory.createIdentifier("__kBindingValue"), undefined, [node.name]))
5610
- }
5611
- if (ts.isIdentifier(node) && captures.has(node.text) && isReferenceIdentifier(node)) {
5612
- return factory.createCallExpression(factory.createIdentifier("__kBindingValue"), undefined, [node])
5613
- }
5614
- return ts.visitEachChild(node, rewriteInitial, context)
5615
- }
5616
- return [
5617
- ts.visitNode(expression, rewriteInitial),
5618
- factory.createStringLiteral(handlerUrl),
5619
- factory.createStringLiteral(exportName),
5620
- factory.createArrayLiteralExpression(states),
5621
- factory.createArrayLiteralExpression(scope)
5622
- ]
5623
- }
5624
-
5625
- function conditionalParts(expression) {
5626
- const unwrap = node => ts.isParenthesizedExpression(node) ? unwrap(node.expression) : node
5627
- const value = unwrap(expression)
5628
- if (ts.isBinaryExpression(value) && value.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
5629
- return { kind: "and", condition: value.left, truthy: unwrap(value.right), falsy: factoryNull() }
5630
- }
5631
- if (ts.isConditionalExpression(value)) {
5632
- return { kind: "ternary", condition: value.condition, truthy: unwrap(value.whenTrue), falsy: unwrap(value.whenFalse) }
5633
- }
5634
- return undefined
5635
- }
5636
-
5637
- function factoryNull() {
5638
- return ts.factory.createNull()
5639
- }
5640
-
5641
- function compileEvent(expression, setters, reducers, functions, factory, nativeHandlers, handlerUrl, listItem, importBindings, clientImports) {
5642
- if (ts.isIdentifier(expression)) expression = functions.get(expression.text)
5643
- if (!expression || (!ts.isArrowFunction(expression) && !ts.isFunctionExpression(expression) && !ts.isFunctionDeclaration(expression))) return undefined
5644
-
5645
- const optimized = referencedReducerDispatches(expression.body, reducers, expression).size ? undefined : compileOptimizedEvent(expression, setters, factory)
5646
- if (optimized) return optimized
5647
-
5648
- rejectWorkerConstructions(expression, expression.getSourceFile(), "Relative TypeScript Worker construction is only supported directly inside an inline useEffect() callback")
5649
- const descriptor = compileNativeCallback(expression, setters, reducers, factory, nativeHandlers, importBindings, clientImports, "handler", listItem)
5650
- return factory.createCallExpression(factory.createIdentifier("__kNativeBehavior"), undefined, [
5651
- factory.createStringLiteral(handlerUrl),
5652
- factory.createStringLiteral(descriptor.exportName),
5653
- descriptor.states,
5654
- descriptor.scope
5655
- ])
5656
- }
5657
-
5658
- function compileNativeCallback(expression, setters, reducers, factory, entries, importBindings, clientImports, prefix, listItem, deferValues = false, snapshotNested = false, liveStates = new Set()) {
5659
- const allCaptures = nativeCaptureNames(expression, setters)
5660
- const usedReducers = referencedReducerDispatches(expression.body, reducers, expression)
5661
- const imports = [...referencedImportedBindings(expression, importBindings)].map(name => importBindings.get(name))
5662
- imports.push(...[...usedReducers].map(name => reducers.get(name).import).filter(Boolean))
5663
- const captures = new Set([...allCaptures].filter(name => !importBindings.has(name) && !usedReducers.has(name)))
5664
- for (const entry of imports) if (!entry.package) clientImports.add(entry.target)
5665
- const usedStates = nativeStateNames(expression, setters)
5666
- for (const name of usedReducers) {
5667
- const reducer = reducers.get(name)
5668
- if (reducer.contextAction) for (const state of referencedStateNames(reducer.contextAction.body, reducer.states, reducer.contextAction)) usedStates.add(state)
5669
- }
5670
- const exportName = `${prefix}${entries.length}`
5671
- entries.push({ exportName, expression, captures, imports, liveStates, setters: new Map([...setters].filter(([, state]) => usedStates.has(state))), reducers: new Map([...reducers].filter(([name]) => usedReducers.has(name))), snapshotNested })
5672
- const value = name => deferValues
5673
- ? factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), factory.createIdentifier(name))
5674
- : factory.createIdentifier(name)
5675
- return {
5676
- exportName,
5677
- states: factory.createArrayLiteralExpression([...usedStates].map(name => factory.createArrayLiteralExpression([
5678
- factory.createStringLiteral(name),
5679
- value(name)
5680
- ]))),
5681
- scope: factory.createArrayLiteralExpression([...captures].map(name => factory.createArrayLiteralExpression([
5682
- factory.createStringLiteral(name),
5683
- name === (typeof listItem === "string" ? listItem : listItem?.item)
5684
- ? factory.createCallExpression(factory.createIdentifier("__kListItem"), undefined, [])
5685
- : name === listItem?.index
5686
- ? factory.createCallExpression(factory.createIdentifier("__kListIndex"), undefined, [])
5687
- : value(name)
5688
- ])))
5689
- }
5690
- }
5691
-
5692
- function referencedReducerDispatches(root, reducers, scopeRoot = root) {
5693
- const used = new Set()
5694
- const visit = node => {
5695
- if (ts.isIdentifier(node) && reducers.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, scopeRoot)) used.add(node.text)
5696
- ts.forEachChild(node, visit)
5697
- }
5698
- visit(root)
5699
- return used
5700
- }
5701
-
5702
- function nativeStateNames(expression, setters) {
5703
- return referencedStateNames(expression.body, setters, expression)
5704
- }
5705
-
5706
- function referencedStateNames(root, setters, scopeRoot = root) {
5707
- const stateNames = new Set(setters.values())
5708
- const used = new Set()
5709
- const visit = node => {
5710
- if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && setters.has(node.expression.text) && !isShadowedIdentifier(node.expression, scopeRoot)) used.add(setters.get(node.expression.text))
5711
- if (ts.isIdentifier(node) && setters.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, scopeRoot)) used.add(setters.get(node.text))
5712
- if (ts.isIdentifier(node) && stateNames.has(node.text) && isReferenceIdentifier(node) && !isShadowedByParameter(node, scopeRoot)) used.add(node.text)
5713
- ts.forEachChild(node, visit)
5714
- }
5715
- visit(root)
5716
- return used
5717
- }
5718
-
5719
- function compileOptimizedEvent(expression, setters, factory) {
5720
- const statements = ts.isBlock(expression.body) ? expression.body.statements : [factory.createExpressionStatement(expression.body)]
5721
- const commands = statements.map(statement => {
5722
- if (!ts.isExpressionStatement(statement)) return undefined
5723
- return compileEventCommand(statement.expression, setters, factory)
5724
- })
5725
- if (!commands.length || commands.some(command => !command)) return undefined
5726
-
5727
- return factory.createCallExpression(factory.createIdentifier("__kBehavior"), undefined, [factory.createArrayLiteralExpression(commands)])
5728
- }
5729
-
5730
- const nativeGlobals = new Set([
5731
- "Array", "ArrayBuffer", "BigInt", "Blob", "Boolean", "Date", "Error", "Event", "FileReader", "FormData", "Infinity", "IntersectionObserver", "Intl", "JSON", "Map", "Math", "NaN", "Number", "Object", "Promise", "Proxy", "RangeError", "ReferenceError", "Reflect", "RegExp", "Set", "String", "Symbol", "TypeError", "URL", "URLSearchParams", "WeakMap", "WeakSet", "WebSocket", "Worker", "alert", "atob", "btoa", "cancelAnimationFrame", "clearInterval", "clearTimeout", "console", "crypto", "document", "fetch", "globalThis", "history", "isFinite", "isNaN", "localStorage", "location", "navigator", "parseFloat", "parseInt", "performance", "queueMicrotask", "requestAnimationFrame", "setInterval", "setTimeout", "structuredClone", "undefined", "window"
5732
- ])
5733
-
5734
- function rewriteEffectWorkers(callback, file, sourceFile, sourceFiles, workerReferences, factory, context) {
5735
- const visit = node => {
5736
- const candidate = relativeWorkerCandidate(node, sourceFile)
5737
- if (candidate) {
5738
- if (nearestFunction(node) !== callback) throw sourceNodeError(node, sourceFile, "Relative TypeScript Worker construction must be directly inside the inline useEffect() callback, not a nested function")
5739
- const { worker, url, specifier, options } = validateWorkerCandidate(candidate, sourceFile)
5740
- const target = resolve(dirname(file), specifier)
5741
- const sourceRelative = relative(sourceDirectory, target)
5742
- if (sourceRelative.startsWith(`..${sep}`) || sourceRelative === ".." || resolve(sourceDirectory, sourceRelative) !== target) throw sourceNodeError(url.arguments[0], sourceFile, "Relative TypeScript Worker source must remain under src/")
5743
- if (!sourceFiles.has(target)) throw sourceNodeError(url.arguments[0], sourceFile, `Relative TypeScript Worker ${JSON.stringify(specifier)} must resolve to an existing .worker.ts file under src/`)
5744
- const identity = `${sourceRelative.replaceAll(sep, "/")}:${ts.getOriginalNode(node).getStart(sourceFile)}`
5745
- const placeholder = `/__kudzu_worker_${createHash("sha256").update(identity).digest("hex").slice(0, 16)}__.js`
5746
- workerReferences.push({ root: target, placeholder })
5747
- return factory.updateNewExpression(worker, worker.expression, worker.typeArguments, [factory.createStringLiteral(placeholder), options])
5748
- }
5749
- return ts.visitEachChild(node, visit, context)
5750
- }
5751
- return ts.visitEachChild(callback, visit, context)
5752
- }
5753
-
5754
- function rejectWorkerConstructions(expression, sourceFile, message) {
5755
- const visit = node => {
5756
- if (relativeWorkerCandidate(node, sourceFile)) throw sourceNodeError(node, sourceFile, message)
5757
- ts.forEachChild(node, visit)
5758
- }
5759
- visit(expression.body ?? expression)
5760
- }
5761
-
5762
- function relativeWorkerCandidate(node, sourceFile) {
5763
- if (!ts.isNewExpression(node) || !ts.isIdentifier(node.expression) || node.expression.text !== "Worker") return undefined
5764
- const first = node.arguments?.[0]
5765
- if (!first || !ts.isNewExpression(first) || !ts.isIdentifier(first.expression) || first.expression.text !== "URL") return undefined
5766
- const specifier = first.arguments?.[0]
5767
- const base = first.arguments?.[1]
5768
- const relativeLiteral = ts.isStringLiteral(specifier) && (specifier.text.startsWith("./") || specifier.text.startsWith("../"))
5769
- if (!relativeLiteral && !(specifier && !ts.isStringLiteral(specifier) && base && isImportMetaUrl(base))) return undefined
5770
- return { worker: node, url: first, sourceFile }
5771
- }
5772
-
5773
- function validateWorkerCandidate(candidate, sourceFile) {
5774
- const { worker, url } = candidate
5775
- if (!isUnshadowedGlobal(worker.expression, sourceFile)) throw sourceNodeError(worker.expression, sourceFile, "Relative TypeScript Workers require the unshadowed global Worker constructor")
5776
- if (!isUnshadowedGlobal(url.expression, sourceFile)) throw sourceNodeError(url.expression, sourceFile, "Relative TypeScript Workers require the unshadowed global URL constructor")
5777
- if (url.arguments?.length !== 2 || !isImportMetaUrl(url.arguments[1])) throw sourceNodeError(url, sourceFile, "Relative TypeScript Workers require new URL(relativeLiteral, import.meta.url)")
5778
- const specifierNode = url.arguments[0]
5779
- if (!ts.isStringLiteral(specifierNode) || !(specifierNode.text.startsWith("./") || specifierNode.text.startsWith("../"))) throw sourceNodeError(specifierNode, sourceFile, "Relative TypeScript Worker paths must be relative string literals")
5780
- if (/[\\?#]/.test(specifierNode.text) || !specifierNode.text.endsWith(".worker.ts")) throw sourceNodeError(specifierNode, sourceFile, "Relative TypeScript Worker paths must end in .worker.ts")
5781
- if (worker.arguments?.length !== 2) throw sourceNodeError(worker, sourceFile, 'Relative TypeScript Workers require exactly { type: "module" } as the second argument')
5782
- const options = worker.arguments[1]
5783
- if (!ts.isObjectLiteralExpression(options) || options.properties.length !== 1) throw sourceNodeError(options, sourceFile, 'Relative TypeScript Workers require exactly { type: "module" } as the second argument')
5784
- const property = options.properties[0]
5785
- const name = ts.isPropertyAssignment(property) && !ts.isComputedPropertyName(property.name) && (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) ? property.name.text : undefined
5786
- if (name !== "type" || !ts.isStringLiteral(property.initializer) || property.initializer.text !== "module") throw sourceNodeError(property, sourceFile, 'Relative TypeScript Workers require exactly { type: "module" } as the second argument')
5787
- return { worker, url, specifier: specifierNode.text, options }
5788
- }
5789
-
5790
- function isImportMetaUrl(node) {
5791
- return ts.isPropertyAccessExpression(node) && node.name.text === "url" && ts.isMetaProperty(node.expression) && node.expression.keywordToken === ts.SyntaxKind.ImportKeyword && node.expression.name.text === "meta"
5792
- }
5793
-
5794
- function isUnshadowedGlobal(identifier, sourceFile) {
5795
- if (isShadowedIdentifier(identifier, sourceFile)) return false
5796
- return !sourceFile.statements.some(statement => {
5797
- if (statementDeclaresName(statement, identifier.text)) return true
5798
- if (!ts.isImportDeclaration(statement) || !statement.importClause) return false
5799
- const clause = statement.importClause
5800
- if (clause.name?.text === identifier.text) return true
5801
- if (clause.namedBindings && ts.isNamespaceImport(clause.namedBindings)) return clause.namedBindings.name.text === identifier.text
5802
- return clause.namedBindings && ts.isNamedImports(clause.namedBindings) ? clause.namedBindings.elements.some(entry => entry.name.text === identifier.text) : false
5803
- })
2951
+
2952
+ function isDestructuredParameter(identifier, fn) {
2953
+ return fn?.parameters.some(parameter => ts.isObjectBindingPattern(parameter.name) && parameter.name.elements.some(element => ts.isIdentifier(element.name) && element.name.text === identifier.text)) ?? false
5804
2954
  }
5805
2955
 
5806
- function nativeCaptureNames(expression, setters) {
5807
- return captureNames(expression, expression.body, setters)
2956
+ function isExportedDeclaration(node) {
2957
+ const statement = ts.isVariableDeclaration(node) ? node.parent?.parent : node
2958
+ return statement?.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword || modifier.kind === ts.SyntaxKind.DefaultKeyword) ?? false
5808
2959
  }
5809
2960
 
5810
- function referencedImportedBindings(expression, imports) {
5811
- const names = new Set()
2961
+ function jsxTagUses(root, name) {
2962
+ const uses = []
5812
2963
  const visit = node => {
5813
- if (ts.isIdentifier(node) && imports.has(node.text) && isReferenceIdentifier(node)) names.add(node.text)
2964
+ const tag = ts.isJsxElement(node) ? node.openingElement.tagName : ts.isJsxSelfClosingElement(node) ? node.tagName : undefined
2965
+ if (tag && ts.isIdentifier(tag) && tag.text === name) uses.push(node)
5814
2966
  ts.forEachChild(node, visit)
5815
2967
  }
5816
- visit(expression.body ?? expression)
5817
- return names
2968
+ visit(root)
2969
+ return uses
5818
2970
  }
5819
2971
 
5820
- function captureNames(declarationRoot, referenceRoot, setters) {
5821
- const local = new Set()
5822
- if (!isFunctionLike(declarationRoot)) {
5823
- const collectDeclarations = node => {
5824
- if (ts.isVariableDeclaration(node)) for (const name of bindingNames(node.name)) local.add(name)
5825
- if (ts.isParameter(node)) for (const name of bindingNames(node.name)) local.add(name)
5826
- if ((ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node)) && node.name) local.add(node.name.text)
5827
- ts.forEachChild(node, collectDeclarations)
5828
- }
5829
- collectDeclarations(declarationRoot)
5830
- }
5831
- const stateNames = new Set(setters.values())
5832
- const captures = new Set()
2972
+ const pureListGlobals = new Set(["Boolean", "Infinity", "Math", "NaN", "Number", "String", "undefined"])
2973
+ const assignmentOperators = new Set([
2974
+ ts.SyntaxKind.EqualsToken, ts.SyntaxKind.PlusEqualsToken, ts.SyntaxKind.MinusEqualsToken,
2975
+ ts.SyntaxKind.AsteriskEqualsToken, ts.SyntaxKind.AsteriskAsteriskEqualsToken, ts.SyntaxKind.SlashEqualsToken,
2976
+ ts.SyntaxKind.PercentEqualsToken, ts.SyntaxKind.LessThanLessThanEqualsToken, ts.SyntaxKind.GreaterThanGreaterThanEqualsToken,
2977
+ ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken, ts.SyntaxKind.AmpersandEqualsToken, ts.SyntaxKind.BarEqualsToken,
2978
+ ts.SyntaxKind.CaretEqualsToken, ts.SyntaxKind.BarBarEqualsToken, ts.SyntaxKind.AmpersandAmpersandEqualsToken,
2979
+ ts.SyntaxKind.QuestionQuestionEqualsToken
2980
+ ])
2981
+
2982
+ function validateListExpression(expression, item, source, fail, index, states = new Set()) {
5833
2983
  const visit = node => {
5834
2984
  if (ts.isTypeNode(node)) return
5835
- if (ts.isIdentifier(node)) {
5836
- const declared = isFunctionLike(declarationRoot) ? isShadowedIdentifier(node, declarationRoot) : local.has(node.text)
5837
- if (isReferenceIdentifier(node) && !declared && !setters.has(node.text) && !stateNames.has(node.text) && !nativeGlobals.has(node.text)) captures.add(node.text)
2985
+ if (ts.isElementAccessExpression(node) && referencesIdentifier(node.expression, item)) {
2986
+ const key = node.argumentExpression
2987
+ if (!ts.isStringLiteral(key) && !ts.isNumericLiteral(key)) fail(source, "Derived keyed list item computed properties require a direct string or numeric literal key")
2988
+ if (ts.isStringLiteral(key) && ["__proto__", "constructor", "prototype"].includes(key.text)) fail(source, `Derived keyed list item property "${key.text}" is not supported`)
2989
+ }
2990
+ 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)) {
2991
+ fail(source, "Derived keyed list item expressions cannot read __proto__, prototype, or constructor")
2992
+ }
2993
+ if (ts.isBinaryExpression(node) && assignmentOperators.has(node.operatorToken.kind) || ts.isPostfixUnaryExpression(node) || ts.isPrefixUnaryExpression(node) && [ts.SyntaxKind.PlusPlusToken, ts.SyntaxKind.MinusMinusToken].includes(node.operator)) {
2994
+ fail(source, "Derived keyed list item expressions must be pure; assignments and updates are not supported")
2995
+ }
2996
+ if (ts.isDeleteExpression(node) || ts.isAwaitExpression(node) || ts.isNewExpression(node) || ts.isYieldExpression(node)) {
2997
+ fail(source, "Derived keyed list item expressions must be synchronous and side-effect free; delete, await, yield, and new are not supported")
2998
+ }
2999
+ if (ts.isArrowFunction(node) || ts.isFunctionExpression(node) || ts.isTaggedTemplateExpression(node)) {
3000
+ fail(source, "Derived keyed list item expressions cannot create or invoke arbitrary functions")
3001
+ }
3002
+ if (ts.isCallExpression(node)) {
3003
+ if (ts.isPropertyAccessExpression(node.expression)) {
3004
+ const method = node.expression.name.text
3005
+ if (mutatingListMethods.has(method)) fail(source, `Derived keyed list item expressions cannot call mutating method "${method}"`)
3006
+ const receiver = node.expression.expression
3007
+ const mathCall = ts.isIdentifier(receiver) && receiver.text === "Math" && pureMathMethods.has(method)
3008
+ if (!mathCall && !pureListMethods.has(method)) fail(source, `Derived keyed list item expressions cannot call arbitrary method "${method}"`)
3009
+ } else if (!ts.isIdentifier(node.expression) || !["Boolean", "Number", "String"].includes(node.expression.text)) {
3010
+ fail(source, "Derived keyed list item expressions cannot call arbitrary functions")
3011
+ }
3012
+ }
3013
+ if (ts.isIdentifier(node) && isReferenceIdentifier(node) && !isJsxSyntaxIdentifier(node) && node.text !== item && node.text !== index && !states.has(node.text) && !pureListGlobals.has(node.text)) {
3014
+ fail(source, `Derived keyed list item expression identifier "${node.text}" is not allowed`)
5838
3015
  }
5839
3016
  ts.forEachChild(node, visit)
5840
3017
  }
5841
- visit(referenceRoot)
5842
- return captures
3018
+ visit(expression)
5843
3019
  }
5844
3020
 
5845
- function bindingNames(name) {
5846
- if (ts.isIdentifier(name)) return [name.text]
5847
- return name.elements.flatMap(element => ts.isBindingElement(element) ? bindingNames(element.name) : [])
3021
+ function directProperty(expression, objectName) {
3022
+ const value = unwrapExpression(expression)
3023
+ if (!ts.isPropertyAccessExpression(value) || !ts.isIdentifier(value.expression)) return undefined
3024
+ if (objectName !== undefined && value.expression.text !== objectName) return undefined
3025
+ return value.name.text
5848
3026
  }
5849
3027
 
5850
- function isReferenceIdentifier(node) {
5851
- const parent = node.parent
5852
- if (!parent) return true
5853
- if ((ts.isPropertyAccessExpression(parent) && parent.name === node) ||
5854
- (ts.isPropertyAssignment(parent) && parent.name === node) ||
5855
- (ts.isMethodDeclaration(parent) && parent.name === node) ||
5856
- ((ts.isGetAccessorDeclaration(parent) || ts.isSetAccessorDeclaration(parent)) && parent.name === node) ||
5857
- (ts.isVariableDeclaration(parent) && parent.name === node) ||
5858
- (ts.isParameter(parent) && parent.name === node) ||
5859
- (ts.isFunctionDeclaration(parent) && parent.name === node) ||
5860
- (ts.isJsxAttribute(parent) && parent.name === node) ||
5861
- (ts.isBindingElement(parent) && (parent.name === node || parent.propertyName === node)) ||
5862
- ts.isImportSpecifier(parent) || ts.isImportClause(parent)) return false
5863
- return true
5864
- }
5865
-
5866
- function nearestFunction(node) {
3028
+ function keyedListParentTag(node) {
5867
3029
  for (let current = node.parent; current; current = current.parent) {
5868
- if (ts.isFunctionDeclaration(current) || ts.isFunctionExpression(current) || ts.isArrowFunction(current)) return current
3030
+ if (ts.isJsxElement(current)) return current.openingElement.tagName.getText().toLowerCase()
5869
3031
  }
5870
3032
  return undefined
5871
3033
  }
5872
3034
 
5873
- function nearestFunctionLike(node) {
5874
- for (let current = node.parent; current; current = current.parent) if (isFunctionLike(current)) return current
5875
- return undefined
5876
- }
5877
-
5878
- function isShadowedByParameter(node, scopeRoot) {
5879
- for (let current = node.parent; current; current = current.parent) {
5880
- if (isFunctionLike(current) && current.parameters.some(parameter => bindingNames(parameter.name).includes(node.text))) return true
5881
- if (current === scopeRoot) break
5882
- }
5883
- return false
3035
+ function identifierReferenceCount(root, name) {
3036
+ return identifierReferences(root, name).length
5884
3037
  }
5885
3038
 
5886
- function isShadowedIdentifier(node, scopeRoot) {
5887
- if (isShadowedByParameter(node, scopeRoot)) return true
5888
- if (node === scopeRoot) return false
5889
- if (isFunctionLike(scopeRoot) && scopeRoot.name?.text === node.text) return true
5890
- if (isFunctionLike(scopeRoot) && functionVarDeclaresName(scopeRoot, node.text)) return true
5891
- for (let current = node.parent; current; current = current.parent) {
5892
- if (current === scopeRoot) break
5893
- if (ts.isFunctionExpression(current) && current.name?.text === node.text) return true
5894
- if (ts.isBlock(current) && current.statements.some(statement => statementDeclaresName(statement, node.text))) return true
5895
- if (ts.isCaseBlock(current) && current.clauses.some(clause => clause.statements.some(statement => statementDeclaresName(statement, node.text)))) return true
5896
- if (ts.isCatchClause(current) && current.variableDeclaration && bindingNames(current.variableDeclaration.name).includes(node.text)) return true
5897
- if ((ts.isForStatement(current) || ts.isForInStatement(current) || ts.isForOfStatement(current)) && loopDeclaresName(current, node.text)) return true
5898
- if (isFunctionLike(current) && functionVarDeclaresName(current, node.text)) return true
3039
+ function identifierReferences(root, name) {
3040
+ const references = []
3041
+ const visit = node => {
3042
+ if (ts.isIdentifier(node) && node.text === name && isReferenceIdentifier(node) && !ts.isJsxClosingElement(node.parent)) references.push(node)
3043
+ ts.forEachChild(node, visit)
5899
3044
  }
5900
- return false
3045
+ visit(root)
3046
+ return references
5901
3047
  }
5902
3048
 
5903
- function statementDeclaresName(statement, name) {
5904
- if (ts.isVariableStatement(statement)) return statement.declarationList.declarations.some(declaration => bindingNames(declaration.name).includes(name))
5905
- if (ts.isFunctionDeclaration(statement) || ts.isClassDeclaration(statement) || ts.isImportEqualsDeclaration(statement)) return statement.name?.text === name
5906
- if ((ts.isEnumDeclaration(statement) || ts.isModuleDeclaration(statement)) && !statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.DeclareKeyword)) return ts.isIdentifier(statement.name) && statement.name.text === name
5907
- return false
3049
+ function isJsxLocalValue(expression, known) {
3050
+ const value = unwrapExpression(expression)
3051
+ if (ts.isJsxElement(value) || ts.isJsxSelfClosingElement(value) || ts.isJsxFragment(value)) return true
3052
+ if (ts.isIdentifier(value)) return known.has(value.text)
3053
+ const parts = conditionalParts(value)
3054
+ return Boolean(parts && (isJsxLocalValue(parts.truthy, known) || isJsxLocalValue(parts.falsy, known)))
5908
3055
  }
5909
3056
 
5910
- function rejectOrdinaryWorkerImports(sourceFile, file, sourceFiles) {
5911
- for (const node of sourceFile.statements) {
5912
- let specifier
5913
- let runtime = false
5914
- if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
5915
- specifier = node.moduleSpecifier
5916
- runtime = runtimeModuleReference(node)
5917
- } else if (ts.isImportEqualsDeclaration(node) && ts.isExternalModuleReference(node.moduleReference) && ts.isStringLiteral(node.moduleReference.expression)) {
5918
- specifier = node.moduleReference.expression
5919
- runtime = !node.isTypeOnly
5920
- }
5921
- if (!runtime || !specifier?.text.startsWith(".")) continue
5922
- let target
5923
- try {
5924
- target = resolveSourceImport(file, specifier.text, sourceFiles)
5925
- } catch {
5926
- continue
5927
- }
5928
- if (target.endsWith(".worker.ts")) throw sourceNodeError(specifier, sourceFile, "Worker source modules cannot be imported or re-exported as ordinary runtime modules; use new Worker(new URL(relative.worker.ts, import.meta.url), { type: \"module\" }) inside an inline useEffect() callback")
3057
+ function conditionalParts(expression) {
3058
+ const unwrap = node => ts.isParenthesizedExpression(node) ? unwrap(node.expression) : node
3059
+ const value = unwrap(expression)
3060
+ if (ts.isBinaryExpression(value) && value.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
3061
+ return { kind: "and", condition: value.left, truthy: unwrap(value.right), falsy: factoryNull() }
5929
3062
  }
3063
+ if (ts.isConditionalExpression(value)) {
3064
+ return { kind: "ternary", condition: value.condition, truthy: unwrap(value.whenTrue), falsy: unwrap(value.whenFalse) }
3065
+ }
3066
+ return undefined
5930
3067
  }
5931
3068
 
5932
- function loopDeclaresName(loop, name) {
5933
- const declaration = ts.isForStatement(loop) ? loop.initializer : loop.initializer
5934
- return declaration && ts.isVariableDeclarationList(declaration) && declaration.declarations.some(entry => bindingNames(entry.name).includes(name))
5935
- }
5936
-
5937
- function functionVarDeclaresName(fn, name) {
5938
- let found = false
5939
- const visit = node => {
5940
- if (found || node !== fn.body && isFunctionLike(node)) return
5941
- if (ts.isVariableDeclarationList(node) && (node.flags & ts.NodeFlags.BlockScoped) === 0 && node.declarations.some(entry => bindingNames(entry.name).includes(name))) found = true
5942
- if (!found) ts.forEachChild(node, visit)
5943
- }
5944
- if (fn.body) visit(fn.body)
5945
- return found
3069
+ function factoryNull() {
3070
+ return ts.factory.createNull()
5946
3071
  }
5947
3072
 
5948
3073
  function settersForNode(node, settersByFunction) {
@@ -6097,40 +3222,6 @@ function localComponentDeclaration(sourceFile, name) {
6097
3222
  return undefined
6098
3223
  }
6099
3224
 
6100
- function sourceNodeError(node, fallbackSource, message) {
6101
- const original = ts.getOriginalNode(node)
6102
- const sourceFile = original.getSourceFile?.()?.fileName ? original.getSourceFile() : fallbackSource
6103
- const position = sourceFile.getLineAndCharacterOfPosition(original.getStart(sourceFile))
6104
- return new Error(`${sourceFile.fileName}:${position.line + 1}:${position.character + 1} ${message}`)
6105
- }
6106
-
6107
- function sourceLocation(node, fallbackSource) {
6108
- const original = ts.getOriginalNode(node)
6109
- const sourceFile = original.getSourceFile?.()?.fileName ? original.getSourceFile() : fallbackSource
6110
- const position = sourceFile.getLineAndCharacterOfPosition(original.getStart(sourceFile))
6111
- return `${sourceFile.fileName}:${position.line + 1}:${position.character + 1}`
6112
- }
6113
-
6114
- function effectReturns(callback) {
6115
- let cleanup = false
6116
- let invalid
6117
- const cleanups = []
6118
- const visit = node => {
6119
- if (invalid || node !== callback.body && isFunctionLike(node)) return
6120
- if (ts.isReturnStatement(node) && node.expression) {
6121
- const expression = unwrapExpression(node.expression)
6122
- if (ts.isArrowFunction(expression) || ts.isFunctionExpression(expression)) {
6123
- cleanup = true
6124
- cleanups.push(expression)
6125
- }
6126
- else invalid = node
6127
- }
6128
- if (!invalid) ts.forEachChild(node, visit)
6129
- }
6130
- visit(callback.body)
6131
- return { cleanup, cleanups, invalid }
6132
- }
6133
-
6134
3225
  function validateEffectOwnedBrowserResources(callback, returns, fail) {
6135
3226
  const observers = []
6136
3227
  const frameAssignments = []
@@ -6155,86 +3246,6 @@ function validateEffectOwnedBrowserResources(callback, returns, fail) {
6155
3246
  }
6156
3247
  }
6157
3248
 
6158
- function printClientImports(entries, handlerPath) {
6159
- const unique = new Map(entries.map(entry => [`${entry.target}:${entry.kind}:${entry.imported ?? ""}:${entry.local}`, entry]))
6160
- const groups = Map.groupBy(unique.values(), entry => entry.target)
6161
- const imports = []
6162
- for (const [target, group] of groups) {
6163
- const specifier = group[0].package ? target : relativeModulePath(handlerPath, clientModulePath(target))
6164
- const defaults = group.filter(entry => entry.kind === "default")
6165
- const named = group.filter(entry => entry.kind === "named")
6166
- if (defaults.length === 1 || named.length) imports.push(`import ${defaults.length === 1 ? `${defaults[0].local}${named.length ? ", " : ""}` : ""}${named.length ? `{ ${named.map(entry => entry.imported === entry.local ? entry.local : `${entry.imported} as ${entry.local}`).join(", ")} }` : ""} from ${JSON.stringify(specifier)}`)
6167
- if (defaults.length > 1) for (const entry of defaults) imports.push(`import ${entry.local} from ${JSON.stringify(specifier)}`)
6168
- for (const entry of group.filter(entry => entry.kind === "namespace")) imports.push(`import * as ${entry.local} from ${JSON.stringify(specifier)}`)
6169
- }
6170
- return imports.join("\n")
6171
- }
6172
-
6173
- async function emitWorkers(references, sourceFiles, assetsDirectory, base, minify) {
6174
- const roots = [...new Set(references.map(reference => reference.root))].sort()
6175
- if (!roots.length) return new Map()
6176
- await validateWorkerGraphs(roots, sourceFiles)
6177
- const workerDirectory = join(assetsDirectory, "workers")
6178
- await mkdir(workerDirectory, { recursive: true })
6179
- const result = await bundle({
6180
- absWorkingDir: root,
6181
- entryPoints: roots,
6182
- outbase: sourceDirectory,
6183
- outdir: workerDirectory,
6184
- entryNames: "[dir]/[name]-[hash]",
6185
- chunkNames: "chunks/[name]-[hash]",
6186
- bundle: true,
6187
- splitting: true,
6188
- format: "esm",
6189
- platform: "browser",
6190
- target: "es2022",
6191
- minify,
6192
- legalComments: "none",
6193
- metafile: true,
6194
- logLevel: "silent"
6195
- })
6196
- const emitted = new Map()
6197
- for (const [output, metadata] of Object.entries(result.metafile.outputs)) {
6198
- if (!metadata.entryPoint) continue
6199
- const entry = resolve(root, metadata.entryPoint)
6200
- const rootReferences = references.filter(reference => reference.root === entry)
6201
- const outputFile = resolve(root, output)
6202
- const url = assetPath(base, relative(outputDirectory, outputFile).replaceAll(sep, "/"))
6203
- for (const reference of rootReferences) emitted.set(reference.placeholder, url)
6204
- }
6205
- for (const reference of references) if (!emitted.has(reference.placeholder)) throw new Error(`Worker entry was not emitted: ${relative(root, reference.root)}`)
6206
- return emitted
6207
- }
6208
-
6209
- async function validateWorkerGraphs(roots, sourceFiles) {
6210
- const visited = new Set()
6211
- const queue = [...roots]
6212
- while (queue.length) {
6213
- const file = queue.shift()
6214
- if (visited.has(file)) continue
6215
- visited.add(file)
6216
- const sourceFile = parseSourceFile(file, await readFile(file, "utf8"))
6217
- if (containsJsx(sourceFile)) throw sourceNodeError(sourceFile, sourceFile, "Worker modules must not contain JSX")
6218
- const visit = node => {
6219
- if (ts.isImportEqualsDeclaration(node)) throw sourceNodeError(node, sourceFile, "TypeScript import-equals declarations are not supported in Worker modules; use a relative ESM import")
6220
- if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) throw sourceNodeError(node, sourceFile, "Dynamic imports are not supported in Worker modules")
6221
- if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "require") throw sourceNodeError(node, sourceFile, "require() is not supported in Worker modules")
6222
- ts.forEachChild(node, visit)
6223
- }
6224
- visit(sourceFile)
6225
- for (const node of sourceFile.statements) {
6226
- if ((!ts.isImportDeclaration(node) && !ts.isExportDeclaration(node)) || !node.moduleSpecifier || !ts.isStringLiteral(node.moduleSpecifier) || !runtimeModuleReference(node)) continue
6227
- if (!node.moduleSpecifier.text.startsWith(".")) throw sourceNodeError(node.moduleSpecifier, sourceFile, "Worker modules may only use relative runtime imports")
6228
- try {
6229
- queue.push(resolveSourceImport(file, node.moduleSpecifier.text, sourceFiles))
6230
- } catch (error) {
6231
- const message = error.message.slice(error.message.indexOf("Relative import"))
6232
- throw sourceNodeError(node.moduleSpecifier, sourceFile, message)
6233
- }
6234
- }
6235
- }
6236
- }
6237
-
6238
3249
  async function collectClientModules(entries, sourceFiles) {
6239
3250
  const modules = new Set()
6240
3251
  const queue = [...new Set(entries)]
@@ -6243,7 +3254,7 @@ async function collectClientModules(entries, sourceFiles) {
6243
3254
  if (modules.has(file)) continue
6244
3255
  const source = await readFile(file, "utf8")
6245
3256
  const sourceFile = parseSourceFile(file, source)
6246
- rejectWorkerConstructions(sourceFile, sourceFile, "Relative TypeScript Worker construction is only supported directly inside an inline useEffect() callback, not imported client helpers")
3257
+ workerCompiler.rejectConstructions(sourceFile, sourceFile, "Relative TypeScript Worker construction is only supported directly inside an inline useEffect() callback, not imported client helpers")
6247
3258
  if (containsJsx(sourceFile)) throw new Error(`${relative(root, file)} Imported client helpers must not contain JSX`)
6248
3259
  rejectUnsupportedClientImports(sourceFile, file)
6249
3260
  modules.add(file)
@@ -6570,310 +3581,6 @@ function relativeModulePath(from, to) {
6570
3581
  return path.startsWith(".") ? path : `./${path}`
6571
3582
  }
6572
3583
 
6573
- function printNativeHandler({ exportName, expression, captures, setters, reducers = new Map(), snapshotNested, liveStates = new Set() }) {
6574
- const factory = ts.factory
6575
- const stateNames = new Set(setters.values())
6576
- const snapshotNames = snapshotNested ? nestedStateNames(expression, setters, liveStates) : new Set()
6577
- const snapshots = new Map([...snapshotNames].map(name => [name, factory.createUniqueName("__kEffectState")]))
6578
- const captureSnapshotNames = snapshotNested ? nestedCaptureNames(expression, captures) : new Set()
6579
- const captureSnapshots = new Map([...captureSnapshotNames].map(name => [name, factory.createUniqueName("__kEffectCapture")]))
6580
- const transformer = context => root => {
6581
- const visitor = node => {
6582
- if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && reducers.has(node.expression.text) && !isShadowedIdentifier(node.expression, expression)) {
6583
- const reducer = reducers.get(node.expression.text)
6584
- if (reducer.contextAction) {
6585
- const action = synthesizeTree(cloneAst(reducer.contextAction, factory, context))
6586
- const call = factory.createCallExpression(action, undefined, node.arguments)
6587
- ts.setParentRecursive(call, false)
6588
- return ts.visitNode(call, visitor)
6589
- }
6590
- if (reducer.store) return zustandActionDispatch(factory, reducer, node.arguments.map(argument => ts.visitNode(argument, visitor)))
6591
- if (node.arguments.length !== 1) throw sourceNodeError(node, expression.getSourceFile(), "Reducer dispatches require exactly one action")
6592
- return reducerDispatch(factory, reducer, ts.visitNode(node.arguments[0], visitor))
6593
- }
6594
- if (ts.isShorthandPropertyAssignment(node) && reducers.has(node.name.text) && !isShadowedIdentifier(node.name, expression)) {
6595
- if (reducers.get(node.name.text).contextAction) throw sourceNodeError(node, expression.getSourceFile(), "Context actions must be called directly inside an event handler")
6596
- if (reducers.get(node.name.text).store) throw sourceNodeError(node, expression.getSourceFile(), "Zustand actions must be called directly inside an event handler")
6597
- return factory.createPropertyAssignment(node.name, reducerReference(factory, reducers.get(node.name.text)))
6598
- }
6599
- if (ts.isIdentifier(node) && reducers.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, expression)) {
6600
- if (reducers.get(node.text).contextAction) throw sourceNodeError(node, expression.getSourceFile(), "Context actions must be called directly inside an event handler")
6601
- if (reducers.get(node.text).store) throw sourceNodeError(node, expression.getSourceFile(), "Zustand actions must be called directly inside an event handler")
6602
- return reducerReference(factory, reducers.get(node.text))
6603
- }
6604
- if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && setters.has(node.expression.text) && !isShadowedIdentifier(node.expression, expression)) {
6605
- return factory.createCallExpression(
6606
- factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "set"),
6607
- undefined,
6608
- [factory.createStringLiteral(setters.get(node.expression.text)), ...node.arguments.map(argument => ts.visitNode(argument, visitor))]
6609
- )
6610
- }
6611
- if (ts.isShorthandPropertyAssignment(node) && setters.has(node.name.text) && !isShadowedIdentifier(node.name, expression)) {
6612
- return factory.createPropertyAssignment(node.name, setterReference(factory, setters.get(node.name.text)))
6613
- }
6614
- if (ts.isIdentifier(node) && setters.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, expression)) {
6615
- return setterReference(factory, setters.get(node.text))
6616
- }
6617
- if (ts.isShorthandPropertyAssignment(node) && stateNames.has(node.name.text) && !isShadowedIdentifier(node.name, expression)) {
6618
- if (snapshots.has(node.name.text) && insideNestedFunction(node, expression)) return factory.createPropertyAssignment(node.name, snapshots.get(node.name.text))
6619
- return factory.createPropertyAssignment(node.name, factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "get"), undefined, [factory.createStringLiteral(node.name.text)]))
6620
- }
6621
- if (ts.isIdentifier(node) && stateNames.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, expression)) {
6622
- if (snapshots.has(node.text) && insideNestedFunction(node, expression)) return snapshots.get(node.text)
6623
- return factory.createCallExpression(
6624
- factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "get"),
6625
- undefined,
6626
- [factory.createStringLiteral(node.text)]
6627
- )
6628
- }
6629
- if (ts.isShorthandPropertyAssignment(node) && captures.has(node.name.text) && !isShadowedIdentifier(node.name, expression)) {
6630
- if (captureSnapshots.has(node.name.text) && insideNestedFunction(node, expression)) return factory.createPropertyAssignment(node.name, captureSnapshots.get(node.name.text))
6631
- return factory.createPropertyAssignment(node.name, scopeRead(factory, node.name.text))
6632
- }
6633
- if (ts.isIdentifier(node) && captures.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, expression)) {
6634
- if (captureSnapshots.has(node.text) && insideNestedFunction(node, expression)) return captureSnapshots.get(node.text)
6635
- return scopeRead(factory, node.text)
6636
- }
6637
- return ts.visitEachChild(node, visitor, context)
6638
- }
6639
- return ts.visitNode(root, visitor)
6640
- }
6641
- const transformed = ts.transform(expression.body, [transformer])
6642
- try {
6643
- let body = ts.isBlock(expression.body)
6644
- ? transformed.transformed[0]
6645
- : factory.createBlock([factory.createReturnStatement(transformed.transformed[0])], true)
6646
- const snapshotDeclarations = [
6647
- ...[...snapshots].map(([name, identifier]) => factory.createVariableDeclaration(identifier, undefined, undefined, factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "get"), undefined, [factory.createStringLiteral(name)]))),
6648
- ...[...captureSnapshots].map(([name, identifier]) => factory.createVariableDeclaration(identifier, undefined, undefined, factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "scope"), undefined, [factory.createStringLiteral(name)])))
6649
- ]
6650
- if (snapshotDeclarations.length) body = factory.updateBlock(body, [
6651
- factory.createVariableStatement(undefined, factory.createVariableDeclarationList(snapshotDeclarations, ts.NodeFlags.Const)),
6652
- ...body.statements
6653
- ])
6654
- const modifiers = [factory.createModifier(ts.SyntaxKind.ExportKeyword)]
6655
- if (expression.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) modifiers.push(factory.createModifier(ts.SyntaxKind.AsyncKeyword))
6656
- const declaration = factory.createFunctionDeclaration(
6657
- modifiers,
6658
- expression.asteriskToken,
6659
- exportName,
6660
- undefined,
6661
- [factory.createParameterDeclaration(undefined, undefined, "__k"), ...expression.parameters],
6662
- undefined,
6663
- body
6664
- )
6665
- return ts.createPrinter().printNode(ts.EmitHint.Unspecified, declaration, expression.getSourceFile())
6666
- } finally {
6667
- transformed.dispose()
6668
- }
6669
- }
6670
-
6671
- function nestedCaptureNames(expression, captures) {
6672
- const names = new Set()
6673
- const visit = node => {
6674
- if (ts.isIdentifier(node) && captures.has(node.text) && isReferenceIdentifier(node) && insideNestedFunction(node, expression) && !isShadowedIdentifier(node, expression)) names.add(node.text)
6675
- ts.forEachChild(node, visit)
6676
- }
6677
- visit(expression.body)
6678
- return names
6679
- }
6680
-
6681
- function nestedStateNames(expression, setters, liveStates = new Set()) {
6682
- const states = new Set(setters.values())
6683
- const names = new Set()
6684
- const visit = node => {
6685
- if (ts.isIdentifier(node) && states.has(node.text) && !liveStates.has(node.text) && isReferenceIdentifier(node) && insideNestedFunction(node, expression) && !isShadowedIdentifier(node, expression)) names.add(node.text)
6686
- ts.forEachChild(node, visit)
6687
- }
6688
- visit(expression.body)
6689
- return names
6690
- }
6691
-
6692
- function insideNestedFunction(node, root) {
6693
- for (let current = node.parent; current && current !== root; current = current.parent) {
6694
- if (isFunctionLike(current)) return true
6695
- }
6696
- return false
6697
- }
6698
-
6699
- function setterReference(factory, stateName) {
6700
- return factory.createArrowFunction(
6701
- undefined,
6702
- undefined,
6703
- [factory.createParameterDeclaration(undefined, undefined, "value")],
6704
- undefined,
6705
- factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
6706
- factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "set"), undefined, [factory.createStringLiteral(stateName), factory.createIdentifier("value")])
6707
- )
6708
- }
6709
-
6710
- function reducerReference(factory, reducer) {
6711
- const action = factory.createUniqueName("__kAction")
6712
- return factory.createArrowFunction(undefined, undefined, [factory.createParameterDeclaration(undefined, undefined, action)], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), reducerDispatch(factory, reducer, action))
6713
- }
6714
-
6715
- function reducerDispatch(factory, reducer, action) {
6716
- if (reducer.store) return zustandActionDispatch(factory, reducer, [action])
6717
- const previous = factory.createUniqueName("__kPrevious")
6718
- const update = factory.createArrowFunction(undefined, undefined, [factory.createParameterDeclaration(undefined, undefined, previous)], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), factory.createCallExpression(factory.createIdentifier(reducer.reducer), undefined, [previous, action]))
6719
- return factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "set"), undefined, [factory.createStringLiteral(reducer.state), update])
6720
- }
6721
-
6722
- function zustandActionDispatch(factory, reducer, args) {
6723
- const previous = factory.createUniqueName("__kPrevious")
6724
- const current = factory.createUniqueName("__kStore")
6725
- const updateValue = factory.createUniqueName("__kUpdate")
6726
- const partial = factory.createUniqueName("__kPartial")
6727
- const action = factory.createUniqueName("__kAction")
6728
- const set = factory.createIdentifier(reducer.store.setName)
6729
- const merge = factory.createExpressionStatement(factory.createBinaryExpression(current, factory.createToken(ts.SyntaxKind.EqualsToken), factory.createObjectLiteralExpression([
6730
- factory.createSpreadAssignment(current),
6731
- factory.createSpreadAssignment(partial)
6732
- ])))
6733
- const setBody = factory.createBlock([
6734
- factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(partial, undefined, undefined, factory.createConditionalExpression(
6735
- factory.createBinaryExpression(factory.createTypeOfExpression(updateValue), factory.createToken(ts.SyntaxKind.EqualsEqualsEqualsToken), factory.createStringLiteral("function")),
6736
- undefined,
6737
- factory.createCallExpression(updateValue, undefined, [current]),
6738
- undefined,
6739
- updateValue
6740
- ))], ts.NodeFlags.Const)),
6741
- merge
6742
- ], true)
6743
- const body = factory.createBlock([
6744
- factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(current, undefined, undefined, factory.createObjectLiteralExpression([factory.createPropertyAssignment(reducer.store.field, previous)]))], ts.NodeFlags.Let)),
6745
- factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(set, undefined, undefined, factory.createArrowFunction(undefined, undefined, [factory.createParameterDeclaration(undefined, undefined, updateValue)], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), setBody))], ts.NodeFlags.Const)),
6746
- factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(action, undefined, undefined, synthesizeTree(reducer.store.actions.get(reducer.action)))], ts.NodeFlags.Const)),
6747
- factory.createExpressionStatement(factory.createCallExpression(action, undefined, args)),
6748
- factory.createReturnStatement(factory.createPropertyAccessExpression(current, reducer.store.field))
6749
- ], true)
6750
- const update = factory.createArrowFunction(undefined, undefined, [factory.createParameterDeclaration(undefined, undefined, previous)], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), body)
6751
- return factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "set"), undefined, [factory.createStringLiteral(reducer.state), update])
6752
- }
6753
-
6754
- function printReactiveBinding({ exportName, expression, captures, states }) {
6755
- const factory = ts.factory
6756
- const transformer = context => root => {
6757
- const visitor = node => {
6758
- if (ts.isShorthandPropertyAssignment(node) && states.has(node.name.text)) {
6759
- return factory.createPropertyAssignment(
6760
- node.name,
6761
- factory.createCallExpression(
6762
- factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "get"),
6763
- undefined,
6764
- [factory.createStringLiteral(node.name.text)]
6765
- )
6766
- )
6767
- }
6768
- if (ts.isIdentifier(node) && states.has(node.text) && isReferenceIdentifier(node) && !isShadowedByParameter(node, expression)) {
6769
- return factory.createCallExpression(
6770
- factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "get"),
6771
- undefined,
6772
- [factory.createStringLiteral(node.text)]
6773
- )
6774
- }
6775
- if (ts.isShorthandPropertyAssignment(node) && captures.has(node.name.text)) {
6776
- return factory.createPropertyAssignment(node.name, scopeRead(factory, node.name.text))
6777
- }
6778
- if (ts.isIdentifier(node) && captures.has(node.text) && isReferenceIdentifier(node)) {
6779
- return scopeRead(factory, node.text)
6780
- }
6781
- return ts.visitEachChild(node, visitor, context)
6782
- }
6783
- return ts.visitNode(root, visitor)
6784
- }
6785
- const transformed = ts.transform(expression, [transformer])
6786
- try {
6787
- const declaration = factory.createFunctionDeclaration(
6788
- [factory.createModifier(ts.SyntaxKind.ExportKeyword)],
6789
- undefined,
6790
- exportName,
6791
- undefined,
6792
- [factory.createParameterDeclaration(undefined, undefined, "__k")],
6793
- undefined,
6794
- factory.createBlock([factory.createReturnStatement(transformed.transformed[0])], true)
6795
- )
6796
- return ts.createPrinter().printNode(ts.EmitHint.Unspecified, declaration, expression.getSourceFile())
6797
- } finally {
6798
- transformed.dispose()
6799
- }
6800
- }
6801
-
6802
- function printListExpression({ exportName, expression, item, index, states = new Set() }) {
6803
- const factory = ts.factory
6804
- const transformer = context => root => {
6805
- const visitor = node => {
6806
- if (ts.isShorthandPropertyAssignment(node) && states.has(node.name.text)) {
6807
- return factory.createPropertyAssignment(node.name, factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "get"), undefined, [factory.createStringLiteral(node.name.text)]))
6808
- }
6809
- if (ts.isIdentifier(node) && states.has(node.text) && isReferenceIdentifier(node) && !isShadowedByParameter(node, expression)) {
6810
- return factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "get"), undefined, [factory.createStringLiteral(node.text)])
6811
- }
6812
- return ts.visitEachChild(node, visitor, context)
6813
- }
6814
- return ts.visitNode(root, visitor)
6815
- }
6816
- const transformed = ts.transform(expression, [transformer])
6817
- const declaration = ts.factory.createFunctionDeclaration(
6818
- [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)],
6819
- undefined,
6820
- exportName,
6821
- undefined,
6822
- [ts.factory.createParameterDeclaration(undefined, undefined, item), ts.factory.createParameterDeclaration(undefined, undefined, index ?? "__kIndex"), ts.factory.createParameterDeclaration(undefined, undefined, "__k")],
6823
- undefined,
6824
- ts.factory.createBlock([ts.factory.createReturnStatement(transformed.transformed[0])], true)
6825
- )
6826
- try {
6827
- return ts.createPrinter().printNode(ts.EmitHint.Unspecified, declaration, expression.getSourceFile())
6828
- } finally {
6829
- transformed.dispose()
6830
- }
6831
- }
6832
-
6833
- function scopeRead(factory, name) {
6834
- return factory.createCallExpression(
6835
- factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "scope"),
6836
- undefined,
6837
- [factory.createStringLiteral(name)]
6838
- )
6839
- }
6840
-
6841
- function compileEventCommand(expression, setters, factory) {
6842
- if (ts.isCallExpression(expression) && ts.isPropertyAccessExpression(expression.expression) && ts.isIdentifier(expression.expression.expression) && expression.expression.expression.text === "console" && expression.expression.name.text === "log" && expression.arguments.length === 2 && ts.isStringLiteral(expression.arguments[0]) && ts.isIdentifier(expression.arguments[1]) && [...setters.values()].includes(expression.arguments[1].text)) {
6843
- return command(factory, "log", expression.arguments[1], factory.createStringLiteral(expression.arguments[0].text))
6844
- }
6845
-
6846
- if (!ts.isCallExpression(expression) || !ts.isIdentifier(expression.expression) || expression.arguments.length !== 1) return undefined
6847
- const stateName = setters.get(expression.expression.text)
6848
- if (!stateName) return undefined
6849
-
6850
- const state = factory.createIdentifier(stateName)
6851
- const value = expression.arguments[0]
6852
- if (ts.isBinaryExpression(value) && ts.isIdentifier(value.left) && value.left.text === stateName && ts.isNumericLiteral(value.right)) {
6853
- if (value.operatorToken.kind !== ts.SyntaxKind.PlusToken && value.operatorToken.kind !== ts.SyntaxKind.MinusToken) return undefined
6854
- return command(factory, "add", state, numericExpression(factory, Number(value.right.text), value.operatorToken.kind === ts.SyntaxKind.MinusToken))
6855
- }
6856
- if (ts.isArrowFunction(value) && value.parameters.length === 1 && ts.isIdentifier(value.parameters[0].name) && ts.isBinaryExpression(value.body) && ts.isIdentifier(value.body.left) && value.body.left.text === value.parameters[0].name.text && ts.isNumericLiteral(value.body.right)) {
6857
- if (value.body.operatorToken.kind !== ts.SyntaxKind.PlusToken && value.body.operatorToken.kind !== ts.SyntaxKind.MinusToken) return undefined
6858
- return command(factory, "add", state, numericExpression(factory, Number(value.body.right.text), value.body.operatorToken.kind === ts.SyntaxKind.MinusToken))
6859
- }
6860
- if (isPrimitiveLiteral(value)) return command(factory, "set", state, synthesizeSerializableStateLiteral(value, factory))
6861
- return undefined
6862
- }
6863
-
6864
- function command(factory, operation, state, value) {
6865
- return factory.createArrayLiteralExpression([factory.createStringLiteral(operation), state, value])
6866
- }
6867
-
6868
- function isPrimitiveLiteral(node) {
6869
- return isPrimitiveDefaultLiteral(node)
6870
- }
6871
-
6872
- function numericExpression(factory, value, negative) {
6873
- const literal = factory.createNumericLiteral(value)
6874
- return negative ? factory.createPrefixUnaryExpression(ts.SyntaxKind.MinusToken, literal) : literal
6875
- }
6876
-
6877
3584
  function compiledPath(file) {
6878
3585
  return join(workDirectory, relative(sourceDirectory, file)).replace(/\.(?:ts|tsx)$/, ".mjs")
6879
3586
  }
@@ -7025,6 +3732,25 @@ function withBase(base, path) {
7025
3732
  return base ? `${base}${path}` : path
7026
3733
  }
7027
3734
 
3735
+ const workerCompiler = createWorkerCompiler({
3736
+ root,
3737
+ sourceDirectory,
3738
+ outputDirectory,
3739
+ assetPath,
3740
+ parseSourceFile,
3741
+ resolveSourceImport,
3742
+ runtimeModuleReference
3743
+ })
3744
+
3745
+ const printEffectEntry = createEffectCodegen({ assetPath, inlineJson, relativeModulePath })
3746
+ const printHandlerModule = createHandlerCodegen({
3747
+ cloneAst,
3748
+ synthesizeTree,
3749
+ resolveClientImport: (entry, handlerPath) => entry.package ? entry.target : relativeModulePath(handlerPath, clientModulePath(entry.target))
3750
+ })
3751
+ const { normalizeReactMigrationSyntax, validateUseIdSyntax } = createReactMigrationPass({ cloneAst, jsxTagName })
3752
+ const normalizeReactRouterSyntax = createRouterPass({ withBase })
3753
+
7028
3754
  async function staticPathEntries(module, file) {
7029
3755
  if (typeof module.getStaticPaths !== "function") return [{ params: {}, props: {} }]
7030
3756
  const entries = await module.getStaticPaths()
@@ -7109,13 +3835,3 @@ async function exists(path) {
7109
3835
  return false
7110
3836
  }
7111
3837
  }
7112
-
7113
- function contentType(file) {
7114
- return {
7115
- ".html": "text/html; charset=utf-8",
7116
- ".css": "text/css; charset=utf-8",
7117
- ".js": "text/javascript; charset=utf-8",
7118
- ".json": "application/json; charset=utf-8",
7119
- ".svg": "image/svg+xml"
7120
- }[extname(file)] ?? "application/octet-stream"
7121
- }