@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.
- package/README.md +34 -6
- package/RELEASES.md +62 -0
- package/framework/README.md +22 -2
- package/framework/build.mjs +123 -2868
- package/framework/compiler/animation-frame-pass.mjs +103 -0
- package/framework/compiler/ast-helpers.mjs +181 -0
- package/framework/compiler/browser-signal-passes.mjs +182 -0
- package/framework/compiler/custom-hook-timer-pass.mjs +126 -0
- package/framework/compiler/effect-codegen.mjs +884 -0
- package/framework/compiler/handler-codegen.mjs +296 -0
- package/framework/compiler/normalization-pipeline.mjs +9 -0
- package/framework/compiler/react-migration-pass.mjs +338 -0
- package/framework/compiler/render-control-pass.mjs +96 -0
- package/framework/compiler/router-pass.mjs +245 -0
- package/framework/compiler/worker-compiler.mjs +163 -0
- package/framework/dev-server.mjs +244 -0
- package/package.json +1 -1
package/framework/build.mjs
CHANGED
|
@@ -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,
|
|
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 {
|
|
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
|
|
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
|
|
1694
|
-
return
|
|
684
|
+
function inlineJson(value) {
|
|
685
|
+
return JSON.stringify(value).replaceAll("<", "\\u003c").replaceAll("\u2028", "\\u2028").replaceAll("\u2029", "\\u2029")
|
|
1695
686
|
}
|
|
1696
687
|
|
|
1697
|
-
function
|
|
1698
|
-
|
|
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("&", "&").replaceAll("<", "<").replaceAll(">", ">")
|
|
1702
690
|
}
|
|
1703
691
|
|
|
1704
|
-
|
|
1705
|
-
|
|
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("&", "&").replaceAll("<", "<").replaceAll(">", ">")
|
|
1767
|
-
}
|
|
1768
|
-
|
|
1769
|
-
function escapeAttribute(value) {
|
|
1770
|
-
return escapeHtml(value).replaceAll('"', """).replaceAll("'", "'")
|
|
692
|
+
function escapeAttribute(value) {
|
|
693
|
+
return escapeHtml(value).replaceAll('"', """).replaceAll("'", "'")
|
|
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 =
|
|
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
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
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
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
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
|
|
3069
|
-
const importedTimerStates = new Map()
|
|
1006
|
+
const importedSourceCache = new Map()
|
|
3070
1007
|
const importedSource = target => {
|
|
3071
|
-
let
|
|
3072
|
-
if (!
|
|
3073
|
-
|
|
3074
|
-
|
|
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
|
|
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 =>
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
6100
|
-
const
|
|
6101
|
-
const
|
|
6102
|
-
const
|
|
6103
|
-
|
|
6104
|
-
|
|
6105
|
-
|
|
6106
|
-
|
|
6107
|
-
|
|
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 (
|
|
6119
|
-
if (ts.
|
|
6120
|
-
|
|
6121
|
-
|
|
6122
|
-
|
|
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
|
-
|
|
6131
|
-
|
|
6132
|
-
|
|
6133
|
-
|
|
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
|
-
|
|
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
|
-
}
|