UNPKG

esbuild-node-loader

Version:

Transpile TypeScript to ESM with Node.js loader

254 lines (213 loc) 5.96 kB
import { URL, fileURLToPath, pathToFileURL } from 'url' import fs from 'fs' import { dirname } from 'path' import { build, transformSync } from 'esbuild' import semver from 'semver' const isWindows = process.platform === 'win32' const httpRegex = /^https?:\/\// const extensionsRegex = /\.m?(tsx?|json)$/ async function esbuildResolve(id, dir) { let result await build({ stdin: { contents: `import ${JSON.stringify(id)}`, resolveDir: dir, }, write: false, bundle: true, treeShaking: false, ignoreAnnotations: true, platform: 'node', plugins: [{ name: 'resolve', setup({ onLoad }) { onLoad({ filter: /.*/ }, (args) => { result = args.path return { contents: '' } }) }, }], }) return result } function esbuildTransformSync(rawSource, filename, url, format) { const { code: js, warnings, map: jsSourceMap, } = transformSync(rawSource.toString(), { sourcefile: filename, sourcemap: 'both', loader: new URL(url).pathname.match(extensionsRegex)[1], target: `node${process.versions.node}`, format: format === 'module' ? 'esm' : 'cjs', }) if (warnings && warnings.length > 0) { for (const warning of warnings) { console.warn(warning.location) console.warn(warning.text) } } return { js, jsSourceMap } } function getTsCompatSpecifier(parentURL, specifier) { let tsSpecifier let search if (specifier.startsWith('./') || specifier.startsWith('../')) { // Relative import const url = new URL(specifier, parentURL) tsSpecifier = fileURLToPath(url).replace(/\.tsx?$/, '') search = url.search } else { // Bare import tsSpecifier = specifier search = '' } return { tsSpecifier, search, } } function isValidURL(s) { try { return !!new URL(s) } catch (e) { if (e instanceof TypeError) return false throw e } } async function resolveBase(specifier, context, defaultResolve) { const { parentURL, } = context if (httpRegex.test(specifier) || httpRegex.test(parentURL)) { return { url: new URL(specifier, parentURL).href, format: 'module', } } let url // According to Node's algorithm, we first check if it is a valid URL. // When the module is the entry point, node will provides a file URL to it. if (isValidURL(specifier)) { url = new URL(specifier) } else { // Try to resolve the module according to typescript's algorithm, // and construct a valid url. const parsed = getTsCompatSpecifier(parentURL, specifier) const path = await esbuildResolve(parsed.tsSpecifier, dirname(fileURLToPath(parentURL))) if (path) { url = pathToFileURL(path) url.search = parsed.search } } if (url) { // If the resolved file is typescript if (extensionsRegex.test(url.pathname)) { return { url: url.href, format: 'module', } } // Else, for other types, use default resolve with the valid path return defaultResolve(url.href, context, defaultResolve) } return defaultResolve(specifier, context, defaultResolve) } async function loadBase(url, context, defaultLoad) { if (httpRegex.test(url)) { return { format: 'module', source: await fetchNetworkModule(url), } } if (extensionsRegex.test(new URL(url).pathname)) { const { format } = context let filename = url if (!isWindows) filename = fileURLToPath(url) const rawSource = fs.readFileSync(new URL(url), { encoding: 'utf8' }) const { js } = esbuildTransformSync(rawSource, filename, url, format) return { format: 'module', source: js, } } // Let Node.js handle all other format / sources. return defaultLoad(url, context, defaultLoad) } function getFormatBase(url, context, defaultGetFormat) { if (httpRegex.test(url)) { return { format: 'module', } } if (extensionsRegex.test(new URL(url).pathname)) { return { format: 'module', } } // Let Node.js handle all other URLs. return defaultGetFormat(url, context, defaultGetFormat) } async function transformSourceBase(source, context, defaultTransformSource) { const { url, format } = context if (httpRegex.test(url)) { return { format: 'module', source: await fetchNetworkModule(url), } } if (extensionsRegex.test(new URL(url).pathname)) { let filename = url if (!isWindows) filename = fileURLToPath(url) const { js } = esbuildTransformSync(source, filename, url, format) return { source: js, } } // Let Node.js handle all other sources. return defaultTransformSource(source, context, defaultTransformSource) } async function getSourceBase(url, context, defaultGetSource) { if (httpRegex.test(url)) { return { source: await fetchNetworkModule(url), } } return defaultGetSource(url, context, defaultGetSource) } const networkModuleCache = new Map() function fetchNetworkModule(url) { if (!networkModuleCache.has(url)) { const promise = (async () => { const _fetch = (typeof fetch != 'undefined') ? fetch : (await import('node-fetch')).default return await _fetch(url).then(r => r.text()) })() networkModuleCache.set(url, promise) } return networkModuleCache.get(url) } // New hook starting from Node v16.12.0 // See: https://github.com/nodejs/node/pull/37468 const _resolve = resolveBase let _load, _getFormat, _transformSource, _getSource if (semver.satisfies(process.versions.node, '>=16.12.0')) { _load = loadBase } else { _getFormat = getFormatBase _transformSource = transformSourceBase _getSource = getSourceBase } export const resolve = _resolve export const load = _load export const getFormat = _getFormat export const transformSource = _transformSource export const getSource = _getSource export { networkModuleCache }