@kudzujs/core 0.8.13 → 0.8.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,24 @@
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 { normalizeCustomHookTimerRefs } from "./compiler/custom-hook-timer-pass.mjs"
11
+ import { createEffectCodegen } from "./compiler/effect-codegen.mjs"
12
+ import { createHandlerCodegen } from "./compiler/handler-codegen.mjs"
13
+ import { applyNormalizationPasses } from "./compiler/normalization-pipeline.mjs"
14
+ import { createReactMigrationPass, reactMemoExpression } from "./compiler/react-migration-pass.mjs"
15
+ import { normalizeRenderControlFlow } from "./compiler/render-control-pass.mjs"
16
+ import { createRouterPass } from "./compiler/router-pass.mjs"
17
+ import { createWorkerCompiler } from "./compiler/worker-compiler.mjs"
8
18
  import { renderPage } from "./core.mjs"
9
- import { stateSchema } from "./dev-state.js"
19
+ import { parseDevHost, parseDevPort, startDevServer } from "./dev-server.mjs"
20
+
21
+ export { parseDevHost, parseDevPort }
10
22
 
11
23
  const root = process.cwd()
12
24
  const sourceDirectory = join(root, "src")
@@ -15,8 +27,6 @@ const workDirectory = join(root, ".kudzu")
15
27
  const outputDirectory = join(root, "dist")
16
28
  const staticAssetExtensions = new Set([".avif", ".gif", ".ico", ".jpeg", ".jpg", ".otf", ".png", ".svg", ".ttf", ".webp", ".woff", ".woff2"])
