利用require hook ,使用 esbuild 去编译代码 , 核心代码, addhook 添加 require hook 的hijack,installSourceMapSupport 添加 sourcemap 支持,patchCommonJsLoader 对于某些 esm的 require 错误进行重试,编译成 cjs 再暴露出去
const revert = addHook(compile, {
exts: extensions,
ignoreNodeModules: hookIgnoreNodeModules,
matcher: hookMatcher,
})
installSourceMapSupport()
patchCommonJsLoader(compile)
const unregisterTsconfigPaths = registerTsconfigPaths()
return {
unregister() {
revert()
unregisterTsconfigPaths()
},
}
添加 sourcemap 支持,如果 node 版本支持开启,则打开,否则使用sourceMapSupport进行支持
function installSourceMapSupport() {
if ((process as any).setSourceMapsEnabled) {
(process as any).setSourceMapsEnabled(true);
} else {
sourceMapSupport.install({
handleUncaughtExceptions: false,
environment: 'node',
retrieveSourceMap(file) {
if (map[file]) {
return {
url: file,
map: map[file],
}
}
return null
},
})
}
}
patchCommonJsLoader, 可以看到,对于require 错误,如果不是 ERR_REQUIRE_ESM错误,则不进行处理,如果是esm的,则进行编译, 打补丁的原因可以看看issue
/**
* Patch the Node CJS loader to suppress the ESM error
* https://github.com/nodejs/node/blob/069b5df/lib/internal/modules/cjs/loader.js#L1125
*
* As per https://github.com/standard-things/esm/issues/868#issuecomment-594480715
*/
function patchCommonJsLoader(compile: COMPILE) {
// @ts-expect-error
const extensions = module.Module._extensions
const jsHandler = extensions['.js']
extensions['.js'] = function (module: any, filename: string) {
try {
return jsHandler.call(this, module, filename)
} catch (error: any) {
if (error.code !== 'ERR_REQUIRE_ESM') {
throw error
}
let content = fs.readFileSync(filename, 'utf8')
content = compile(content, filename, 'cjs')
module._compile(content, filename)
}
}
}
再看看编译函数,调用 esbuild 的transformSync进行编译代码
const compile: COMPILE = function compile(code, filename, format) {
const dir = dirname(filename)
const options = getOptions(dir)
format = format ?? inferPackageFormat(dir, filename)
const {
code: js,
warnings,
map: jsSourceMap,
} = transformSync(code, {
sourcefile: filename,
sourcemap: 'both',
loader: getLoader(filename),
target: options.target,
jsxFactory: options.jsxFactory,
jsxFragment: options.jsxFragment,
format,
...overrides,
})
map[filename] = jsSourceMap
if (warnings && warnings.length > 0) {
for (const warning of warnings) {
console.log(warning.location)
console.log(warning.text)
}
}
if (format === 'esm') return js
return removeNodePrefix(js)
}
FILE_LOADERS 就是为了告诉esbuild使用什么loder
type LOADERS = 'js' | 'jsx' | 'ts' | 'tsx'
const FILE_LOADERS = {
'.js': 'js',
'.jsx': 'jsx',
'.ts': 'ts',
'.tsx': 'tsx',
'.mjs': 'js',
} as const
最后看一个不使用json.parse 格式化代码的小技巧,利用 new Function
export function jsoncParse(data: string) {
try {
return new Function('return ' + stripJsonComments(data).trim())()
} catch (_) {
// Silently ignore any error
// That's what tsc/jsonc-parser did after all
return {}
}
}