127 lines
3.7 KiB
TypeScript
127 lines
3.7 KiB
TypeScript
/**
|
|
* Node.js module shim for TypeScript code that runs in both browser and Node.js
|
|
*
|
|
* This utility provides conditional imports for Node.js-only modules, allowing
|
|
* TypeScript files to be bundled for the browser (via Hugo/esbuild) while still
|
|
* working in Node.js environments.
|
|
*
|
|
* @module utils/node-shim
|
|
*/
|
|
|
|
/**
|
|
* Detect if running in Node.js vs browser environment
|
|
*/
|
|
export const isNode =
|
|
typeof process !== 'undefined' &&
|
|
process.versions != null &&
|
|
process.versions.node != null;
|
|
|
|
/**
|
|
* Node.js module references (lazily loaded in Node.js environment)
|
|
*/
|
|
export interface NodeModules {
|
|
fileURLToPath: (url: string) => string;
|
|
dirname: (path: string) => string;
|
|
join: (...paths: string[]) => string;
|
|
readFileSync: (path: string, encoding: BufferEncoding) => string;
|
|
existsSync: (path: string) => boolean;
|
|
yaml: { load: (content: string) => unknown };
|
|
}
|
|
|
|
let nodeModulesCache: NodeModules | undefined;
|
|
|
|
/**
|
|
* Lazy load Node.js modules (only when running in Node.js)
|
|
*
|
|
* This function dynamically imports Node.js built-in modules (`url`, `path`, `fs`)
|
|
* and third-party modules (`js-yaml`) only when called in a Node.js environment.
|
|
* In browser environments, this returns undefined and the imports are tree-shaken out.
|
|
*
|
|
* @returns Promise resolving to NodeModules or undefined
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* import { loadNodeModules, isNode } from './utils/node-shim.js';
|
|
*
|
|
* async function readConfig() {
|
|
* if (!isNode) return null;
|
|
*
|
|
* const nodeModules = await loadNodeModules();
|
|
* if (!nodeModules) return null;
|
|
*
|
|
* const configPath = nodeModules.join(__dirname, 'config.yml');
|
|
* if (nodeModules.existsSync(configPath)) {
|
|
* const content = nodeModules.readFileSync(configPath, 'utf8');
|
|
* return nodeModules.yaml.load(content);
|
|
* }
|
|
* }
|
|
* ```
|
|
*/
|
|
export async function loadNodeModules(): Promise<NodeModules | undefined> {
|
|
// Early return for browser - this branch will be eliminated by tree-shaking
|
|
if (!isNode) {
|
|
return undefined;
|
|
}
|
|
|
|
// Return cached modules if already loaded
|
|
if (nodeModulesCache) {
|
|
return nodeModulesCache;
|
|
}
|
|
|
|
// This code path is never reached in browser builds due to isNode check above
|
|
// The dynamic imports will be tree-shaken out by esbuild
|
|
try {
|
|
// Use Function constructor to hide imports from static analysis
|
|
// This prevents esbuild from trying to resolve them during browser builds
|
|
const loadModule = new Function('moduleName', 'return import(moduleName)');
|
|
|
|
const [urlModule, pathModule, fsModule, yamlModule] = await Promise.all([
|
|
loadModule('url'),
|
|
loadModule('path'),
|
|
loadModule('fs'),
|
|
loadModule('js-yaml'),
|
|
]);
|
|
|
|
nodeModulesCache = {
|
|
fileURLToPath: urlModule.fileURLToPath,
|
|
dirname: pathModule.dirname,
|
|
join: pathModule.join,
|
|
readFileSync: fsModule.readFileSync,
|
|
existsSync: fsModule.existsSync,
|
|
yaml: yamlModule.default as { load: (content: string) => unknown },
|
|
};
|
|
|
|
return nodeModulesCache;
|
|
} catch (err) {
|
|
if (err instanceof Error) {
|
|
console.warn('Failed to load Node.js modules:', err.message);
|
|
}
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the directory path of the current module (Node.js only)
|
|
*
|
|
* @param importMetaUrl - import.meta.url from the calling module
|
|
* @returns Directory path or undefined if not in Node.js
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* import { getModuleDir } from './utils/node-shim.js';
|
|
*
|
|
* const moduleDir = await getModuleDir(import.meta.url);
|
|
* ```
|
|
*/
|
|
export async function getModuleDir(
|
|
importMetaUrl: string
|
|
): Promise<string | undefined> {
|
|
const nodeModules = await loadNodeModules();
|
|
if (!nodeModules) {
|
|
return undefined;
|
|
}
|
|
|
|
const filename = nodeModules.fileURLToPath(importMetaUrl);
|
|
return nodeModules.dirname(filename);
|
|
}
|