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,
};
}
这个方法主要做了三件事件:
- 通过 normalizeFile 将传入的文件转化为 AST。
- 通过 transformFile 处理 AST 产出新的 AST。
- 通过 generateCode 将新的 AST 转化为目标代码。
至此,babel-core 的工作就结束,至于三个步骤是如何运行的,分布在 babel-parser,babel-traverse, babel-generator 三个包中,具体下一篇文章会具体介绍。