开始分析
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, --watch | Recompile files on changes |
|---|---|
--skip-initial-build | Do not compile files before watching |
| ----------------------- | ------------------------------------------- |
--delete-dir-on-start | Delete 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 命令行工具,可通过命令行编译文件。