Files
Termix/src/backend/plugins/host-modules.ts
T

66 lines
2.1 KiB
TypeScript

import module from "node:module";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { getPluginsDir } from "./paths.js";
/**
* A plugin build leaves host packages (drizzle-orm, express, ssh2, ...) for
* core to supply, and Node finds them by walking up from the importing file.
* That works for bundled plugins inside the app, but a .tmxplug unpacked into
* a data dir outside it (the desktop app keeps it in AppData) finds nothing.
* Those imports fall back to core's own copy, the one bundled plugins get.
*/
let installed = false;
export function isBareSpecifier(specifier: string): boolean {
if (specifier.startsWith(".") || specifier.startsWith("/")) return false;
if (specifier.startsWith("#")) return false;
// file:, node:, data:, and a Windows drive letter
return !/^[a-zA-Z][a-zA-Z\d+.-]*:/.test(specifier);
}
/** Whether an import from parentURL may fall back to core's packages. */
export function isUserPluginFile(
parentURL: string | undefined,
pluginsDir: string,
): boolean {
if (!parentURL?.startsWith("file:")) return false;
let file: string;
try {
file = fileURLToPath(new URL(parentURL));
} catch {
return false;
}
const relative = path.relative(path.resolve(pluginsDir), file);
return (
relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative)
);
}
export function installHostModuleFallback(): void {
if (installed) return;
installed = true;
// Node 22.15+. Older runtimes keep plain resolution.
if (typeof module.registerHooks !== "function") return;
module.registerHooks({
resolve(specifier, context, nextResolve) {
try {
return nextResolve(specifier, context);
} catch (error) {
if (
(error as { code?: string })?.code !== "ERR_MODULE_NOT_FOUND" ||
!isBareSpecifier(specifier) ||
!isUserPluginFile(context.parentURL, getPluginsDir())
) {
throw error;
}
return nextResolve(specifier, {
...context,
parentURL: import.meta.url,
});
}
},
});
}