17
29
 
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>`
19
-
20
30
  export async function build({ quiet = false, minify = true } = {}) {
21
31
  const config = await loadConfig()
22
32
  const base = normalizeBase(config.base)
@@ -189,7 +199,7 @@ export async function build({ quiet = false, minify = true } = {}) {
189
199
  const renderedEffects = new Set(plans.flatMap(plan => plan.effects.map(effect => `${effect.module}:${effect.handler}`)))
190
200
  const renderedWorkerReferences = workerReferences.filter(reference => renderedEffects.has(`${reference.module}:${reference.handler}`))
191
201
  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)
202
+ const workerAssets = await workerCompiler.emit(renderedWorkerReferences, sourceFileSet, assetsDirectory, base, minify)
193
203
  for (const module of emittedHandlerModules) {
194
204
  for (const reference of workerReferences) {
195
205
  if (reference.module !== assetPath(base, `assets/${module.path}`)) continue
@@ -399,11 +409,7 @@ export async function build({ quiet = false, minify = true } = {}) {
399
409
  for (const entry of effectEntries) {
400
410
  const output = join(assetsDirectory, entry.path)
401
411
  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)
412
+ await writeJavaScript(output, printEffectEntry(entry.effects, output, emittedHandlerModules, assetsDirectory, base, entry.paramPath, runtimeName(entry.usesDependencyRuntime), entry.navigable), minify)
407
413
  }
408
414
  const clientModules = await collectClientModules(emittedHandlerModules.flatMap(module => module.clientImports), sourceFileSet)
409
415
  for (const file of clientModules) {
@@ -540,559 +546,6 @@ async function printNativeEntry(entry, assetsDirectory, base, minify) {
540
546
  await writeJavaScript(output, `import { registerNativeModules } from ${JSON.stringify(runtime)}\n${imports}\nregisterNativeModules([${registrations}])`, minify)
541
547
  }
542
548
 
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
549
  function runtimeEffects(effects, lifetimes = false) {
1097
550
  return effects.map(effect => ({
1098
551
  module: effect.module,
@@ -1109,328 +562,6 @@ function runtimeEffects(effects, lifetimes = false) {
1109
562
  }))
1110
563
  }
1111
564
 
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
565
  function printParamEntry(schema, params, searchParams, searchParamsWritable, output, assetsDirectory, base, runtimeName, navigable) {
1435
566
  const hasSearch = searchParams.length || searchParamsWritable
1436
567
  const signature = hasSearch ? "pathname, search" : "pathname"
@@ -1543,231 +674,23 @@ async function writeBundledJavaScript(file, source, minify, define) {
1543
674
  await writeFile(file, result.outputFiles[0].contents)
1544
675
  }
1545
676
 
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
677
  export async function dev({ port = parseDevPort(process.env.PORT), host = parseDevHost(process.env.HOST) } = {}) {
1559
678
  if (!Number.isInteger(port) || port < 0 || port > 65535) throw new Error(`Invalid dev server port: ${port}`)
1560
679
  if (typeof host !== "string" || !host.trim()) throw new Error(`Invalid dev server host: ${host}`)
1561
680
  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
- }
681
+ return startDevServer({ build, port, host, base, sourceDirectory, workDirectory, outputDirectory })
1691
682
  }
1692
683
 
1693
- function injectDevClient(html, session, revision, schema) {
1694
- return `${html}${devClient(session, revision, schema).replace("binding|list|native", "binding|deps|list|native")}`
684
+ function inlineJson(value) {
685
+ return JSON.stringify(value).replaceAll("<", "\\u003c").replaceAll("\u2028", "\\u2028").replaceAll("\u2029", "\\u2029")
1695
686
  }
1696
687
 
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")
688
+ function escapeHtml(value) {
689
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;")
1702
690
  }
1703
691
 
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
1747
- }
1748
-
1749
- function inlineJson(value) {
1750
- return JSON.stringify(value).replaceAll("<", "\\u003c").replaceAll("\u2028", "\\u2028").replaceAll("\u2029", "\\u2029")
1751
- }
1752
-
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
- function escapeHtml(value) {
1766
- return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;")
1767
- }
1768
-
1769
- function escapeAttribute(value) {
1770
- return escapeHtml(value).replaceAll('"', "&quot;").replaceAll("'", "&#39;")
692
+ function escapeAttribute(value) {
693
+ return escapeHtml(value).replaceAll('"', "&quot;").replaceAll("'", "&#39;")
1771
694
  }
1772
695
 
1773
696
  async function compile(file, sourceFiles, sourceIndex, staticFiles, importedAssets, cssModules, base, workerReferences) {
@@ -1803,12 +726,7 @@ async function compile(file, sourceFiles, sourceIndex, staticFiles, importedAsse
1803
726
 
1804
727
  if (!nativeHandlers.length && !effectHandlers.length && !reactiveBindings.length && !listExpressions.length) return undefined
1805
728
  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")
729
+ const moduleSource = printHandlerModule({ callbacks, reactiveBindings, listExpressions, handlerPath })
1812
730
  const moduleResult = ts.transpileModule(moduleSource, {
1813
731
  compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext },
1814
732
  reportDiagnostics: true
@@ -1843,7 +761,7 @@ function reachableSourceFiles(entries, sourceFiles, sourceIndex) {
1843
761
  if (specifier && ts.isStringLiteral(specifier) && specifier.text.startsWith(".") && !isStaticImport(specifier.text)) {
1844
762
  try { queue.push(resolveSourceImport(file, specifier.text, sourceFiles)) } catch {}
1845
763
  }
1846
- const worker = relativeWorkerCandidate(node, sourceFile)
764
+ const worker = workerCompiler.candidate(node, sourceFile)
1847
765
  if (worker && ts.isStringLiteral(worker.url.arguments[0]) && worker.url.arguments[0].text.endsWith(".worker.ts")) {
1848
766
  try { queue.push(resolveSourceImport(file, worker.url.arguments[0].text, sourceFiles)) } catch {}
1849
767
  }
@@ -1854,247 +772,6 @@ function reachableSourceFiles(entries, sourceFiles, sourceIndex) {
1854
772
  return [...reachable].sort()
1855
773
  }
1856
774
 
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
2025
- }
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)
2082
- }
2083
- return ts.visitEachChild(node, visitor, context)
2084
- }
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)
2096
- }
2097
-
2098
775
  function normalizeClsxSyntax(sourceFile, factory, context) {
2099
776
  const names = new Set()
2100
777
  for (const statement of sourceFile.statements) {
@@ -2230,272 +907,6 @@ function normalizeZustandMigrationSyntax(sourceFile, factory, context) {
2230
907
  return factory.updateSourceFile(normalized, statements)
2231
908
  }
2232
909
 
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
910
  function normalizeLazyStateInitializers(sourceFile, factory, context, file, sourceFiles, sourceIndex) {
2500
911
  const bindings = new Set()
2501
912
  for (const statement of sourceFile.statements) {
@@ -2525,538 +936,65 @@ function normalizeLazyStateInitializers(sourceFile, factory, context, file, sour
2525
936
  }
2526
937
  if (!declaration || declaration.parameters.length !== 1 || !ts.isIdentifier(declaration.parameters[0].name) || declaration.parameters[0].initializer || declaration.parameters[0].dotDotDotToken || declaration.asteriskToken || declaration.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword) || ts.isFunctionExpression(initializer) && initializer.name) throw sourceNodeError(initializer, sourceFile, "Lazy useReducer() requires one inline, same-file, or relative-imported synchronous one-parameter initializer")
2527
938
  if (!isSerializableStateLiteral(initialArg)) throw sourceNodeError(initialArg, sourceFile, "Lazy useReducer() initial argument must be directly serializable")
2528
- const expression = reactMemoExpression(declaration)
2529
- const lowered = expression && substituteClone(expression, new Map([[declaration.parameters[0].name.text, initialArg]]), factory, context)
2530
- if (!lowered || !isSerializableStateLiteral(lowered)) throw sourceNodeError(initializer, sourceFile, "Lazy useReducer() initializer must directly return a serializable primitive, plain-object, or array literal derived only from its initial argument")
2531
- return factory.updateCallExpression(node, node.expression, node.typeArguments, [node.arguments[0], synthesizeSerializableStateLiteral(lowered, factory)])
2532
- }
2533
- if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && bindings.has(node.expression.text) && !isShadowedIdentifier(node.expression, sourceFile) && node.arguments[0] && (ts.isArrowFunction(node.arguments[0]) || ts.isFunctionExpression(node.arguments[0]))) {
2534
- const initializer = node.arguments[0]
2535
- if (node.arguments.length !== 1 || initializer.parameters.length || initializer.asteriskToken || initializer.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword) || ts.isFunctionExpression(initializer) && initializer.name) throw sourceNodeError(initializer, sourceFile, "Lazy useState() requires one anonymous synchronous zero-parameter initializer")
2536
- const expression = ts.isBlock(initializer.body)
2537
- ? initializer.body.statements.length === 1 && ts.isReturnStatement(initializer.body.statements[0]) ? initializer.body.statements[0].expression : undefined
2538
- : initializer.body
2539
- if (!expression || !isSerializableStateLiteral(expression)) throw sourceNodeError(initializer.body, sourceFile, "Lazy useState() initializer must return one directly serializable primitive, plain-object, or array literal")
2540
- return factory.updateCallExpression(node, node.expression, node.typeArguments, [cloneAst(expression, factory, context)])
2541
- }
2542
- return ts.visitEachChild(node, visitor, context)
2543
- }
2544
- return ts.visitNode(sourceFile, visitor)
2545
- }
2546
-
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)
939
+ const expression = reactMemoExpression(declaration)
940
+ const lowered = expression && substituteClone(expression, new Map([[declaration.parameters[0].name.text, initialArg]]), factory, context)
941
+ if (!lowered || !isSerializableStateLiteral(lowered)) throw sourceNodeError(initializer, sourceFile, "Lazy useReducer() initializer must directly return a serializable primitive, plain-object, or array literal derived only from its initial argument")
942
+ return factory.updateCallExpression(node, node.expression, node.typeArguments, [node.arguments[0], synthesizeSerializableStateLiteral(lowered, factory)])
2992
943
  }
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)
944
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && bindings.has(node.expression.text) && !isShadowedIdentifier(node.expression, sourceFile) && node.arguments[0] && (ts.isArrowFunction(node.arguments[0]) || ts.isFunctionExpression(node.arguments[0]))) {
945
+ const initializer = node.arguments[0]
946
+ if (node.arguments.length !== 1 || initializer.parameters.length || initializer.asteriskToken || initializer.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword) || ts.isFunctionExpression(initializer) && initializer.name) throw sourceNodeError(initializer, sourceFile, "Lazy useState() requires one anonymous synchronous zero-parameter initializer")
947
+ const expression = ts.isBlock(initializer.body)
948
+ ? initializer.body.statements.length === 1 && ts.isReturnStatement(initializer.body.statements[0]) ? initializer.body.statements[0].expression : undefined
949
+ : initializer.body
950
+ if (!expression || !isSerializableStateLiteral(expression)) throw sourceNodeError(initializer.body, sourceFile, "Lazy useState() initializer must return one directly serializable primitive, plain-object, or array literal")
951
+ return factory.updateCallExpression(node, node.expression, node.typeArguments, [cloneAst(expression, factory, context)])
3010
952
  }
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
953
  return ts.visitEachChild(node, visitor, context)
3025
954
  }
3026
955
  return ts.visitNode(sourceFile, visitor)
3027
956
  }
3028
957
 
958
+ function normalizeCompilerSource(sourceFile, { base, context, file, importedCollections, importedStaticCollections, sourceFiles, sourceIndex }) {
959
+ const factory = context.factory
960
+ let customHookTimerStates = new Set()
961
+ sourceFile = applyNormalizationPasses(sourceFile, [
962
+ ...(importedStaticCollections ? [source => normalizeImportedStaticCollections(source, importedStaticCollections, factory, context)] : []),
963
+ source => normalizeReactRouterSyntax(source, factory, context, base),
964
+ source => normalizeClsxSyntax(source, factory, context),
965
+ source => normalizeMediaQueryExternalStores(source, factory, context),
966
+ source => normalizeReactMigrationSyntax(source, factory, context, importedCollections ?? importedSerializableCollectionNames(source, file, sourceFiles, sourceIndex)),
967
+ source => normalizeNavigatorCapabilityConditions(source, factory, context),
968
+ source => normalizeEffectAnimationFrameRefs(source, factory, context),
969
+ source => {
970
+ const result = normalizeCustomHookTimerRefs(source, factory, context)
971
+ customHookTimerStates = result.timerStates
972
+ return result.sourceFile
973
+ },
974
+ source => {
975
+ validateUseIdSyntax(source)
976
+ return source
977
+ },
978
+ source => normalizeLazyStateInitializers(source, factory, context, file, sourceFiles, sourceIndex),
979
+ source => normalizeZustandMigrationSyntax(source, factory, context),
980
+ source => normalizeRenderControlFlow(source, factory, context),
981
+ source => {
982
+ workerCompiler.rejectOrdinaryImports(source, file, sourceFiles)
983
+ return source
984
+ }
985
+ ])
986
+ return { sourceFile, customHookTimerStates }
987
+ }
988
+
3029
989
  function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings, listExpressions, handlerUrl, file, sourceFiles, sourceIndex, staticFiles, importedAssets, cssModules, base, clientImports, workerReferences) {
3030
990
  return context => sourceFile => {
3031
- const factory = context.factory
3032
991
  const hasLinkElements = /<link/i.test(sourceFile.text)
3033
992
  const importedStaticCollections = importedSerializableCollections(sourceFile, file, sourceFiles, sourceIndex)
3034
993
  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)
994
+ const normalized = normalizeCompilerSource(sourceFile, { base, context, file, importedCollections, importedStaticCollections, sourceFiles, sourceIndex })
995
+ sourceFile = normalized.sourceFile
996
+ const { customHookTimerStates } = normalized
997
+ const factory = context.factory
3060
998
  const importBindings = clientImportBindings(sourceFile, file, sourceFiles)
3061
999
  const packageBindings = packageImportBindings(sourceFile)
3062
1000
  for (const [name] of packageBindings) {
@@ -3065,36 +1003,14 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
3065
1003
  if (invalid) throw sourceNodeError(invalid, sourceFile, `Package import ${JSON.stringify(name)} may only be referenced directly inside JSX event handlers`)
3066
1004
  }
3067
1005
  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()
1006
+ const importedSourceCache = new Map()
3070
1007
  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)
1008
+ let result = importedSourceCache.get(target)
1009
+ if (!result) {
1010
+ result = normalizeCompilerSource(parseSourceFile(target, sourceIndex.get(target)), { base, context, file: target, sourceFiles, sourceIndex })
1011
+ importedSourceCache.set(target, result)
3096
1012
  }
3097
- return imported
1013
+ return result.sourceFile
3098
1014
  }
3099
1015
  const importedCollectionTransforms = new Map()
3100
1016
  const importedCalculationFunctions = new Map()
@@ -3255,7 +1171,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
3255
1171
  const capture = nativeCaptureNames(callback, states).values().next().value
3256
1172
  if (capture) throw sourceNodeError(callback, hookSource, `Relative custom hook callback ${JSON.stringify(name)} cannot capture private binding ${JSON.stringify(capture)}`)
3257
1173
  }
3258
- const privateStates = new Set([...states.values()].filter(state => importedTimerStates.get(hookSource.fileName)?.has(state)))
1174
+ const privateStates = new Set([...states.values()].filter(state => importedSourceCache.get(hookSource.fileName)?.customHookTimerStates.has(state)))
3259
1175
  const analysis = { callbacks, fields, privateStates, states }
3260
1176
  customHooks.set(key, analysis)
3261
1177
  return analysis
@@ -4231,6 +2147,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
4231
2147
  const invalidCleanup = returns.cleanups.find(cleanup => cleanup.parameters.length || cleanup.asteriskToken)
4232
2148
  if (invalidCleanup) effectFail(invalidCleanup, "useEffect() cleanup functions cannot declare parameters or be generators")
4233
2149
  if (returns.cleanup && callback.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) effectFail(callback, "useEffect() async callbacks cannot return cleanup functions")
2150
+ validateEffectOwnedBrowserResources(callback, returns, effectFail)
4234
2151
  const callbackSource = specializedEffect?.sourceFile ?? sourceFile
4235
2152
  const callbackFile = callbackSource.fileName
4236
2153
  const workerStart = workerReferences.length
@@ -4241,9 +2158,9 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
4241
2158
  }
4242
2159
  if (listEffect && callbackFile !== file) {
4243
2160
  const originalCallback = listEffect.source.arguments[0]
4244
- 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")
2161
+ 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")
4245
2162
  } else {
4246
- compiledCallback = rewriteEffectWorkers(compiledCallback, callbackFile, callbackSource, sourceFiles, workerReferences, factory, context)
2163
+ compiledCallback = workerCompiler.rewriteEffect(compiledCallback, callbackFile, callbackSource, sourceFiles, workerReferences, factory, context)
4247
2164
  }
4248
2165
  const descriptor = compileNativeCallback(compiledCallback, setters, reducersForNode(node, reducersByFunction), factory, effectHandlers, specializedEffect?.imports ?? importBindings, clientImports, "effect", dependencyItem, true, returns.cleanup, customHookTimerStates)
4249
2166
  for (const reference of workerReferences.slice(workerStart)) Object.assign(reference, { module: handlerUrl, handler: descriptor.exportName })
@@ -4423,100 +2340,6 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
4423
2340
  }
4424
2341
  }
4425
2342
 
4426
- function normalizeRenderControlFlow(sourceFile, factory, context) {
4427
- const normalizeStatements = statements => {
4428
- const nested = statements.map(statement => ts.visitEachChild(statement, visitNested, context))
4429
- const assigned = []
4430
- for (let index = 0; index < nested.length; index++) {
4431
- const statement = nested[index]
4432
- const next = nested[index + 1]
4433
- const declaration = singleUninitializedLet(statement)
4434
- const assignment = declaration && next && assignmentConditional(next, declaration.name.text, factory)
4435
- if (declaration && assignment) {
4436
- const updated = factory.updateVariableDeclaration(declaration, declaration.name, declaration.exclamationToken, declaration.type, assignment)
4437
- const list = factory.createVariableDeclarationList([updated], ts.NodeFlags.Const)
4438
- assigned.push(factory.updateVariableStatement(statement, statement.modifiers, list))
4439
- index++
4440
- } else {
4441
- assigned.push(statement)
4442
- }
4443
- }
4444
-
4445
- if (!assigned.length) return assigned
4446
- const finalIf = returnConditional(assigned.at(-1), factory)
4447
- if (finalIf) return [...assigned.slice(0, -1), factory.createReturnStatement(finalIf)]
4448
- if (!ts.isReturnStatement(assigned.at(-1)) || !assigned.at(-1).expression) return assigned
4449
- let expression = assigned.at(-1).expression
4450
- let start = assigned.length - 1
4451
- while (start > 0) {
4452
- const previous = assigned[start - 1]
4453
- if (!ts.isIfStatement(previous) || previous.elseStatement) break
4454
- const truthy = returnOnlyExpression(previous.thenStatement)
4455
- if (!truthy) break
4456
- expression = factory.createConditionalExpression(previous.expression, factory.createToken(ts.SyntaxKind.QuestionToken), truthy, factory.createToken(ts.SyntaxKind.ColonToken), expression)
4457
- start--
4458
- }
4459
- return start === assigned.length - 1 ? assigned : [...assigned.slice(0, start), factory.createReturnStatement(expression)]
4460
- }
4461
-
4462
- const visitNested = node => {
4463
- if (ts.isBlock(node)) return factory.updateBlock(node, normalizeStatements([...node.statements]))
4464
- if (isFunctionLike(node) && ts.isBlock(node.body)) {
4465
- if (!isRenderFunction(node)) return node
4466
- const body = factory.updateBlock(node.body, normalizeStatements([...node.body.statements]))
4467
- if (ts.isFunctionDeclaration(node)) return factory.updateFunctionDeclaration(node, node.modifiers, node.asteriskToken, node.name, node.typeParameters, node.parameters, node.type, body)
4468
- if (ts.isFunctionExpression(node)) return factory.updateFunctionExpression(node, node.modifiers, node.asteriskToken, node.name, node.typeParameters, node.parameters, node.type, body)
4469
- if (ts.isArrowFunction(node)) return factory.updateArrowFunction(node, node.modifiers, node.typeParameters, node.parameters, node.type, node.equalsGreaterThanToken, body)
4470
- }
4471
- return ts.visitEachChild(node, visitNested, context)
4472
- }
4473
-
4474
- return ts.visitEachChild(sourceFile, visitNested, context)
4475
- }
4476
-
4477
- function isRenderFunction(node) {
4478
- if (ts.isFunctionDeclaration(node)) return node.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.DefaultKeyword) || Boolean(node.name && /^[A-Z]/.test(node.name.text))
4479
- const declaration = node.parent
4480
- return ts.isVariableDeclaration(declaration) && ts.isIdentifier(declaration.name) && /^[A-Z]/.test(declaration.name.text)
4481
- }
4482
-
4483
- function singleUninitializedLet(statement) {
4484
- if (!ts.isVariableStatement(statement) || (statement.declarationList.flags & ts.NodeFlags.Let) === 0 || statement.declarationList.declarations.length !== 1) return undefined
4485
- const declaration = statement.declarationList.declarations[0]
4486
- return ts.isIdentifier(declaration.name) && !declaration.initializer ? declaration : undefined
4487
- }
4488
-
4489
- function assignmentConditional(statement, name, factory) {
4490
- if (!ts.isIfStatement(statement) || !statement.elseStatement) return undefined
4491
- const truthy = assignmentOnlyExpression(statement.thenStatement, name)
4492
- const falsy = ts.isIfStatement(statement.elseStatement)
4493
- ? assignmentConditional(statement.elseStatement, name, factory)
4494
- : assignmentOnlyExpression(statement.elseStatement, name)
4495
- if (!truthy || !falsy) return undefined
4496
- return factory.createConditionalExpression(statement.expression, factory.createToken(ts.SyntaxKind.QuestionToken), truthy, factory.createToken(ts.SyntaxKind.ColonToken), falsy)
4497
- }
4498
-
4499
- function assignmentOnlyExpression(statement, name) {
4500
- const candidate = ts.isBlock(statement) && statement.statements.length === 1 ? statement.statements[0] : statement
4501
- 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
4502
- return candidate.expression.right
4503
- }
4504
-
4505
- function returnConditional(statement, factory) {
4506
- if (!ts.isIfStatement(statement) || !statement.elseStatement) return undefined
4507
- const truthy = returnOnlyExpression(statement.thenStatement)
4508
- const falsy = ts.isIfStatement(statement.elseStatement)
4509
- ? returnConditional(statement.elseStatement, factory)
4510
- : returnOnlyExpression(statement.elseStatement)
4511
- if (!truthy || !falsy) return undefined
4512
- return factory.createConditionalExpression(statement.expression, factory.createToken(ts.SyntaxKind.QuestionToken), truthy, factory.createToken(ts.SyntaxKind.ColonToken), falsy)
4513
- }
4514
-
4515
- function returnOnlyExpression(statement) {
4516
- const candidate = ts.isBlock(statement) && statement.statements.length === 1 ? statement.statements[0] : statement
4517
- return ts.isReturnStatement(candidate) && candidate.expression ? candidate.expression : undefined
4518
- }
4519
-
4520
2343
  function containsRenderControl(root, knownLocals) {
4521
2344
  let found = false
4522
2345
  const visit = node => {
@@ -4850,16 +2673,6 @@ function runtimeImportNames(sourceFile, relative) {
4850
2673
  return names
4851
2674
  }
4852
2675
 
4853
- function referenceIdentifiers(root, name) {
4854
- const references = []
4855
- const visit = node => {
4856
- if (ts.isIdentifier(node) && node.text === name && isReferenceIdentifier(node) && !isShadowedIdentifier(node, root)) references.push(node)
4857
- ts.forEachChild(node, visit)
4858
- }
4859
- visit(root)
4860
- return references
4861
- }
4862
-
4863
2676
  function insideJsxEventHandler(node, root) {
4864
2677
  for (let current = node.parent; current && current !== root.parent; current = current.parent) {
4865
2678
  if (ts.isJsxAttribute(current) && /^on[A-Z]/.test(current.name.text)) return true
@@ -5385,10 +3198,6 @@ function isJsxSyntaxIdentifier(node) {
5385
3198
  return (ts.isJsxOpeningElement(parent) || ts.isJsxClosingElement(parent) || ts.isJsxSelfClosingElement(parent)) && parent.tagName === node || ts.isJsxAttribute(parent) && parent.name === node
5386
3199
  }
5387
3200
 
5388
- function isFunctionLike(node) {
5389
- return ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node) || ts.isMethodDeclaration(node) || ts.isGetAccessorDeclaration(node) || ts.isSetAccessorDeclaration(node) || ts.isConstructorDeclaration(node)
5390
- }
5391
-
5392
3201
  function isDestructuredParameter(identifier, fn) {
5393
3202
  return fn?.parameters.some(parameter => ts.isObjectBindingPattern(parameter.name) && parameter.name.elements.some(element => ts.isIdentifier(element.name) && element.name.text === identifier.text)) ?? false
5394
3203
  }
@@ -5461,16 +3270,6 @@ function validateListExpression(expression, item, source, fail, index, states =
5461
3270
  visit(expression)
5462
3271
  }
5463
3272
 
5464
- function containsJsx(root) {
5465
- let found = false
5466
- const visit = node => {
5467
- if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node)) found = true
5468
- if (!found) ts.forEachChild(node, visit)
5469
- }
5470
- visit(root)
5471
- return found
5472
- }
5473
-
5474
3273
  function compileListExpression(read, expression, item, factory, listExpressions, handlerUrl, index, states = new Set()) {
5475
3274
  const exportName = `listExpression${listExpressions.length}`
5476
3275
  listExpressions.push({ exportName, expression, item, index, states })
@@ -5516,16 +3315,6 @@ function keyedListParentTag(node) {
5516
3315
  return undefined
5517
3316
  }
5518
3317
 
5519
- function referencesIdentifier(root, name) {
5520
- let found = false
5521
- const visit = node => {
5522
- if (ts.isIdentifier(node) && node.text === name && isReferenceIdentifier(node)) found = true
5523
- if (!found) ts.forEachChild(node, visit)
5524
- }
5525
- visit(root)
5526
- return found
5527
- }
5528
-
5529
3318
  function identifierReferenceCount(root, name) {
5530
3319
  return identifierReferences(root, name).length
5531
3320
  }
@@ -5540,16 +3329,6 @@ function identifierReferences(root, name) {
5540
3329
  return references
5541
3330
  }
5542
3331
 
5543
- function unwrapExpression(node) {
5544
- return ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isTypeAssertionExpression(node) || ts.isNonNullExpression(node) || ts.isSatisfiesExpression(node) ? unwrapExpression(node.expression) : node
5545
- }
5546
-
5547
- function isLocalConst(node) {
5548
- const list = node.parent
5549
- const statement = list?.parent
5550
- return ts.isVariableDeclarationList(list) && (list.flags & ts.NodeFlags.Const) !== 0 && ts.isVariableStatement(statement)
5551
- }
5552
-
5553
3332
  function isJsxLocalValue(expression, known) {
5554
3333
  const value = unwrapExpression(expression)
5555
3334
  if (ts.isJsxElement(value) || ts.isJsxSelfClosingElement(value) || ts.isJsxFragment(value)) return true
@@ -5644,7 +3423,7 @@ function compileEvent(expression, setters, reducers, functions, factory, nativeH
5644
3423
  const optimized = referencedReducerDispatches(expression.body, reducers, expression).size ? undefined : compileOptimizedEvent(expression, setters, factory)
5645
3424
  if (optimized) return optimized
5646
3425
 
5647
- rejectWorkerConstructions(expression, expression.getSourceFile(), "Relative TypeScript Worker construction is only supported directly inside an inline useEffect() callback")
3426
+ workerCompiler.rejectConstructions(expression, expression.getSourceFile(), "Relative TypeScript Worker construction is only supported directly inside an inline useEffect() callback")
5648
3427
  const descriptor = compileNativeCallback(expression, setters, reducers, factory, nativeHandlers, importBindings, clientImports, "handler", listItem)
5649
3428
  return factory.createCallExpression(factory.createIdentifier("__kNativeBehavior"), undefined, [
5650
3429
  factory.createStringLiteral(handlerUrl),
@@ -5727,81 +3506,9 @@ function compileOptimizedEvent(expression, setters, factory) {
5727
3506
  }
5728
3507
 
5729
3508
  const nativeGlobals = new Set([
5730
- "Array", "ArrayBuffer", "BigInt", "Blob", "Boolean", "Date", "Error", "Event", "FileReader", "FormData", "Infinity", "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", "queueMicrotask", "requestAnimationFrame", "setInterval", "setTimeout", "structuredClone", "undefined", "window"
3509
+ "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"
5731
3510
  ])
5732
3511
 
5733
- function rewriteEffectWorkers(callback, file, sourceFile, sourceFiles, workerReferences, factory, context) {
5734
- const visit = node => {
5735
- const candidate = relativeWorkerCandidate(node, sourceFile)
5736
- if (candidate) {
5737
- if (nearestFunction(node) !== callback) throw sourceNodeError(node, sourceFile, "Relative TypeScript Worker construction must be directly inside the inline useEffect() callback, not a nested function")
5738
- const { worker, url, specifier, options } = validateWorkerCandidate(candidate, sourceFile)
5739
- const target = resolve(dirname(file), specifier)
5740
- const sourceRelative = relative(sourceDirectory, target)
5741
- if (sourceRelative.startsWith(`..${sep}`) || sourceRelative === ".." || resolve(sourceDirectory, sourceRelative) !== target) throw sourceNodeError(url.arguments[0], sourceFile, "Relative TypeScript Worker source must remain under src/")
5742
- 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/`)
5743
- const identity = `${sourceRelative.replaceAll(sep, "/")}:${ts.getOriginalNode(node).getStart(sourceFile)}`
5744
- const placeholder = `/__kudzu_worker_${createHash("sha256").update(identity).digest("hex").slice(0, 16)}__.js`
5745
- workerReferences.push({ root: target, placeholder })
5746
- return factory.updateNewExpression(worker, worker.expression, worker.typeArguments, [factory.createStringLiteral(placeholder), options])
5747
- }
5748
- return ts.visitEachChild(node, visit, context)
5749
- }
5750
- return ts.visitEachChild(callback, visit, context)
5751
- }
5752
-
5753
- function rejectWorkerConstructions(expression, sourceFile, message) {
5754
- const visit = node => {
5755
- if (relativeWorkerCandidate(node, sourceFile)) throw sourceNodeError(node, sourceFile, message)
5756
- ts.forEachChild(node, visit)
5757
- }
5758
- visit(expression.body ?? expression)
5759
- }
5760
-
5761
- function relativeWorkerCandidate(node, sourceFile) {
5762
- if (!ts.isNewExpression(node) || !ts.isIdentifier(node.expression) || node.expression.text !== "Worker") return undefined
5763
- const first = node.arguments?.[0]
5764
- if (!first || !ts.isNewExpression(first) || !ts.isIdentifier(first.expression) || first.expression.text !== "URL") return undefined
5765
- const specifier = first.arguments?.[0]
5766
- const base = first.arguments?.[1]
5767
- const relativeLiteral = ts.isStringLiteral(specifier) && (specifier.text.startsWith("./") || specifier.text.startsWith("../"))
5768
- if (!relativeLiteral && !(specifier && !ts.isStringLiteral(specifier) && base && isImportMetaUrl(base))) return undefined
5769
- return { worker: node, url: first, sourceFile }
5770
- }
5771
-
5772
- function validateWorkerCandidate(candidate, sourceFile) {
5773
- const { worker, url } = candidate
5774
- if (!isUnshadowedGlobal(worker.expression, sourceFile)) throw sourceNodeError(worker.expression, sourceFile, "Relative TypeScript Workers require the unshadowed global Worker constructor")
5775
- if (!isUnshadowedGlobal(url.expression, sourceFile)) throw sourceNodeError(url.expression, sourceFile, "Relative TypeScript Workers require the unshadowed global URL constructor")
5776
- if (url.arguments?.length !== 2 || !isImportMetaUrl(url.arguments[1])) throw sourceNodeError(url, sourceFile, "Relative TypeScript Workers require new URL(relativeLiteral, import.meta.url)")
5777
- const specifierNode = url.arguments[0]
5778
- if (!ts.isStringLiteral(specifierNode) || !(specifierNode.text.startsWith("./") || specifierNode.text.startsWith("../"))) throw sourceNodeError(specifierNode, sourceFile, "Relative TypeScript Worker paths must be relative string literals")
5779
- if (/[\\?#]/.test(specifierNode.text) || !specifierNode.text.endsWith(".worker.ts")) throw sourceNodeError(specifierNode, sourceFile, "Relative TypeScript Worker paths must end in .worker.ts")
5780
- if (worker.arguments?.length !== 2) throw sourceNodeError(worker, sourceFile, 'Relative TypeScript Workers require exactly { type: "module" } as the second argument')
5781
- const options = worker.arguments[1]
5782
- if (!ts.isObjectLiteralExpression(options) || options.properties.length !== 1) throw sourceNodeError(options, sourceFile, 'Relative TypeScript Workers require exactly { type: "module" } as the second argument')
5783
- const property = options.properties[0]
5784
- const name = ts.isPropertyAssignment(property) && !ts.isComputedPropertyName(property.name) && (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) ? property.name.text : undefined
5785
- 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')
5786
- return { worker, url, specifier: specifierNode.text, options }
5787
- }
5788
-
5789
- function isImportMetaUrl(node) {
5790
- return ts.isPropertyAccessExpression(node) && node.name.text === "url" && ts.isMetaProperty(node.expression) && node.expression.keywordToken === ts.SyntaxKind.ImportKeyword && node.expression.name.text === "meta"
5791
- }
5792
-
5793
- function isUnshadowedGlobal(identifier, sourceFile) {
5794
- if (isShadowedIdentifier(identifier, sourceFile)) return false
5795
- return !sourceFile.statements.some(statement => {
5796
- if (statementDeclaresName(statement, identifier.text)) return true
5797
- if (!ts.isImportDeclaration(statement) || !statement.importClause) return false
5798
- const clause = statement.importClause
5799
- if (clause.name?.text === identifier.text) return true
5800
- if (clause.namedBindings && ts.isNamespaceImport(clause.namedBindings)) return clause.namedBindings.name.text === identifier.text
5801
- return clause.namedBindings && ts.isNamedImports(clause.namedBindings) ? clause.namedBindings.elements.some(entry => entry.name.text === identifier.text) : false
5802
- })
5803
- }
5804
-
5805
3512
  function nativeCaptureNames(expression, setters) {
5806
3513
  return captureNames(expression, expression.body, setters)
5807
3514
  }
@@ -5841,109 +3548,6 @@ function captureNames(declarationRoot, referenceRoot, setters) {
5841
3548
  return captures
5842
3549
  }
5843
3550
 
5844
- function bindingNames(name) {
5845
- if (ts.isIdentifier(name)) return [name.text]
5846
- return name.elements.flatMap(element => ts.isBindingElement(element) ? bindingNames(element.name) : [])
5847
- }
5848
-
5849
- function isReferenceIdentifier(node) {
5850
- const parent = node.parent
5851
- if (!parent) return true
5852
- if ((ts.isPropertyAccessExpression(parent) && parent.name === node) ||
5853
- (ts.isPropertyAssignment(parent) && parent.name === node) ||
5854
- (ts.isMethodDeclaration(parent) && parent.name === node) ||
5855
- ((ts.isGetAccessorDeclaration(parent) || ts.isSetAccessorDeclaration(parent)) && parent.name === node) ||
5856
- (ts.isVariableDeclaration(parent) && parent.name === node) ||
5857
- (ts.isParameter(parent) && parent.name === node) ||
5858
- (ts.isFunctionDeclaration(parent) && parent.name === node) ||
5859
- (ts.isJsxAttribute(parent) && parent.name === node) ||
5860
- (ts.isBindingElement(parent) && (parent.name === node || parent.propertyName === node)) ||
5861
- ts.isImportSpecifier(parent) || ts.isImportClause(parent)) return false
5862
- return true
5863
- }
5864
-
5865
- function nearestFunction(node) {
5866
- for (let current = node.parent; current; current = current.parent) {
5867
- if (ts.isFunctionDeclaration(current) || ts.isFunctionExpression(current) || ts.isArrowFunction(current)) return current
5868
- }
5869
- return undefined
5870
- }
5871
-
5872
- function nearestFunctionLike(node) {
5873
- for (let current = node.parent; current; current = current.parent) if (isFunctionLike(current)) return current
5874
- return undefined
5875
- }
5876
-
5877
- function isShadowedByParameter(node, scopeRoot) {
5878
- for (let current = node.parent; current; current = current.parent) {
5879
- if (isFunctionLike(current) && current.parameters.some(parameter => bindingNames(parameter.name).includes(node.text))) return true
5880
- if (current === scopeRoot) break
5881
- }
5882
- return false
5883
- }
5884
-
5885
- function isShadowedIdentifier(node, scopeRoot) {
5886
- if (isShadowedByParameter(node, scopeRoot)) return true
5887
- if (node === scopeRoot) return false
5888
- if (isFunctionLike(scopeRoot) && scopeRoot.name?.text === node.text) return true
5889
- if (isFunctionLike(scopeRoot) && functionVarDeclaresName(scopeRoot, node.text)) return true
5890
- for (let current = node.parent; current; current = current.parent) {
5891
- if (current === scopeRoot) break
5892
- if (ts.isFunctionExpression(current) && current.name?.text === node.text) return true
5893
- if (ts.isBlock(current) && current.statements.some(statement => statementDeclaresName(statement, node.text))) return true
5894
- if (ts.isCaseBlock(current) && current.clauses.some(clause => clause.statements.some(statement => statementDeclaresName(statement, node.text)))) return true
5895
- if (ts.isCatchClause(current) && current.variableDeclaration && bindingNames(current.variableDeclaration.name).includes(node.text)) return true
5896
- if ((ts.isForStatement(current) || ts.isForInStatement(current) || ts.isForOfStatement(current)) && loopDeclaresName(current, node.text)) return true
5897
- if (isFunctionLike(current) && functionVarDeclaresName(current, node.text)) return true
5898
- }
5899
- return false
5900
- }
5901
-
5902
- function statementDeclaresName(statement, name) {
5903
- if (ts.isVariableStatement(statement)) return statement.declarationList.declarations.some(declaration => bindingNames(declaration.name).includes(name))
5904
- if (ts.isFunctionDeclaration(statement) || ts.isClassDeclaration(statement) || ts.isImportEqualsDeclaration(statement)) return statement.name?.text === name
5905
- 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
5906
- return false
5907
- }
5908
-
5909
- function rejectOrdinaryWorkerImports(sourceFile, file, sourceFiles) {
5910
- for (const node of sourceFile.statements) {
5911
- let specifier
5912
- let runtime = false
5913
- if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
5914
- specifier = node.moduleSpecifier
5915
- runtime = runtimeModuleReference(node)
5916
- } else if (ts.isImportEqualsDeclaration(node) && ts.isExternalModuleReference(node.moduleReference) && ts.isStringLiteral(node.moduleReference.expression)) {
5917
- specifier = node.moduleReference.expression
5918
- runtime = !node.isTypeOnly
5919
- }
5920
- if (!runtime || !specifier?.text.startsWith(".")) continue
5921
- let target
5922
- try {
5923
- target = resolveSourceImport(file, specifier.text, sourceFiles)
5924
- } catch {
5925
- continue
5926
- }
5927
- 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")
5928
- }
5929
- }
5930
-
5931
- function loopDeclaresName(loop, name) {
5932
- const declaration = ts.isForStatement(loop) ? loop.initializer : loop.initializer
5933
- return declaration && ts.isVariableDeclarationList(declaration) && declaration.declarations.some(entry => bindingNames(entry.name).includes(name))
5934
- }
5935
-
5936
- function functionVarDeclaresName(fn, name) {
5937
- let found = false
5938
- const visit = node => {
5939
- if (found || node !== fn.body && isFunctionLike(node)) return
5940
- if (ts.isVariableDeclarationList(node) && (node.flags & ts.NodeFlags.BlockScoped) === 0 && node.declarations.some(entry => bindingNames(entry.name).includes(name))) found = true
5941
- if (!found) ts.forEachChild(node, visit)
5942
- }
5943
- if (fn.body) visit(fn.body)
5944
- return found
5945
- }
5946
-
5947
3551
  function settersForNode(node, settersByFunction) {
5948
3552
  for (let current = node.parent; current; current = current.parent) {
5949
3553
  if (!ts.isFunctionDeclaration(current) && !ts.isFunctionExpression(current) && !ts.isArrowFunction(current)) continue
@@ -6096,117 +3700,27 @@ function localComponentDeclaration(sourceFile, name) {
6096
3700
  return undefined
6097
3701
  }
6098
3702
 
6099
- function sourceNodeError(node, fallbackSource, message) {
6100
- const original = ts.getOriginalNode(node)
6101
- const sourceFile = original.getSourceFile?.()?.fileName ? original.getSourceFile() : fallbackSource
6102
- const position = sourceFile.getLineAndCharacterOfPosition(original.getStart(sourceFile))
6103
- return new Error(`${sourceFile.fileName}:${position.line + 1}:${position.character + 1} ${message}`)
6104
- }
6105
-
6106
- function sourceLocation(node, fallbackSource) {
6107
- const original = ts.getOriginalNode(node)
6108
- const sourceFile = original.getSourceFile?.()?.fileName ? original.getSourceFile() : fallbackSource
6109
- const position = sourceFile.getLineAndCharacterOfPosition(original.getStart(sourceFile))
6110
- return `${sourceFile.fileName}:${position.line + 1}:${position.character + 1}`
6111
- }
6112
-
6113
- function effectReturns(callback) {
6114
- let cleanup = false
6115
- let invalid
6116
- const cleanups = []
3703
+ function validateEffectOwnedBrowserResources(callback, returns, fail) {
3704
+ const observers = []
3705
+ const frameAssignments = []
3706
+ const cancellations = new Set()
3707
+ const disconnected = new Set()
3708
+ const insideCleanup = node => returns.cleanups.some(cleanup => {
3709
+ for (let current = node; current; current = current.parent) if (current === cleanup) return true
3710
+ return false
3711
+ })
6117
3712
  const visit = node => {
6118
- if (invalid || node !== callback.body && isFunctionLike(node)) return
6119
- if (ts.isReturnStatement(node) && node.expression) {
6120
- const expression = unwrapExpression(node.expression)
6121
- if (ts.isArrowFunction(expression) || ts.isFunctionExpression(expression)) {
6122
- cleanup = true
6123
- cleanups.push(expression)
6124
- }
6125
- else invalid = node
6126
- }
6127
- if (!invalid) ts.forEachChild(node, visit)
3713
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isNewExpression(unwrapExpression(node.initializer)) && ts.isIdentifier(unwrapExpression(node.initializer).expression) && unwrapExpression(node.initializer).expression.text === "IntersectionObserver") observers.push(node)
3714
+ if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken && ts.isIdentifier(unwrapExpression(node.left)) && ts.isCallExpression(unwrapExpression(node.right)) && ts.isIdentifier(unwrapExpression(node.right).expression) && unwrapExpression(node.right).expression.text === "requestAnimationFrame") frameAssignments.push(node)
3715
+ if (insideCleanup(node) && ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "cancelAnimationFrame" && node.arguments.length === 1 && ts.isIdentifier(unwrapExpression(node.arguments[0]))) cancellations.add(unwrapExpression(node.arguments[0]).text)
3716
+ if (insideCleanup(node) && ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.expression) && node.expression.name.text === "disconnect" && node.arguments.length === 0) disconnected.add(node.expression.expression.text)
3717
+ ts.forEachChild(node, visit)
6128
3718
  }
6129
3719
  visit(callback.body)
6130
- return { cleanup, cleanups, invalid }
6131
- }
6132
-
6133
- function printClientImports(entries, handlerPath) {
6134
- const unique = new Map(entries.map(entry => [`${entry.target}:${entry.kind}:${entry.imported ?? ""}:${entry.local}`, entry]))
6135
- const groups = Map.groupBy(unique.values(), entry => entry.target)
6136
- const imports = []
6137
- for (const [target, group] of groups) {
6138
- const specifier = group[0].package ? target : relativeModulePath(handlerPath, clientModulePath(target))
6139
- const defaults = group.filter(entry => entry.kind === "default")
6140
- const named = group.filter(entry => entry.kind === "named")
6141
- 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)}`)
6142
- if (defaults.length > 1) for (const entry of defaults) imports.push(`import ${entry.local} from ${JSON.stringify(specifier)}`)
6143
- for (const entry of group.filter(entry => entry.kind === "namespace")) imports.push(`import * as ${entry.local} from ${JSON.stringify(specifier)}`)
6144
- }
6145
- return imports.join("\n")
6146
- }
6147
-
6148
- async function emitWorkers(references, sourceFiles, assetsDirectory, base, minify) {
6149
- const roots = [...new Set(references.map(reference => reference.root))].sort()
6150
- if (!roots.length) return new Map()
6151
- await validateWorkerGraphs(roots, sourceFiles)
6152
- const workerDirectory = join(assetsDirectory, "workers")
6153
- await mkdir(workerDirectory, { recursive: true })
6154
- const result = await bundle({
6155
- absWorkingDir: root,
6156
- entryPoints: roots,
6157
- outbase: sourceDirectory,
6158
- outdir: workerDirectory,
6159
- entryNames: "[dir]/[name]-[hash]",
6160
- chunkNames: "chunks/[name]-[hash]",
6161
- bundle: true,
6162
- splitting: true,
6163
- format: "esm",
6164
- platform: "browser",
6165
- target: "es2022",
6166
- minify,
6167
- legalComments: "none",
6168
- metafile: true,
6169
- logLevel: "silent"
6170
- })
6171
- const emitted = new Map()
6172
- for (const [output, metadata] of Object.entries(result.metafile.outputs)) {
6173
- if (!metadata.entryPoint) continue
6174
- const entry = resolve(root, metadata.entryPoint)
6175
- const rootReferences = references.filter(reference => reference.root === entry)
6176
- const outputFile = resolve(root, output)
6177
- const url = assetPath(base, relative(outputDirectory, outputFile).replaceAll(sep, "/"))
6178
- for (const reference of rootReferences) emitted.set(reference.placeholder, url)
6179
- }
6180
- for (const reference of references) if (!emitted.has(reference.placeholder)) throw new Error(`Worker entry was not emitted: ${relative(root, reference.root)}`)
6181
- return emitted
6182
- }
6183
-
6184
- async function validateWorkerGraphs(roots, sourceFiles) {
6185
- const visited = new Set()
6186
- const queue = [...roots]
6187
- while (queue.length) {
6188
- const file = queue.shift()
6189
- if (visited.has(file)) continue
6190
- visited.add(file)
6191
- const sourceFile = parseSourceFile(file, await readFile(file, "utf8"))
6192
- if (containsJsx(sourceFile)) throw sourceNodeError(sourceFile, sourceFile, "Worker modules must not contain JSX")
6193
- const visit = node => {
6194
- if (ts.isImportEqualsDeclaration(node)) throw sourceNodeError(node, sourceFile, "TypeScript import-equals declarations are not supported in Worker modules; use a relative ESM import")
6195
- if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) throw sourceNodeError(node, sourceFile, "Dynamic imports are not supported in Worker modules")
6196
- if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "require") throw sourceNodeError(node, sourceFile, "require() is not supported in Worker modules")
6197
- ts.forEachChild(node, visit)
6198
- }
6199
- visit(sourceFile)
6200
- for (const node of sourceFile.statements) {
6201
- if ((!ts.isImportDeclaration(node) && !ts.isExportDeclaration(node)) || !node.moduleSpecifier || !ts.isStringLiteral(node.moduleSpecifier) || !runtimeModuleReference(node)) continue
6202
- if (!node.moduleSpecifier.text.startsWith(".")) throw sourceNodeError(node.moduleSpecifier, sourceFile, "Worker modules may only use relative runtime imports")
6203
- try {
6204
- queue.push(resolveSourceImport(file, node.moduleSpecifier.text, sourceFiles))
6205
- } catch (error) {
6206
- const message = error.message.slice(error.message.indexOf("Relative import"))
6207
- throw sourceNodeError(node.moduleSpecifier, sourceFile, message)
6208
- }
6209
- }
3720
+ for (const observer of observers) if (!disconnected.has(observer.name.text)) fail(observer, `IntersectionObserver effects must disconnect ${JSON.stringify(observer.name.text)} in cleanup`)
3721
+ for (const assignment of frameAssignments) {
3722
+ const name = unwrapExpression(assignment.left).text
3723
+ if (!cancellations.has(name)) fail(assignment, `Animation loop effects must cancel ${JSON.stringify(name)} in cleanup`)
6210
3724
  }
6211
3725
  }
6212
3726
 
@@ -6218,7 +3732,7 @@ async function collectClientModules(entries, sourceFiles) {
6218
3732
  if (modules.has(file)) continue
6219
3733
  const source = await readFile(file, "utf8")
6220
3734
  const sourceFile = parseSourceFile(file, source)
6221
- rejectWorkerConstructions(sourceFile, sourceFile, "Relative TypeScript Worker construction is only supported directly inside an inline useEffect() callback, not imported client helpers")
3735
+ workerCompiler.rejectConstructions(sourceFile, sourceFile, "Relative TypeScript Worker construction is only supported directly inside an inline useEffect() callback, not imported client helpers")
6222
3736
  if (containsJsx(sourceFile)) throw new Error(`${relative(root, file)} Imported client helpers must not contain JSX`)
6223
3737
  rejectUnsupportedClientImports(sourceFile, file)
6224
3738
  modules.add(file)
@@ -6545,274 +4059,6 @@ function relativeModulePath(from, to) {
6545
4059
  return path.startsWith(".") ? path : `./${path}`
6546
4060
  }
6547
4061
 
6548
- function printNativeHandler({ exportName, expression, captures, setters, reducers = new Map(), snapshotNested, liveStates = new Set() }) {
6549
- const factory = ts.factory
6550
- const stateNames = new Set(setters.values())
6551
- const snapshotNames = snapshotNested ? nestedStateNames(expression, setters, liveStates) : new Set()
6552
- const snapshots = new Map([...snapshotNames].map(name => [name, factory.createUniqueName("__kEffectState")]))
6553
- const captureSnapshotNames = snapshotNested ? nestedCaptureNames(expression, captures) : new Set()
6554
- const captureSnapshots = new Map([...captureSnapshotNames].map(name => [name, factory.createUniqueName("__kEffectCapture")]))
6555
- const transformer = context => root => {
6556
- const visitor = node => {
6557
- if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && reducers.has(node.expression.text) && !isShadowedIdentifier(node.expression, expression)) {
6558
- const reducer = reducers.get(node.expression.text)
6559
- if (reducer.contextAction) {
6560
- const action = synthesizeTree(cloneAst(reducer.contextAction, factory, context))
6561
- const call = factory.createCallExpression(action, undefined, node.arguments)
6562
- ts.setParentRecursive(call, false)
6563
- return ts.visitNode(call, visitor)
6564
- }
6565
- if (reducer.store) return zustandActionDispatch(factory, reducer, node.arguments.map(argument => ts.visitNode(argument, visitor)))
6566
- if (node.arguments.length !== 1) throw sourceNodeError(node, expression.getSourceFile(), "Reducer dispatches require exactly one action")
6567
- return reducerDispatch(factory, reducer, ts.visitNode(node.arguments[0], visitor))
6568
- }
6569
- if (ts.isShorthandPropertyAssignment(node) && reducers.has(node.name.text) && !isShadowedIdentifier(node.name, expression)) {
6570
- if (reducers.get(node.name.text).contextAction) throw sourceNodeError(node, expression.getSourceFile(), "Context actions must be called directly inside an event handler")
6571
- if (reducers.get(node.name.text).store) throw sourceNodeError(node, expression.getSourceFile(), "Zustand actions must be called directly inside an event handler")
6572
- return factory.createPropertyAssignment(node.name, reducerReference(factory, reducers.get(node.name.text)))
6573
- }
6574
- if (ts.isIdentifier(node) && reducers.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, expression)) {
6575
- if (reducers.get(node.text).contextAction) throw sourceNodeError(node, expression.getSourceFile(), "Context actions must be called directly inside an event handler")
6576
- if (reducers.get(node.text).store) throw sourceNodeError(node, expression.getSourceFile(), "Zustand actions must be called directly inside an event handler")
6577
- return reducerReference(factory, reducers.get(node.text))
6578
- }
6579
- if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && setters.has(node.expression.text) && !isShadowedIdentifier(node.expression, expression)) {
6580
- return factory.createCallExpression(
6581
- factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "set"),
6582
- undefined,
6583
- [factory.createStringLiteral(setters.get(node.expression.text)), ...node.arguments.map(argument => ts.visitNode(argument, visitor))]
6584
- )
6585
- }
6586
- if (ts.isShorthandPropertyAssignment(node) && setters.has(node.name.text) && !isShadowedIdentifier(node.name, expression)) {
6587
- return factory.createPropertyAssignment(node.name, setterReference(factory, setters.get(node.name.text)))
6588
- }
6589
- if (ts.isIdentifier(node) && setters.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, expression)) {
6590
- return setterReference(factory, setters.get(node.text))
6591
- }
6592
- if (ts.isShorthandPropertyAssignment(node) && stateNames.has(node.name.text) && !isShadowedIdentifier(node.name, expression)) {
6593
- if (snapshots.has(node.name.text) && insideNestedFunction(node, expression)) return factory.createPropertyAssignment(node.name, snapshots.get(node.name.text))
6594
- return factory.createPropertyAssignment(node.name, factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "get"), undefined, [factory.createStringLiteral(node.name.text)]))
6595
- }
6596
- if (ts.isIdentifier(node) && stateNames.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, expression)) {
6597
- if (snapshots.has(node.text) && insideNestedFunction(node, expression)) return snapshots.get(node.text)
6598
- return factory.createCallExpression(
6599
- factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "get"),
6600
- undefined,
6601
- [factory.createStringLiteral(node.text)]
6602
- )
6603
- }
6604
- if (ts.isShorthandPropertyAssignment(node) && captures.has(node.name.text) && !isShadowedIdentifier(node.name, expression)) {
6605
- if (captureSnapshots.has(node.name.text) && insideNestedFunction(node, expression)) return factory.createPropertyAssignment(node.name, captureSnapshots.get(node.name.text))
6606
- return factory.createPropertyAssignment(node.name, scopeRead(factory, node.name.text))
6607
- }
6608
- if (ts.isIdentifier(node) && captures.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, expression)) {
6609
- if (captureSnapshots.has(node.text) && insideNestedFunction(node, expression)) return captureSnapshots.get(node.text)
6610
- return scopeRead(factory, node.text)
6611
- }
6612
- return ts.visitEachChild(node, visitor, context)
6613
- }
6614
- return ts.visitNode(root, visitor)
6615
- }
6616
- const transformed = ts.transform(expression.body, [transformer])
6617
- try {
6618
- let body = ts.isBlock(expression.body)
6619
- ? transformed.transformed[0]
6620
- : factory.createBlock([factory.createReturnStatement(transformed.transformed[0])], true)
6621
- const snapshotDeclarations = [
6622
- ...[...snapshots].map(([name, identifier]) => factory.createVariableDeclaration(identifier, undefined, undefined, factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "get"), undefined, [factory.createStringLiteral(name)]))),
6623
- ...[...captureSnapshots].map(([name, identifier]) => factory.createVariableDeclaration(identifier, undefined, undefined, factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "scope"), undefined, [factory.createStringLiteral(name)])))
6624
- ]
6625
- if (snapshotDeclarations.length) body = factory.updateBlock(body, [
6626
- factory.createVariableStatement(undefined, factory.createVariableDeclarationList(snapshotDeclarations, ts.NodeFlags.Const)),
6627
- ...body.statements
6628
- ])
6629
- const modifiers = [factory.createModifier(ts.SyntaxKind.ExportKeyword)]
6630
- if (expression.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) modifiers.push(factory.createModifier(ts.SyntaxKind.AsyncKeyword))
6631
- const declaration = factory.createFunctionDeclaration(
6632
- modifiers,
6633
- expression.asteriskToken,
6634
- exportName,
6635
- undefined,
6636
- [factory.createParameterDeclaration(undefined, undefined, "__k"), ...expression.parameters],
6637
- undefined,
6638
- body
6639
- )
6640
- return ts.createPrinter().printNode(ts.EmitHint.Unspecified, declaration, expression.getSourceFile())
6641
- } finally {
6642
- transformed.dispose()
6643
- }
6644
- }
6645
-
6646
- function nestedCaptureNames(expression, captures) {
6647
- const names = new Set()
6648
- const visit = node => {
6649
- if (ts.isIdentifier(node) && captures.has(node.text) && isReferenceIdentifier(node) && insideNestedFunction(node, expression) && !isShadowedIdentifier(node, expression)) names.add(node.text)
6650
- ts.forEachChild(node, visit)
6651
- }
6652
- visit(expression.body)
6653
- return names
6654
- }
6655
-
6656
- function nestedStateNames(expression, setters, liveStates = new Set()) {
6657
- const states = new Set(setters.values())
6658
- const names = new Set()
6659
- const visit = node => {
6660
- if (ts.isIdentifier(node) && states.has(node.text) && !liveStates.has(node.text) && isReferenceIdentifier(node) && insideNestedFunction(node, expression) && !isShadowedIdentifier(node, expression)) names.add(node.text)
6661
- ts.forEachChild(node, visit)
6662
- }
6663
- visit(expression.body)
6664
- return names
6665
- }
6666
-
6667
- function insideNestedFunction(node, root) {
6668
- for (let current = node.parent; current && current !== root; current = current.parent) {
6669
- if (isFunctionLike(current)) return true
6670
- }
6671
- return false
6672
- }
6673
-
6674
- function setterReference(factory, stateName) {
6675
- return factory.createArrowFunction(
6676
- undefined,
6677
- undefined,
6678
- [factory.createParameterDeclaration(undefined, undefined, "value")],
6679
- undefined,
6680
- factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
6681
- factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "set"), undefined, [factory.createStringLiteral(stateName), factory.createIdentifier("value")])
6682
- )
6683
- }
6684
-
6685
- function reducerReference(factory, reducer) {
6686
- const action = factory.createUniqueName("__kAction")
6687
- return factory.createArrowFunction(undefined, undefined, [factory.createParameterDeclaration(undefined, undefined, action)], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), reducerDispatch(factory, reducer, action))
6688
- }
6689
-
6690
- function reducerDispatch(factory, reducer, action) {
6691
- if (reducer.store) return zustandActionDispatch(factory, reducer, [action])
6692
- const previous = factory.createUniqueName("__kPrevious")
6693
- 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]))
6694
- return factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "set"), undefined, [factory.createStringLiteral(reducer.state), update])
6695
- }
6696
-
6697
- function zustandActionDispatch(factory, reducer, args) {
6698
- const previous = factory.createUniqueName("__kPrevious")
6699
- const current = factory.createUniqueName("__kStore")
6700
- const updateValue = factory.createUniqueName("__kUpdate")
6701
- const partial = factory.createUniqueName("__kPartial")
6702
- const action = factory.createUniqueName("__kAction")
6703
- const set = factory.createIdentifier(reducer.store.setName)
6704
- const merge = factory.createExpressionStatement(factory.createBinaryExpression(current, factory.createToken(ts.SyntaxKind.EqualsToken), factory.createObjectLiteralExpression([
6705
- factory.createSpreadAssignment(current),
6706
- factory.createSpreadAssignment(partial)
6707
- ])))
6708
- const setBody = factory.createBlock([
6709
- factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(partial, undefined, undefined, factory.createConditionalExpression(
6710
- factory.createBinaryExpression(factory.createTypeOfExpression(updateValue), factory.createToken(ts.SyntaxKind.EqualsEqualsEqualsToken), factory.createStringLiteral("function")),
6711
- undefined,
6712
- factory.createCallExpression(updateValue, undefined, [current]),
6713
- undefined,
6714
- updateValue
6715
- ))], ts.NodeFlags.Const)),
6716
- merge
6717
- ], true)
6718
- const body = factory.createBlock([
6719
- factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(current, undefined, undefined, factory.createObjectLiteralExpression([factory.createPropertyAssignment(reducer.store.field, previous)]))], ts.NodeFlags.Let)),
6720
- 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)),
6721
- factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(action, undefined, undefined, synthesizeTree(reducer.store.actions.get(reducer.action)))], ts.NodeFlags.Const)),
6722
- factory.createExpressionStatement(factory.createCallExpression(action, undefined, args)),
6723
- factory.createReturnStatement(factory.createPropertyAccessExpression(current, reducer.store.field))
6724
- ], true)
6725
- const update = factory.createArrowFunction(undefined, undefined, [factory.createParameterDeclaration(undefined, undefined, previous)], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), body)
6726
- return factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "set"), undefined, [factory.createStringLiteral(reducer.state), update])
6727
- }
6728
-
6729
- function printReactiveBinding({ exportName, expression, captures, states }) {
6730
- const factory = ts.factory
6731
- const transformer = context => root => {
6732
- const visitor = node => {
6733
- if (ts.isShorthandPropertyAssignment(node) && states.has(node.name.text)) {
6734
- return factory.createPropertyAssignment(
6735
- node.name,
6736
- factory.createCallExpression(
6737
- factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "get"),
6738
- undefined,
6739
- [factory.createStringLiteral(node.name.text)]
6740
- )
6741
- )
6742
- }
6743
- if (ts.isIdentifier(node) && states.has(node.text) && isReferenceIdentifier(node) && !isShadowedByParameter(node, expression)) {
6744
- return factory.createCallExpression(
6745
- factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "get"),
6746
- undefined,
6747
- [factory.createStringLiteral(node.text)]
6748
- )
6749
- }
6750
- if (ts.isShorthandPropertyAssignment(node) && captures.has(node.name.text)) {
6751
- return factory.createPropertyAssignment(node.name, scopeRead(factory, node.name.text))
6752
- }
6753
- if (ts.isIdentifier(node) && captures.has(node.text) && isReferenceIdentifier(node)) {
6754
- return scopeRead(factory, node.text)
6755
- }
6756
- return ts.visitEachChild(node, visitor, context)
6757
- }
6758
- return ts.visitNode(root, visitor)
6759
- }
6760
- const transformed = ts.transform(expression, [transformer])
6761
- try {
6762
- const declaration = factory.createFunctionDeclaration(
6763
- [factory.createModifier(ts.SyntaxKind.ExportKeyword)],
6764
- undefined,
6765
- exportName,
6766
- undefined,
6767
- [factory.createParameterDeclaration(undefined, undefined, "__k")],
6768
- undefined,
6769
- factory.createBlock([factory.createReturnStatement(transformed.transformed[0])], true)
6770
- )
6771
- return ts.createPrinter().printNode(ts.EmitHint.Unspecified, declaration, expression.getSourceFile())
6772
- } finally {
6773
- transformed.dispose()
6774
- }
6775
- }
6776
-
6777
- function printListExpression({ exportName, expression, item, index, states = new Set() }) {
6778
- const factory = ts.factory
6779
- const transformer = context => root => {
6780
- const visitor = node => {
6781
- if (ts.isShorthandPropertyAssignment(node) && states.has(node.name.text)) {
6782
- return factory.createPropertyAssignment(node.name, factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "get"), undefined, [factory.createStringLiteral(node.name.text)]))
6783
- }
6784
- if (ts.isIdentifier(node) && states.has(node.text) && isReferenceIdentifier(node) && !isShadowedByParameter(node, expression)) {
6785
- return factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "get"), undefined, [factory.createStringLiteral(node.text)])
6786
- }
6787
- return ts.visitEachChild(node, visitor, context)
6788
- }
6789
- return ts.visitNode(root, visitor)
6790
- }
6791
- const transformed = ts.transform(expression, [transformer])
6792
- const declaration = ts.factory.createFunctionDeclaration(
6793
- [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)],
6794
- undefined,
6795
- exportName,
6796
- undefined,
6797
- [ts.factory.createParameterDeclaration(undefined, undefined, item), ts.factory.createParameterDeclaration(undefined, undefined, index ?? "__kIndex"), ts.factory.createParameterDeclaration(undefined, undefined, "__k")],
6798
- undefined,
6799
- ts.factory.createBlock([ts.factory.createReturnStatement(transformed.transformed[0])], true)
6800
- )
6801
- try {
6802
- return ts.createPrinter().printNode(ts.EmitHint.Unspecified, declaration, expression.getSourceFile())
6803
- } finally {
6804
- transformed.dispose()
6805
- }
6806
- }
6807
-
6808
- function scopeRead(factory, name) {
6809
- return factory.createCallExpression(
6810
- factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "scope"),
6811
- undefined,
6812
- [factory.createStringLiteral(name)]
6813
- )
6814
- }
6815
-
6816
4062
  function compileEventCommand(expression, setters, factory) {
6817
4063
  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)) {
6818
4064
  return command(factory, "log", expression.arguments[1], factory.createStringLiteral(expression.arguments[0].text))
@@ -7000,6 +4246,25 @@ function withBase(base, path) {
7000
4246
  return base ? `${base}${path}` : path
7001
4247
  }
7002
4248
 
4249
+ const workerCompiler = createWorkerCompiler({
4250
+ root,
4251
+ sourceDirectory,
4252
+ outputDirectory,
4253
+ assetPath,
4254
+ parseSourceFile,
4255
+ resolveSourceImport,
4256
+ runtimeModuleReference
4257
+ })
4258
+
4259
+ const printEffectEntry = createEffectCodegen({ assetPath, inlineJson, relativeModulePath })
4260
+ const printHandlerModule = createHandlerCodegen({
4261
+ cloneAst,
4262
+ synthesizeTree,
4263
+ resolveClientImport: (entry, handlerPath) => entry.package ? entry.target : relativeModulePath(handlerPath, clientModulePath(entry.target))
4264
+ })
4265
+ const { normalizeReactMigrationSyntax, validateUseIdSyntax } = createReactMigrationPass({ cloneAst, isArrayFromCall, jsxTagName, renderedCollectionSource })
4266
+ const normalizeReactRouterSyntax = createRouterPass({ withBase })
4267
+
7003
4268
  async function staticPathEntries(module, file) {
7004
4269
  if (typeof module.getStaticPaths !== "function") return [{ params: {}, props: {} }]
7005
4270
  const entries = await module.getStaticPaths()
@@ -7084,13 +4349,3 @@ async function exists(path) {
7084
4349
  return false
7085
4350
  }
7086
4351
  }
7087
-
7088
- function contentType(file) {
7089
- return {
7090
- ".html": "text/html; charset=utf-8",
7091
- ".css": "text/css; charset=utf-8",
7092
- ".js": "text/javascript; charset=utf-8",
7093
- ".json": "application/json; charset=utf-8",
7094
- ".svg": "image/svg+xml"
7095
- }[extname(file)] ?? "application/octet-stream"
7096
- }