Skip to content

Instantly share code, notes, and snippets.

@morrelinko
Created March 13, 2025 00:40
Show Gist options
  • Save morrelinko/285301ab416eba48aab3c3218e96b5f5 to your computer and use it in GitHub Desktop.
Save morrelinko/285301ab416eba48aab3c3218e96b5f5 to your computer and use it in GitHub Desktop.
[Playground] NodeJS features
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
}
@morrelinko
Copy link
Author

DO NOT USE THIS IN PRODUCTION -> Use Jiti

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment