babel详解(七):配置文件

本篇记录babel配置文件相关的要点。

概述

babel7目前来说有以下四种配置的方式:

  1. babel.config.js
    在项目的root目录,新建一个babel.config.js文件,然后就可以把这个配置应用到整个项目范围内。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    module.exports = function (api) {
    api.cache(true);

    const presets = [ ];// 类似.babelrc.js中的presets
    const plugins = [ ];// 类似.babelrc.js中的plugins

    return {
    presets,
    plugins
    };
    }
  2. .babelrc
    在项目的package.json同目录,新建.babelrc文件,然后用json格式来编写配置:

    1
    2
    3
    4
    {
    "presets": [],
    "plugins": []
    }

    .babelrc等效的还有两种形式。第一种是之前博客中一直在用的.babelrc.js文件,它们的区别,就是.babelrc.js是通过js编写的,所以具备动态配置的能力。第二种形式,是直接在项目的package.json文件中编写配置,如:

    1
    2
    3
    4
    5
    6
    7
    8
    {
    "name": "my-package",
    "version": "1.0.0",
    "babel": {
    "presets": [ ],
    "plugins": [ ]
    }
    }
  3. 直接通过cli命令行传递配置
    举例:

    1
    babel --plugins @babel/plugin-transform-arrow-functions script.js
  4. 使用api: babel.transform
    举例:

    1
    2
    3
    require("@babel/core").transform("code", {
    plugins: ["@babel/plugin-transform-arrow-functions"]
    });

    通过@babel/core,可以对代码进行动态转码。

第1、2种是babel配置方式的重点,所以后面重点解释。

项目范围的配置

Babel7开始,Babel具有“根”目录的概念,默认为当前工作目录。对于项目范围的配置,Babel将自动在此根目录中搜索babel.config.js。 或者用户可以使用显式指定configFileoption来覆盖默认的配置文件搜索行为。

由于项目范围的配置文件与配置文件的物理位置是分开的,因此它们非常适合必须广泛应用的配置,甚至允许plugins和presets轻松应用于node_modulessymlinked packages中的文件。

babel.config.js的配置,有两种情况会启用。第一种,就是在babel.config.js所在目录运行babel,会自动去寻找babel的root目录下有没有babel.config.js。假如有以下项目结构:

1
2
3
4
repo-root/
src/
main.js
babel.config.js

如果在repo-root目录运行npx babel src -d dist,则src/main.js会被成功转码。如果先cd src/,然后再运行npx babel . -d dist转码,则babel此时的root目录为repo-root/src,无法找到repo-root/babel.config.js,导致无法转码。

第二种,是通过configFile这个option,明确地指定babel.config.js文件的位置。假如有以下项目结构:

1
2
3
4
5
repo-root/
src/
main.js
build.js
babel.config.js

main.js是一段很简单的ES代码:

1
2
let foo = () => {
};

build.js是一个将被node运行的文件,它会调用babel.transform进行编程式转码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let fs = require('fs');
let babel = require('@babel/core');

console.log(process.env.NODE_ENV);

let code = fs.readFileSync('./main.js').toString();

babel.transform(code, {
configFile: '../babel.config.js'
}, function(err, result) {
let { code, ast } = result;

console.log(code);
console.log(ast);
});

这个文件内通过configFileoption,启用了repo-root/babel.config.js这个配置文件。 运行cd src && node build.js,将能看到正确的转码输出,说明configFileoption已生效。

babel.config.js的其它作用,就是它可以对node_modulessymlinked packages内的文件进行转码。为了测试这个功能,可以运行npm install lodash-es --save,把这个ES modules版本的lodash安装到项目中。 然后在项目的根目录添加一个.babelrc.js,把正常的配置写进去;然后运行npx babel ./node_modules/lodash-es/array.js -d dist,会发现array.js不会被转码。 接着删除.babelrc.js,然后在项目根目录创建一个babel.config.js并做好相同配置,然后再次运行npx babel ./node_modules/lodash-es/array.js -d dist,此时就会看到array.js被babel转码了。

symlinked packages也是类似的。 为了测试这个特性,先建立一个如下结构的文件夹:

1
2
3
/home/learn/some-package
bar.js
package.json

注意:/home/learn/some-package/package.json必须得有,里面可以是空的。接着在bar.js里编写一些简单的ES代码:

1
export default 'bar';

然后再新建一个如下结构的文件夹:(通过软连接,把src/outer-package链接到/home/learn/some-package)

1
2
3
4
5
6
/home/learn/repo-root
src
outer-package/ # symlinked to `/home/learn/some-package`
main.js
package.json
.babelrc.js

main.js里写点简单的ES代码:

1
2
let foo = () => {
};

为了验证babel.config.js的作用,先把babel配置放到.babelrc.js里面:

