babel cli 源码分析

103 阅读3分钟

开始分析

version:7.22.5

具体位置:babel-cli/src/babel/index.ts

if (opts) {
// 根据配置中的outDir去决定使用文件夹命令还是文件命令
  const fn = opts.cliOptions.outDir ? dirCommand : fileCommand;
  fn(opts).catch(err => {
    console.error(err);
    process.exitCode = 1;
  });
} else {
  process.exitCode = 2;
}

假设我们启动的命令是babel src --out-dir lib 那我们获取的clioptions就是:

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

因为命令中我们只传了入口文件src和出口文件lib,所以其他配置都是undefined,这里outdir是存在,所以会读取dirCommand,进入dir.ts

具体位置: babel-cli/src/babel/dir.ts

export default async function ({
  cliOptions,
  babelOptions,
}: CmdOptions): Promise<void> {
   async function write(
    src: string,
    base: string,
  ): Promise<keyof typeof FILE_TYPE> {...}
  
  function getDest(filename: string, base: string): string {...}
  
  async function handleFile(src: string, base: string): Promise<boolean> {...}
  
  async function handle(filenameOrDir: string): Promise<number> {...}
  
  let compiledFiles = 0;
  let startTime: [number, number] | null = null;
  
  const logSuccess = util.debounce(function () {...},100)
  
  if (cliOptions.watch) watcher.enable({ enableGlobbing: true });

  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) {
      // compiledFiles is just incremented without reading its value, so we
      // don't risk race conditions.
      // eslint-disable-next-line require-atomic-updates
      compiledFiles += await handle(filename);
    }

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

可以看出里面由一些函数和主要逻辑组成,函数我们可以先忽略,直接看主要逻辑

 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) {
    // 把文件名传入hanle()执行
      compiledFiles += await handle(filename);
    }

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

说一下参数的作用

-w, --watchRecompile files on changes
--skip-initial-buildDo not compile files before watching
------------------------------------------------------------------
--delete-dir-on-startDelete the out directory before compilation
------------------------------------------------------------------

接下来我们看看hanle函数具体干了些什么。 这个方法会读取 src 目录下的所有文件,组成数组,然后逐个传入到 handleFile 中执行


  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);
        // 重点是这句,把文件路径传进handleFile函数里
        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;
    }
  }

然后就是 handleFile函数

async function handleFile(src: string, base: string): Promise<boolean> {
    // 重点是这句,执行write函数
    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<keyof typeof FILE_TYPE> {
  // 获取相对路径
    let relative = path.relative(base, src);
 // 测试文件名是否以可编译扩展名结尾。
    if (!util.isCompilableExtension(relative, cliOptions.extensions)) {
      return FILE_TYPE.NON_COMPILABLE;
    }

    relative = util.withExtension(
      relative,
      cliOptions.keepFileExtension
        ? path.extname(relative)
        : cliOptions.outFileExtension,
    );
    // 获取输出目录
    const dest = getDest(relative, base);

    try {
    // 重点执行语句
    // 把配置和文件传进compile里
      const res = await util.compile(src, {
        ...babelOptions,
        sourceFileName: slash(path.relative(dest + "/..", src)),
      });

      if (!res) return FILE_TYPE.IGNORED;
        // 是否有配置source map
      if (res.map) {
        let outputMap: "both" | "external" | false = false;
        if (babelOptions.sourceMaps && babelOptions.sourceMaps !== "inline") {
          outputMap = "external";
        } else if (babelOptions.sourceMaps == undefined) {
          outputMap = util.hasDataSourcemap(res.code) ? "external" : "both";
        }
        
        if (outputMap) {
          const mapLoc = dest + ".map";
          if (outputMap === "external") {
          // 如果source map类型不是inline的话,就在输出文件结尾加上source map 的location
            res.code = util.addSourceMappingUrl(res.code, mapLoc);
          }
          res.map.file = path.basename(relative);
          outputFileSync(mapLoc, JSON.stringify(res.map));
        }
      }
        // 输出最终转换后的文件
      outputFileSync(dest, res.code);
      util.chmod(src, dest);

      if (cliOptions.verbose) {
        console.log(path.relative(process.cwd(), src) + " -> " + dest);
      }

      return FILE_TYPE.COMPILED;
    } catch (err) {
      if (cliOptions.watch) {
        console.error(err);
        return FILE_TYPE.ERR_COMPILATION;
      }

      throw err;
    }
  }

所以write函数简单来说就是把文件和配置传进compile函数里,然后把输出的文件写回到输出文件目录中。 接下来看compile函数。


export async function compile(filename: string, opts: InputOptions) {
  opts = {
    ...opts,
    caller: CALLER,
  };

// 重点语句在这里,把输入文件和配置传进babel.transformFile里面
  const result = process.env.BABEL_8_BREAKING
    ? await babel.transformFileAsync(filename, opts)
    : await new Promise<FileResult>((resolve, reject) => {
        babel.transformFile(filename, opts, (err, result) => {
          if (err) reject(err);
          else resolve(result);
        });
      });

  if (result) {
    if (!process.env.BABEL_8_BREAKING) {
      if (!result.externalDependencies) return result;
    }
    watcher.updateExternalDependencies(filename, result.externalDependencies);
  }

  return result;
}

到这里其实babel/cli的工作已经完成,接下来要去看babel/core的工作原理。

总结

那么其实我们可以发现,其实babel/cli的工作其实并不多。就是处理传入的配置,处理路径问题。然后把需要处理的文件传进相关的函数。把最终传出的结果输出来。

最终目的就是一个CLI 命令行工具,可通过命令行编译文件。