@travetto/registry
Version:
Patterns and utilities for handling registration of metadata and functionality for run-time use
88 lines (75 loc) • 2.81 kB
text/typescript
import { Module } from 'node:module';
import { path } from '@travetto/manifest';
import { Runtime, RuntimeIndex } from '@travetto/runtime';
import { RetargettingProxy } from '../proxy';
declare module 'module' {
// eslint-disable-next-line @typescript-eslint/naming-convention
function _resolveFilename(req: string, parent: typeof Module): string;
// eslint-disable-next-line @typescript-eslint/naming-convention
function _load(req: string, parent: typeof Module): unknown;
}
type ModuleLoader = typeof Module['_load'];
type ModuleProxy = <T>(file: string, mod?: T) => (T | undefined);
const moduleLoad: ModuleLoader = Module._load.bind(Module);
/**
* Dynamic commonjs module loader. Hooks into module loading
*/
export class DynamicCommonjsLoader {
/**
* Build a module loader
*/
static buildModuleLoader(proxyModuleLoad?: ModuleProxy): ModuleLoader {
return (request: string, parent: typeof Module): unknown => {
let mod: unknown;
try {
mod = moduleLoad.apply(null, [request, parent]);
} catch (err: unknown) {
const name = Module._resolveFilename!(request, parent);
if (err instanceof Error && Runtime.dynamic && !name.startsWith('test/')) {
const errMsg = err.message;
console.debug(`Unable to load ${name}: stubbing out with error proxy.`, errMsg);
const e = (): never => { throw new Error(errMsg); };
mod = new Proxy({}, { getOwnPropertyDescriptor: e, get: e, has: e });
} else {
throw err;
}
}
const file = Module._resolveFilename!(request, parent);
const src = RuntimeIndex.getEntry(file)?.sourceFile;
// Only proxy workspace modules
if (src && RuntimeIndex.getModuleFromSource(src)?.workspace) {
return proxyModuleLoad ? proxyModuleLoad(file, mod) : mod;
} else {
return mod;
}
};
}
#loader: ModuleLoader;
#modules = new Map<string, RetargettingProxy<unknown>>();
#proxyModule<T>(file: string, mod?: T): T | undefined {
if (!this.#modules.has(file)) {
this.#modules.set(file, new RetargettingProxy<T>(mod!));
} else {
this.#modules.get(file)!.setTarget(mod);
}
return this.#modules.get(file)!.get<T>();
}
async init(): Promise<void> {
this.#loader = DynamicCommonjsLoader.buildModuleLoader((file, mod) => this.#proxyModule(file, mod));
}
async unload(file: string): Promise<void> {
const native = path.toNative(file);
if (native in require.cache) {
delete require.cache[native]; // Remove require cached element
}
this.#proxyModule(file, null);
}
async load(file: string): Promise<void> {
try {
Module._load = this.#loader;
require(file);
} finally {
Module._load = moduleLoad;
}
}
}