1
2
3
4
5
6
7
module.exports = {
presets: [
[
'@babel/preset-env'
]
]
}

运行npx babel src -d dist,会发现src/main.js被转码,而src/outer-package/bar.js没有被转码。

如果把.babelrc.js替换为babel.config.js

1
2
3
4
5
6
7
8
9
10
11
module.exports = function(api){
api.cache(true);

return {
presets: [
[
'@babel/preset-env'
]
]
};
};

再次运行npx babel src/outer-package -d dist,会发现src/outer-package/bar.jssrc/main.js都被转码了。这就说明babel.config.js,是会把babel的转码范围扩大到symlinked packages内的。曾经babel6,要想包含对node_modulessymlinked packages内的文件进行处理,似乎是很麻烦的,babel7简化了这些工作。

babel.config.js会成为未来babel主要的配置方式,这也是向webpack等工具看齐的举措吧!

另外,babel自动搜索babel.config.js作为配置文件的行为,可以明确地指定configFile: false来关闭。

相对文件的配置

.babelrc .babelrc.js以及package.json文件中的babel,三种配置方式都是相对文件的配置方式。 babel7重新调整了这种配置的默认搜索行为:它会从当前正在编译的文件所在文件夹开始,基于它的filename,向上搜索父级文件夹中包含的.babelrc文件(或.babelrc.js文件,以及package.jsonbabel配置节),找到则停止搜索。

注意:

  1. 往上搜索配置的过程中,如果在某一层找到了package.json文件,就会停止搜索,这种配置的作用范围限定在单个的package内(babel用package.json文件来划定package的范围);
  2. 这种搜索行为找到的配置,如.babelrc文件,必须位于babel运行的root目录下,或者是包含在babelrcRoots这个option配置的目录下,否则找到的配置会直接被忽略;

在大部分情况下,使用相对文件的配置和使用项目范围的配置区别不大,babel之所以要分出这两种配置,是为了方便开发者想要管理类似@babel这种mono packages的时候,既能统一集中的管理通用的babel配置(项目范围的配置),又能根据各个package的特殊性单独做额外的配置(相对文件的配置);当两种配置同时找到了的时候,相对文件的配置,将会与项目范围的配置进行合并,然后才应用到子package。

准备如下一个项目结构:

1
2
3
4
5
6
7
src/
mod/
util/
main.js
package.json
package.json
.babelrc.js

main.js:

1
2
3
let foo = () => {

};

两个package.json都是:{}

如果cd src/ && npx babel src -d dist,会发现main.js不会被转码。 这是因为main.js同级的位置有一个package.json文件,当main.js被babel编译的时候,它会相对main.js去找.babelrc.js配置,但是遇到package.json的文件夹,就会停止,所以导致编译main.js,找不到src同级的.babelrc.js文件。只要把util/package.json删掉,再次编译,即可恢复正常。

如果文件内容不变,把文件夹结构调整为:

1
2
3
4
5
6
7
src/
mod/
util/
main.js
package.json
.babelrc.js
package.json

如果cd src/ && npx babel src -d dist,会发现main.js还是不会被转码。这是因为运行babel的目录是src,也就是babel运行的根目录是src/,虽然这次相对main.js可以找到一个.babelrc.js文件,但是因为这个文件不位于babel的root目录,所以导致它会直接被忽略。解决这个问题有两个办法:

  1. 运行:cd src/mod/util && npx babel . -d dist

  2. src/目录同级的位置添加一个babel.config.js,用来配置babelrcRoots,内容:

    1
    2
    3
    4
    5
    6
    module.exports = {
    babelrcRoots: [
    ".",
    "mod/util",
    ]
    }

    这个情况的文件夹结构为:

    1
    2
    3
    4
    5
    6
    7
    8
    src/
    mod/
    util/
    main.js
    package.json
    .babelrc.js
    package.json
    babel.config.js

    然后继续使用cd src/ && npx babel src -d dist即可正常转码main.js
    注意:babelrcRoots不能用.babelrc.babelrc.js文件来替代,否则还是不能正常转码。

babel6 vs babel7

相对文件的配置在babel6中,是已经存在的特性,babel7对这个特性调整地比较大,这是因为babel6下的相对文件的配置行为有以下几个问题:

  1. babel6下的.babelrc文件,有时候会出人意料地应用到node_modules里面的文件;
  2. babel6下的.babelrc文件,无法应用到symlinkednode_modules
  3. babel6下,node_modules的package内包含的.babelrc文件也会检测到,但是这些配置内依赖的presets和plugins可能外部主体包都没有安装,而且这些配置内用到的版本很可能也跟外部主体包安装的版本不一致。

在babel6,类似下面的配置结构可以应用到packages下的所有mod文件夹:

