esbuild-register 浅析笔记

527 阅读1分钟

利用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 {}
  }
}