2026-04-10 15:52:59 +09:00
|
|
|
import { createRequire } from "node:module"
|
2026-05-30 13:44:54 +09:00
|
|
|
import { fileURLToPath } from "node:url"
|
2026-05-30 16:04:59 +09:00
|
|
|
import { defaultGetCallerStack, isModuleEvaluationStack, resolveCallerUrlFromStack } from "./module-mock-stack"
|
2026-05-30 16:04:45 +09:00
|
|
|
import { createRestoreExports } from "./module-mock-restore-exports"
|
2026-04-10 15:52:59 +09:00
|
|
|
|
|
|
|
|
type MockModuleFactory = () => Record<string, unknown>
|
|
|
|
|
|
|
|
|
|
type MockApi = {
|
|
|
|
|
module: (specifier: string, factory: MockModuleFactory) => unknown
|
|
|
|
|
restore: () => unknown
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type ModuleLoadResult =
|
|
|
|
|
| { ok: true; value: unknown }
|
|
|
|
|
| { ok: false; error: Error }
|
|
|
|
|
|
|
|
|
|
type ModuleSnapshot = {
|
2026-05-30 13:44:54 +09:00
|
|
|
restoreSpecifiers: Set<string>
|
|
|
|
|
restoreFactory: MockModuleFactory
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type PersistentModuleSnapshot = {
|
2026-05-30 15:34:11 +09:00
|
|
|
originalSpecifier: string
|
|
|
|
|
reappliedDuringActiveRestore: boolean
|
2026-05-30 13:44:54 +09:00
|
|
|
restoreSpecifiers: Set<string>
|
2026-04-10 15:52:59 +09:00
|
|
|
restoreFactory: MockModuleFactory
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type ModuleMockLifecycleOptions = {
|
2026-05-30 13:44:54 +09:00
|
|
|
getCallerStack?: () => string
|
2026-04-10 15:52:59 +09:00
|
|
|
getCallerUrl?: () => string
|
2026-05-30 13:44:54 +09:00
|
|
|
trackOnlyDuringActiveTest?: boolean
|
2026-05-30 15:34:11 +09:00
|
|
|
isPersistentModuleMockOwner?: (callerUrl: string) => boolean
|
2026-04-10 15:52:59 +09:00
|
|
|
resolveSpecifier?: (specifier: string, callerUrl: string) => string
|
|
|
|
|
loadOriginalModule?: (specifier: string, callerUrl: string) => ModuleLoadResult
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-30 13:44:54 +09:00
|
|
|
let originalLoadNonce = 0
|
2026-04-10 15:52:59 +09:00
|
|
|
|
2026-05-30 13:44:54 +09:00
|
|
|
function toError(error: unknown): Error {
|
|
|
|
|
return error instanceof Error ? error : new Error(String(error))
|
2026-04-10 15:52:59 +09:00
|
|
|
}
|
|
|
|
|
|
2026-05-30 13:44:54 +09:00
|
|
|
function resolveWithBun(specifier: string, callerUrl: string): string {
|
|
|
|
|
const callerDirectory = fileURLToPath(new URL(".", callerUrl))
|
|
|
|
|
return Bun.resolveSync(specifier, callerDirectory)
|
2026-04-10 15:52:59 +09:00
|
|
|
}
|
|
|
|
|
|
2026-05-30 13:44:54 +09:00
|
|
|
function isSchemeSpecifier(specifier: string): boolean {
|
|
|
|
|
return /^[a-zA-Z][a-zA-Z\d+.-]*:/.test(specifier)
|
|
|
|
|
}
|
2026-04-10 15:52:59 +09:00
|
|
|
|
2026-05-30 13:44:54 +09:00
|
|
|
function defaultResolveSpecifier(specifier: string, callerUrl: string): string {
|
|
|
|
|
try {
|
|
|
|
|
return resolveWithBun(specifier, callerUrl)
|
|
|
|
|
} catch {
|
|
|
|
|
return specifier
|
2026-04-10 15:52:59 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-30 13:44:54 +09:00
|
|
|
function createOriginalLoadSpecifier(specifier: string, callerUrl: string): string {
|
2026-04-10 15:52:59 +09:00
|
|
|
try {
|
2026-05-30 13:44:54 +09:00
|
|
|
const resolved = resolveWithBun(specifier, callerUrl)
|
|
|
|
|
if (isSchemeSpecifier(resolved)) {
|
|
|
|
|
return specifier
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
originalLoadNonce += 1
|
|
|
|
|
return `${resolved}?omo_original=${originalLoadNonce}`
|
2026-04-10 15:52:59 +09:00
|
|
|
} catch {
|
|
|
|
|
return specifier
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function defaultLoadOriginalModule(specifier: string, callerUrl: string): ModuleLoadResult {
|
|
|
|
|
try {
|
|
|
|
|
const require = createRequire(callerUrl)
|
2026-05-30 13:44:54 +09:00
|
|
|
return { ok: true, value: require(createOriginalLoadSpecifier(specifier, callerUrl)) }
|
2026-04-10 15:52:59 +09:00
|
|
|
} catch (error) {
|
|
|
|
|
return { ok: false, error: toError(error) }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-30 15:34:11 +09:00
|
|
|
function defaultIsPersistentModuleMockOwner(_callerUrl: string): boolean {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 15:52:59 +09:00
|
|
|
export function installModuleMockLifecycle(
|
|
|
|
|
mockApi: MockApi,
|
|
|
|
|
options: ModuleMockLifecycleOptions = {},
|
2026-05-30 13:44:54 +09:00
|
|
|
): {
|
|
|
|
|
beginTestMockTracking: () => void
|
|
|
|
|
endTestMockTracking: () => void
|
|
|
|
|
restoreModuleMocks: () => void
|
|
|
|
|
} {
|
2026-04-10 15:52:59 +09:00
|
|
|
const snapshots = new Map<string, ModuleSnapshot>()
|
2026-05-30 15:34:11 +09:00
|
|
|
const persistentSnapshots = new Map<string, Map<string, PersistentModuleSnapshot>>()
|
2026-05-30 13:44:54 +09:00
|
|
|
let lastRestoredSnapshots: ModuleSnapshot[] = []
|
|
|
|
|
let isActiveTest = !options.trackOnlyDuringActiveTest
|
2026-05-30 15:34:11 +09:00
|
|
|
let hasStartedTest = false
|
2026-05-30 16:04:59 +09:00
|
|
|
let activeTestOwnerUrl: string | null = null
|
2026-04-10 15:52:59 +09:00
|
|
|
const delegateModule = mockApi.module.bind(mockApi)
|
|
|
|
|
const delegateRestore = mockApi.restore.bind(mockApi)
|
2026-05-30 13:44:54 +09:00
|
|
|
const getCallerStack = options.getCallerStack ?? defaultGetCallerStack
|
2026-04-10 15:52:59 +09:00
|
|
|
const resolveSpecifier = options.resolveSpecifier ?? defaultResolveSpecifier
|
|
|
|
|
const loadOriginalModule = options.loadOriginalModule ?? defaultLoadOriginalModule
|
2026-05-30 15:34:11 +09:00
|
|
|
const isPersistentModuleMockOwner = options.isPersistentModuleMockOwner ?? defaultIsPersistentModuleMockOwner
|
2026-04-10 15:52:59 +09:00
|
|
|
|
2026-05-30 13:44:54 +09:00
|
|
|
function getCallerUrl(callerStack: string): string {
|
|
|
|
|
return options.getCallerUrl?.() ?? resolveCallerUrlFromStack(callerStack)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function restoreModuleMocksForRestoreCall(): void {
|
|
|
|
|
const snapshotsToRestore = snapshots.size > 0 ? Array.from(snapshots.values()) : lastRestoredSnapshots
|
|
|
|
|
|
|
|
|
|
for (const snapshot of snapshotsToRestore) {
|
|
|
|
|
for (const restoreSpecifier of snapshot.restoreSpecifiers) {
|
|
|
|
|
delegateModule(restoreSpecifier, snapshot.restoreFactory)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (snapshots.size > 0) {
|
|
|
|
|
lastRestoredSnapshots = snapshotsToRestore
|
|
|
|
|
snapshots.clear()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-30 15:34:11 +09:00
|
|
|
function restorePersistentModuleMocksForRestoreCall(markReapplied: boolean): void {
|
|
|
|
|
for (const snapshotsByOwner of persistentSnapshots.values()) {
|
|
|
|
|
for (const snapshot of snapshotsByOwner.values()) {
|
|
|
|
|
if (markReapplied) {
|
|
|
|
|
snapshot.reappliedDuringActiveRestore = true
|
|
|
|
|
}
|
|
|
|
|
for (const restoreSpecifier of snapshot.restoreSpecifiers) {
|
|
|
|
|
delegateModule(restoreSpecifier, snapshot.restoreFactory)
|
|
|
|
|
}
|
2026-05-30 13:44:54 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-30 15:34:11 +09:00
|
|
|
function restorePersistentOriginals(snapshot: PersistentModuleSnapshot, ownerUrl: string): void {
|
|
|
|
|
const originalModule = loadOriginalModule(snapshot.originalSpecifier, ownerUrl)
|
|
|
|
|
if (!originalModule.ok) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const originalFactory = () => createRestoreExports(originalModule.value)
|
|
|
|
|
for (const restoreSpecifier of snapshot.restoreSpecifiers) {
|
|
|
|
|
delegateModule(restoreSpecifier, originalFactory)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function clearPersistentModuleMocksForOwner(
|
|
|
|
|
ownerUrl: string,
|
|
|
|
|
restoreOriginals: boolean,
|
|
|
|
|
forceRestoreOriginals = false,
|
|
|
|
|
): void {
|
|
|
|
|
let clearedOwnerSnapshot = false
|
|
|
|
|
|
|
|
|
|
for (const [resolvedSpecifier, snapshotsByOwner] of persistentSnapshots) {
|
|
|
|
|
const snapshot = snapshotsByOwner.get(ownerUrl)
|
|
|
|
|
if (snapshot) {
|
|
|
|
|
if (restoreOriginals && (forceRestoreOriginals || snapshot.reappliedDuringActiveRestore)) {
|
|
|
|
|
restorePersistentOriginals(snapshot, ownerUrl)
|
|
|
|
|
}
|
|
|
|
|
snapshotsByOwner.delete(ownerUrl)
|
|
|
|
|
clearedOwnerSnapshot = true
|
|
|
|
|
}
|
|
|
|
|
if (snapshotsByOwner.size === 0) {
|
|
|
|
|
persistentSnapshots.delete(resolvedSpecifier)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (clearedOwnerSnapshot || !restoreOriginals) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const [resolvedSpecifier, snapshotsByOwner] of persistentSnapshots) {
|
|
|
|
|
for (const [snapshotOwnerUrl, snapshot] of snapshotsByOwner) {
|
|
|
|
|
if (!snapshot.reappliedDuringActiveRestore) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
restorePersistentOriginals(snapshot, snapshotOwnerUrl)
|
|
|
|
|
snapshotsByOwner.delete(snapshotOwnerUrl)
|
|
|
|
|
}
|
|
|
|
|
if (snapshotsByOwner.size === 0) {
|
2026-05-30 13:44:54 +09:00
|
|
|
persistentSnapshots.delete(resolvedSpecifier)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 15:52:59 +09:00
|
|
|
function restoreModuleMocks(): void {
|
2026-05-30 13:44:54 +09:00
|
|
|
if (snapshots.size === 0) {
|
|
|
|
|
return
|
2026-04-10 15:52:59 +09:00
|
|
|
}
|
|
|
|
|
|
2026-05-30 13:44:54 +09:00
|
|
|
restoreModuleMocksForRestoreCall()
|
2026-04-10 15:52:59 +09:00
|
|
|
}
|
|
|
|
|
|
2026-05-30 13:44:54 +09:00
|
|
|
function beginTestMockTracking(): void {
|
2026-05-30 15:34:11 +09:00
|
|
|
hasStartedTest = true
|
2026-05-30 13:44:54 +09:00
|
|
|
isActiveTest = true
|
2026-05-30 16:04:59 +09:00
|
|
|
const callerStack = getCallerStack()
|
|
|
|
|
activeTestOwnerUrl = getCallerUrl(callerStack)
|
2026-05-30 13:44:54 +09:00
|
|
|
}
|
2026-04-10 15:52:59 +09:00
|
|
|
|
2026-05-30 13:44:54 +09:00
|
|
|
function endTestMockTracking(): void {
|
|
|
|
|
isActiveTest = !options.trackOnlyDuringActiveTest
|
2026-05-30 16:04:59 +09:00
|
|
|
activeTestOwnerUrl = null
|
2026-05-30 13:44:54 +09:00
|
|
|
}
|
2026-04-10 15:52:59 +09:00
|
|
|
|
2026-05-30 13:44:54 +09:00
|
|
|
mockApi.module = (specifier: string, factory: MockModuleFactory): unknown => {
|
|
|
|
|
lastRestoredSnapshots = []
|
|
|
|
|
const callerStack = getCallerStack()
|
|
|
|
|
const callerUrl = getCallerUrl(callerStack)
|
2026-05-30 16:04:59 +09:00
|
|
|
const isParallelFileEvaluationMock =
|
|
|
|
|
isActiveTest &&
|
|
|
|
|
options.trackOnlyDuringActiveTest === true &&
|
|
|
|
|
activeTestOwnerUrl !== null &&
|
|
|
|
|
callerUrl !== activeTestOwnerUrl &&
|
|
|
|
|
isModuleEvaluationStack(callerStack)
|
|
|
|
|
|
|
|
|
|
if ((!isActiveTest || isParallelFileEvaluationMock) && isPersistentModuleMockOwner(callerUrl)) {
|
2026-05-30 13:44:54 +09:00
|
|
|
const resolvedSpecifier = resolveSpecifier(specifier, callerUrl)
|
2026-05-30 15:34:11 +09:00
|
|
|
const snapshotsByOwner = persistentSnapshots.get(resolvedSpecifier) ?? new Map<string, PersistentModuleSnapshot>()
|
|
|
|
|
const existingSnapshot = snapshotsByOwner.get(callerUrl)
|
2026-05-30 13:44:54 +09:00
|
|
|
if (existingSnapshot) {
|
|
|
|
|
existingSnapshot.restoreSpecifiers.add(specifier)
|
|
|
|
|
existingSnapshot.restoreSpecifiers.add(resolvedSpecifier)
|
2026-05-30 15:34:11 +09:00
|
|
|
existingSnapshot.restoreFactory = factory
|
2026-05-30 13:44:54 +09:00
|
|
|
} else {
|
2026-05-30 15:34:11 +09:00
|
|
|
snapshotsByOwner.set(callerUrl, {
|
|
|
|
|
originalSpecifier: specifier,
|
|
|
|
|
reappliedDuringActiveRestore: false,
|
2026-05-30 13:44:54 +09:00
|
|
|
restoreSpecifiers: new Set([specifier, resolvedSpecifier]),
|
|
|
|
|
restoreFactory: factory,
|
2026-04-10 15:52:59 +09:00
|
|
|
})
|
|
|
|
|
}
|
2026-05-30 15:34:11 +09:00
|
|
|
persistentSnapshots.set(resolvedSpecifier, snapshotsByOwner)
|
2026-05-30 13:44:54 +09:00
|
|
|
return delegateModule(specifier, factory)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (isActiveTest) {
|
|
|
|
|
const resolvedSpecifier = resolveSpecifier(specifier, callerUrl)
|
|
|
|
|
const existingSnapshot = snapshots.get(resolvedSpecifier)
|
|
|
|
|
|
|
|
|
|
if (existingSnapshot) {
|
2026-05-30 15:34:11 +09:00
|
|
|
existingSnapshot.restoreSpecifiers.add(specifier)
|
2026-05-30 13:44:54 +09:00
|
|
|
existingSnapshot.restoreSpecifiers.add(resolvedSpecifier)
|
|
|
|
|
} else {
|
|
|
|
|
const originalModule = loadOriginalModule(specifier, callerUrl)
|
|
|
|
|
|
|
|
|
|
if (originalModule.ok) {
|
|
|
|
|
const restoreExports = createRestoreExports(originalModule.value)
|
|
|
|
|
snapshots.set(resolvedSpecifier, {
|
2026-05-30 15:34:11 +09:00
|
|
|
restoreSpecifiers: new Set([specifier, resolvedSpecifier]),
|
2026-05-30 13:44:54 +09:00
|
|
|
restoreFactory: () => restoreExports,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-10 15:52:59 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return delegateModule(specifier, factory)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mockApi.restore = (): unknown => {
|
2026-05-30 13:44:54 +09:00
|
|
|
const callerStack = getCallerStack()
|
|
|
|
|
const callerUrl = getCallerUrl(callerStack)
|
2026-05-15 16:26:57 +09:00
|
|
|
const result = delegateRestore()
|
2026-05-30 13:44:54 +09:00
|
|
|
if (!isActiveTest) {
|
2026-05-30 15:34:11 +09:00
|
|
|
restoreModuleMocksForRestoreCall()
|
2026-05-30 13:44:54 +09:00
|
|
|
snapshots.clear()
|
|
|
|
|
lastRestoredSnapshots = []
|
2026-05-30 15:34:11 +09:00
|
|
|
clearPersistentModuleMocksForOwner(callerUrl, hasStartedTest)
|
|
|
|
|
restorePersistentModuleMocksForRestoreCall(false)
|
2026-05-30 13:44:54 +09:00
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
restoreModuleMocksForRestoreCall()
|
2026-05-30 15:34:11 +09:00
|
|
|
restorePersistentModuleMocksForRestoreCall(true)
|
2026-05-15 16:26:57 +09:00
|
|
|
return result
|
2026-04-10 15:52:59 +09:00
|
|
|
}
|
|
|
|
|
|
2026-05-30 13:44:54 +09:00
|
|
|
return { beginTestMockTracking, endTestMockTracking, restoreModuleMocks }
|
2026-04-10 15:52:59 +09:00
|
|
|
}
|