Created
March 13, 2025 00:40
-
-
Save morrelinko/285301ab416eba48aab3c3218e96b5f5 to your computer and use it in GitHub Desktop.
[Playground] NodeJS features
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import vm from 'node:vm' | |
import fse from 'fs-extra' | |
import { transform } from 'esbuild' | |
import { resolve, dirname, isAbsolute } from 'pathe' | |
interface TryImportOptions { | |
rootDir?: string | |
loader?: 'ts' | 'js' | |
define?: { [key: string]: string } | |
alias?: Record<string, string> | |
} | |
export async function tryImport<TModule> ( | |
file: string, | |
options: TryImportOptions = {} | |
): Promise<[TModule | null, Error | null]> { | |
let res: TModule | null = null | |
let err: Error | null = null | |
const originalListeners = process.listeners('warning') | |
process.removeAllListeners('warning') | |
try { | |
// TODO: remove this. NodeJS should be able to resolve ts files | |
const { code } = await transform(await fse.readFile(file), { | |
loader: 'ts', | |
platform: 'node' | |
}) | |
const module = new vm.SourceTextModule(code, {}) | |
const imports = new Map() | |
await module.link(async function (specifier, referencingModule) { | |
if (imports.has(specifier)) { | |
return imports.get(specifier) | |
} | |
const baseDir = dirname(file) | |
const resolvedSpecifier = resolvePath(specifier, baseDir, options) | |
const mod = !isAbsolute(resolvedSpecifier) | |
? await import(resolvedSpecifier) | |
: (async () => { | |
const [m, e] = await tryImport(resolvedSpecifier, options) | |
if (e) return null | |
console.log({ m }) | |
return m | |
})() | |
const exportNames = Object.keys(mod) | |
const imported = new vm.SyntheticModule( | |
exportNames, | |
() => { | |
exportNames.forEach((key) => imported.setExport(key, mod[key])) | |
}, | |
{ | |
identifier: resolvedSpecifier, | |
context: referencingModule.context | |
} | |
) | |
imports.set(specifier, imported) | |
return imported | |
}) | |
await module.evaluate() | |
res = module.namespace as TModule | |
} catch (e) { | |
if (e instanceof Error) err = e | |
} finally { | |
process.removeAllListeners('warning') | |
for (const listener of originalListeners) { | |
process.on('warning', listener) | |
} | |
} | |
return [res, err] | |
} | |
function resolvePath ( | |
specifier: string, | |
baseDir: string, | |
options: TryImportOptions = {} | |
): string { | |
// Handle aliases | |
if (options.alias && Object.keys(options.alias).length > 0) { | |
const matchingAliasKey = Object.keys(options.alias) | |
.filter((k) => { | |
return ( | |
specifier.startsWith(k) && | |
(specifier.length === k.length || | |
specifier[k.length] === '/' || | |
specifier[k.length] === '.') | |
) | |
}) | |
.sort((a, b) => b.length - a.length)?.[0] | |
if (matchingAliasKey) { | |
return normalizedResolvedPath( | |
specifier.replace(matchingAliasKey, options.alias[matchingAliasKey]) | |
) | |
} | |
} | |
// Resolve relative paths | |
if (specifier.startsWith('./') || specifier.startsWith('../')) { | |
return normalizedResolvedPath(resolve(baseDir, specifier)) | |
} | |
// Absolute paths or node_modules | |
return specifier | |
} | |
function normalizedResolvedPath (filePath: string): string { | |
return !filePath.endsWith('ts') ? `${filePath}.ts` : filePath | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
DO NOT USE THIS IN PRODUCTION -> Use Jiti