babel 源码分析 - 入口

5 篇文章 1 订阅
4 篇文章 0 订阅

babel 是日常工作中必不可少的依赖包,虽然天天在用,但是对于内部构造却了解不多,抽空看下了源码,从入口文件开始一步步解密 babel 是如何工作的。

当我们使用 npm run babel 的时候会执行到下面的代码,具体位置是 babel-cli/src/babel/index.js。

#!/usr/bin/env node

import parseArgv from "./options";
import dirCommand from "./dir";
import fileCommand from "./file";

const opts = parseArgv(process.argv);

if (opts) {
 // 🌵🌵🌵 执行这里 🌵🌵🌵
  const fn = opts.cliOptions.outDir ? dirCommand : fileCommand;
  fn(opts).catch(err => {
    console.error(err);
    process.exitCode = 1;
  });
} else {
  process.exitCode = 2;
}

这个这里会从 process 中获取配置信息,然后判断输入的是一个文件还是文件夹,假如我们的执行命令是
babel src --out-dir lib 那么这里的对应生成的 opts 如下:

{
  babelOptions: {},
  cliOptions: {
    filename: undefined,
    filenames: [ 'src' ],
    extensions: undefined,
    keepFileExtension: undefined,
    outFileExtension: undefined,
    watch: true,
    skipInitialBuild: undefined,
    outFile: undefined,
    outDir: 'lib',
    relative: undefined,
    copyFiles: undefined,
    copyIgnored: undefined,
    includeDotfiles: undefined,
    verbose: undefined,
    quiet: undefined,
    deleteDirOnStart: undefined,
    sourceMapTarget: undefined
  }
}

因为我们只传入了入口文件 src 和出口文件 lib 所以其他配置都是空的,这里的 outDir 是存在的,所以会执行 dirCommand 命令,进入 dir.js 中会执行如下代码:

  if (!cliOptions.skipInitialBuild) {
    if (cliOptions.deleteDirOnStart) {
      util.deleteDir(cliOptions.outDir);
    }

    fs.mkdirSync(cliOptions.outDir, { recursive: true });

    startTime = process.hrtime();

    for (const filename of cliOptions.filenames) {
      // 🌵🌵🌵 执行 handle('src') 🌵🌵🌵
      compiledFiles += await handle(filename);
    }

    if (!cliOptions.quiet) {
      logSuccess();
      logSuccess.flush();
    }
  }

也就是循环 filenames 输入然后传入到 handle 执行,这里对应就是执行 handle(‘src’),接着是 handle 的代码:

  async function handle(filenameOrDir: string): Promise<number> {
    if (!fs.existsSync(filenameOrDir)) return 0;

    const stat = fs.statSync(filenameOrDir);

    if (stat.isDirectory()) {
      const dirname = filenameOrDir;

      let count = 0;

      const files = util.readdir(dirname, cliOptions.includeDotfiles);
      for (const filename of files) {
        const src = path.join(dirname, filename);
				// 🌵🌵🌵 执行这里 🌵🌵🌵
        const written = await handleFile(src, dirname);
        if (written) count += 1;
      }

      return count;
    } else {
      const filename = filenameOrDir;
      const written = await handleFile(filename, path.dirname(filename));

      return written ? 1 : 0;
    }
  }

这个方法会读取 src 目录下的所有文件,组成数组,然后逐个传入到 handleFile 中执行

// 假设入口文件的目录结构如下
// ├── src
// │   └── index.js

async function handleFile(src: string, base: string): Promise<boolean> {

  // 🌵🌵🌵 执行 write('src/index.js', 'src') 🌵🌵🌵
  const written = await write(src, base);

if (
  (cliOptions.copyFiles && written === FILE_TYPE.NON_COMPILABLE) ||
  (cliOptions.copyIgnored && written === FILE_TYPE.IGNORED)
) {
  const filename = path.relative(base, src);
  const dest = getDest(filename, base);
  outputFileSync(dest, fs.readFileSync(src));
  util.chmod(src, dest);
}
return written === FILE_TYPE.COMPILED;
}

接着我们查看下 write 方法。

  async function write(
    src: string,
    base: string,
  ): Promise<$Keys<typeof FILE_TYPE>> {
    let relative = path.relative(base, src);

    relative = util.withExtension(
      relative,
      cliOptions.keepFileExtension
        ? path.extname(relative)
        : cliOptions.outFileExtension,
    );

    const dest = getDest(relative, base);
		// 🌵🌵🌵 执行这里 🌵🌵🌵
    const res = await util.compile(src, {
      ...babelOptions,
      sourceFileName: slash(path.relative(dest + "/..", src)),
    });

    if (!res) return FILE_TYPE.IGNORED;

    outputFileSync(dest, res.code);
    util.chmod(src, dest);

    return FILE_TYPE.COMPILED;
  }

其实就是传入了路径和配置然后执行 compile 方法。

export function compile(
  filename: string,
  opts: Object | Function,
): Promise<Object> {
  opts = {
    ...opts,
    caller: CALLER,
  };

  return new Promise((resolve, reject) => {
    // 🌵🌵🌵 执行这里 🌵🌵🌵
    babel.transformFile(filename, opts, (err, result) => {
      if (err) reject(err);
      else resolve(result);
    });
  });
}

然后将文件地址和配置传入到 babel.transformFile 中处理。

至此 babel-cli 的工作就执行完毕,接着会进入到 babel-core 中执行。

// babel-core/src/transform-file.ts

const transformFileRunner = gensync<
  (filename: string, opts?: InputOptions) => FileResult | null
>(function* (filename, opts: InputOptions) {
  const options = { ...opts, filename };

  const config: ResolvedConfig | null = yield* loadConfig(options);
  if (config === null) return null;

  const code = yield* fs.readFile(filename, "utf8");
  // 🌵🌵🌵 执行这里 🌵🌵🌵
  return yield* run(config, code);
});

这个文件主要使用 gensync 生成同步和异步两种执行方法,我们这里调用的是异步执行方式,接着继续向下执行run 方法。

export function* run(
  config: ResolvedConfig,
  code: string,
  ast?: t.File | t.Program | null,
): Handler<FileResult> {
  // 🌵🌵🌵 1. 将代码转化为 AST 🌵🌵🌵
  const file = yield* normalizeFile(
    config.passes,
    normalizeOptions(config),
    code,
    ast,
  );

  const opts = file.opts;
  try {
  	// 🌵🌵🌵 2. 将 ES6 的 AST 转化为 ES5 的 AST 🌵🌵🌵
    yield* transformFile(file, config.passes);
  } catch (e) {}

  let outputCode, outputMap;
  try {
    if (opts.code !== false) {
    	// 🌵🌵🌵 3. 将 ES5 的 AST 生成 ES5 代码 🌵🌵🌵
      ({ outputCode, outputMap } = generateCode(config.passes, file));
    }
  } catch (e) {}

  return {
    metadata: file.metadata,
    options: opts,
    ast: opts.ast === true ? file.ast : null,
    code: outputCode === undefined ? null : outputCode,
    map: outputMap === undefined ? null : outputMap,
    sourceType: file.ast.program.sourceType,
  };
}

这个方法主要做了三件事件:

  1. 通过 normalizeFile 将传入的文件转化为 AST。
  2. 通过 transformFile 处理 AST 产出新的 AST。
  3. 通过 generateCode 将新的 AST 转化为目标代码。

至此,babel-core 的工作就结束,至于三个步骤是如何运行的,分布在 babel-parser,babel-traverse, babel-generator 三个包中,具体下一篇文章会具体介绍。

下一篇:babel 源码分析 一 parse

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值