1
2
3
4
5
6
7
8
.babelrc
packages/
mod1/
package.json
src/index.js
mod2/
package.json
src/index.js

但是在babel7,无法生效了,因为.babelrc文件遇到package.json就停止搜索了。所以顶层的.babelrc没法应用到。解决这种情况,可以在每个mod文件夹下面,再单独建立一个.babelrc文件,通过extendsoption,来引用顶层的.babelrc文件:

1
{ "extends": "../../.babelrc" }

文件夹结构:

1
2
3
4
5
6
7
8
9
10
.babelrc
packages/
mod1/
.babelrc
package.json
src/index.js
mod2/
.babelrc
package.json
src/index.js

但是这个办法,如果在运行babel的时候,是:cd packages/ && npx babel packages/mod1/src -d dist这种方式运行,那么mod1 mod2下面的.babelrc文件,也无法生效,因为它们不在babel运行的根目录下;所以只能用cd packages/mod1 && npx babel src -d dist这种方式运行。

babel7推荐,把顶层的.babelrc替换为babel.config.js,各个mod下面的.babelrc文件可以删掉。 如果用cd packages/ && npx babel packages/mod1/src -d dist运行,肯定会正确转码;如果先cd packages/mod1切换到子模块,则可以通过configFile来指定配置文件的方式运行:npx babel --config-file="../../babel.config.js" src -d dist。 这样的话,不管是在哪个文件夹运行babel,都是OK的。

Monorepos

monorepos结构的库,一般都是类似下面的源码结构:

1
2
3
4
5
6
7
8
9
mono-repo
package.json
packages/
package-b/
.babelrc
src/
package-a/
package.json
src/

对于monorepo项目的配置,要理解的核心是Babel将你的工作目录视为root目录,如果你想在特定子包中运行Babel工具而不将Babel应用于整个repo,则会导致问题。

任何monorepo项目的第一步应该是在repository root中创建一个babel.config.js文件。 这确定了Babel的存储库基本目录的核心概念。

通常可以将所有repo配置放在root babel.config.js中。 使用overrides,可以轻松指定仅适用于某些子文件夹的配置,这通常比在repo中创建许多.babelrc文件更容易。

这种做法最容易遇到的一个问题,就是在子文件夹里面运行babel:

1
2
cd packages/package-a;
babel src -d dist

由于babel.config.js位于repo目录里面,如果从子文件夹运行,那么babel的root目录就是那个子文件夹,它就无法找到root目录下的babel.config.js文件,所以上面的命令根本不会有期望的转码作用。

要解决这个问题,就需要告诉babel在子文件夹的时候,怎么找到babel.config.js

  1. 明确的指定configFile,类似这样:npx babel --config-file="../../babel.config.js" src -d dist
  2. 借助rootMode这个option,将它配置为:{rootMode: "upward"}upward的作用就是告诉babel从当前的工作目录,向父级文件夹向上查找babel.config.js,如果从上层文件夹找到了babel.config.js,就启用该配置文件,同时把它所在目录作为当前运行的root目录。也就是说有了rootMode: upward,就等同于在repo目录运行babel。

设定rootMode: upward的方式有很多种,比如:

  1. cli

    1
    npx babel --root-mode upward src -d lib
  2. @babel/register

    1
    2
    3
    require("@babel/register")({
    rootMode: "upward"
    });
  3. webpack

    1
    2
    3
    4
    5
    6
    7
    8
    module: {
    rules: [{
    loader: "babel-loader",
    options: {
    rootMode: "upward",
    }
    }]
    }

前面说的这种方式,应该适用好多monorepo的情况,这样的方式只需要一个babel.config.js就够了,比较简单好用,而且通过rootMode: upward,不管是在repo还是子文件夹运行babel,都能得到正常的结果。

如果想在子文件夹里通过单独的.babelrc文件再做配置,一定要在repobabel.config.js里面配置babelrcRoots。 假如有以下一个monorepo项目:

1
2
3
4
5
6
7
8
monorepo/
package.json
babel.config.js
packages/
mod/
package.json
.babelrc
index.js

如果从monorepo/目录运行npx babel packages/mod/index.js,是不会启用packages/.babelrc文件的,前面说过的,.babelrc文件必须在babel运行的root目录或者是babelrcRootsoption配置的目录范围内,才会被加载。

monorepo目录运行时,root目录是monorepo/。解决这个问题的办法是在babel.config.js里面配置babelrcRootsoption:

1
2
3
4
babelrcRoots: [
".",
"packages/*",
]

这样的话,再从monorepo/下运行npx babel packages/mod/index.js,就能启用packages/.babelrc文件了。

如果从子文件夹运行babel,还是要选择configFile或者是rootMode其中一种来运行,否则babel只能加载到子文件夹中的.babelrc文件,但是不知道从哪加载babel.config.js文件。