美文网首页
[Node] 随遇而安 TypeScript(六):babel

[Node] 随遇而安 TypeScript(六):babel

作者: 何幻 | 来源:发表于2020-05-27 18:11 被阅读0次

背景

上文我们阅读了 @typescript-eslint/parser 的源码,
它首先将 TypeScript 源码解析为一棵标准的 TypeScript AST,
然后再转换为一棵 ESTree,供 ESLint rules 使用。

除了 ESLint 之外,对 .ts 文件进行解析的场景还有很多,
比如,在 babel 中加载 .ts 文件。
本文将详细看下 babel 是怎么对 .ts 文件进行解析的。

1. 调试 babel

为了对 babel 解析 .ts 文件的过程进行跟踪,我新建了一个调试项目,
用 VSCode 打开安装依赖后,按 F5 就可进行调试了。

源码地址:https://github.com/thzt/debug-babel

1.1 目录结构

debug-babel
├── .babelrc         <- babel 配置
├── .gitignore
├── .vscode
│   └── launch.json  <- VSCode 调试配置
├── package.json
└── src
    └── index.ts     <- 测试 babel 功能的 .ts 文件

1.2 外部依赖

package.json 中增加了这些依赖,

{
  ...,
  "devDependencies": {
    "@babel/cli": "^7.10.0",
    "@babel/core": "^7.10.0",
    "@babel/preset-typescript": "^7.9.0"
  }
}

1.3 测试文件 & ESLint 配置

测试文件,src/index.ts

const a: number = 1;

babel 默认的配置文件 .babelrc

{
  "presets": [
    "@babel/preset-typescript"
  ]
}

我们设置 presets 值为 @babel/preset-typescript,就可以处理 .ts 文件了。

1.4 VSCode 调试配置

VSCode 默认的调试配置文件,.vscode/launch.json

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Launch Program",
      "skipFiles": [
        "<node_internals>/**"
      ],
      "runtimeExecutable": "npm",
      "runtimeArgs": [
        "run-script",
        "debug"
      ],
      "port": 5858,
      "stopOnEntry": true
    }
  ]
}

其中,runtimeExecutableruntimeArgsportstopOnEntry 是我后来添加的,
用于和 package.json 中的 dubug npm scripts 配合使用。

因此,package.json 中需要新增一个 debug scripts,

{
  ...,
  "scripts": {
    "build": "babel src --out-dir dist --extensions '.ts'",
    "debug": "node --inspect-brk=5858 node_modules/.bin/babel src --out-dir dist --extensions '.ts'"
  },
}

这样当 VSCode 启动调试时,会自动执行 debug 这条 scripts,并进入断点了。

1.5 运行

使用 babel 编译源文件,

$ cd debug-babel
$ npm i
$ npm run build

> debug-babel@1.0.0 build /Users/.../debug-babel
> babel src --out-dir dist --extensions '.ts'

Successfully compiled 1 file with Babel (242ms).

编译完成后,会在 dist/ 目录生成一个 index.js 文件,

const a = 1;

对比 src/index.ts

const a: number = 1;

发现 babel 已经把 TypeScript 源码的类型标签去掉,转换成 JavaScript 了。

1.6 调试

使用 VSCode 打开 debug-babel 项目,
安装完依赖后,按 F5 启动调试。

2. 转译流程

2.1 @babel/cli

启动调试后,首先进入的是 babel-cli/bin/babel.js#L1
这是因为我们在 .vscode/launch.json 中配置了 stopOnEntrytrue

后面我们仔细的跟进源码,找到了一个能反映 babel 业务逻辑的典型位置。
babel-cli/src/babel/util.js#L90


我们来看调用栈,最下面那个匿名函数(anonymous function)就是 @babel/cli/bin/babel.js 这里。
沿着调用链路,babel 总共做了这些事情。
(1)handle:处理待编译的文件目录,我们在 debug scripts 中配置的编译目录为 src
(2)handleFile:逐个处理编译目录中的各个文件
(3)compile:编译
最后调用了 babel().transformFile 方法实现编译过程。

2.2 @babel/core

这个 transformFile 却是在 babel-core/src/transform-file.js#L27 定义的。
我们从 babel-cli 模块来到了 babel-core 中。

const transformFileRunner = gensync<[string, ?InputOptions], FileResult | null>(
  function*(filename, opts) {  // <- transformFile 函数
    ...,
    return yield* run(config, code);
  },
);

后面 runbabel-core/src/transformation/index.js#L29 就是具体的代码转译过程了。

export function* run(
  ...
): ... {
  const file = yield* normalizeFile(
    ...
  );
  ...
  try {
    yield* transformFile(file, config.passes);
  } catch (e) {
    ...
  }
  ...
  try {
    if (opts.code !== false) {
      ({ outputCode, outputMap } = generateCode(config.passes, file));
    }
  } catch (e) {
    ...
  }
}

它总共做了 3 件事,
(1)normalizeFile:这个调用完之后,就已经有 AST 了
(2)transformFile:对 AST 进行其他 config.passes 的转换
(3)generateCode:生成目标(.js)文件

3. normalizeFile

下文我们重点分析 normalizeFile,来看它是怎么解析 .ts 文件的。

3.1 @babel/parser

normalizeFile 位于 babel-core/src/transformation/normalize-file.js#L23

export default function* normalizeFile(
  ...
): ... {
  ...
  if (ast) {
    ...
  } else {
    ast = yield* parser(pluginPasses, options, code);
  }
  ...
}

normalizeFile 调用了 parser,位于 babel-core/src/parser/index.js#L10

import { parse } from "@babel/parser";
...
export default function* parser(
  ...
): ... {
  try {
    ...
    if (results.length === 0) {
      return parse(code, parserOpts);
    } else if (results.length === 1) {
      ...
    }
    ...
  } catch (err) {
    ...
  }
}

然后又调用了 @babel/parser 导出的 parse 方法,
位于 babel-parser/src/index.js#L18

export function parse(input: string, options?: Options): File {
  if (options?.sourceType === "unambiguous") {
    ...
  } else {
    return getParser(options, input).parse();
  }
}

其中 getParserbabel-parser/src/index.js#L72,是 @babel/parser 中的一个关键函数,
它会根据传入的 options.plugins 来动态得到一个被不同层数包装后的 parser。

3.2 getParser

我们来重点看下 getParserbabel-parser/src/index.js#L72,是 @babel/parser

function getParser(options: ?Options, input: string): Parser {
  let cls = Parser;
  if (options?.plugins) {
    ...
    cls = getParserClass(options.plugins);
  }

  return new cls(options, input);
}

function getParserClass(pluginsFromOptions: PluginList): Class<Parser> {
  const pluginList = mixinPluginNames.filter(name =>
    hasPlugin(pluginsFromOptions, name),
  );
  ...
  if (!cls) {
    cls = Parser;
    for (const plugin of pluginList) {
      cls = mixinPlugins[plugin](cls);
    }
    ...
  }
  return cls;
}

getParser 调用了 getParserClass 得到了一个 parser 类 cls
然后返回了 new 这个类的结果,得到了一个 parser 实例。

这个 parser 类 cls 是动态生成的,在 getParserClass babel-parser/src/index.js#L85 函数中,
先是通过 mixinPluginNames 过滤了以下,看看哪个 plugin 可以用于 mixin,
然后,就对原始的 Parser 拿每个插件依次 mixin(包装)。

看到这里,给我们的感觉是,这些 plugin 的 mixin 顺序应该不能打乱。


找到 mixinPluginNames babel-parser/src/plugin-utils.js#L126 的定义,

// These plugins are defined using a mixin which extends the parser class.
import estree from "./plugins/estree";
import flow from "./plugins/flow";
import jsx from "./plugins/jsx";
import typescript from "./plugins/typescript";
import placeholders from "./plugins/placeholders";
import v8intrinsic from "./plugins/v8intrinsic";

// NOTE: order is important. estree must come first; placeholders must come last.
export const mixinPlugins: { [name: string]: MixinPlugin } = {
  estree,
  jsx,
  flow,
  typescript,
  v8intrinsic,
  placeholders,
};

发现果然 “order is important”。

过滤以后,pluginList 就只剩下 typescript 了。


要使用 babel-parser/src/plugins/typescript/index.js 来 mixin(包装)parser 了。

3.3 mixin

对 parser 进行 mixin(包装)的过程非常巧妙,
它利用了 JavaScript 语言的类继承,把 babel 最终要用的 parser 分成了多个层次。

其中最里面的 parser,babel-parser/src/parser/index.js#L18 用来解析标准的 ECMAScript,
plugin mixin 之后,就能解析比 ECMAScript 更广泛的语言超集了。

例如,typescript mixin 后,就可以解析 TypeScript了,
jsx minix 之后,就可以解析 JSX 标签了。

这种设计,甚至贯穿于最里面的那个 parser 设计中。
我们来看下这些 parser 的类继承结构。

BaseParser
  CommentsParser 
    LocationParser
      Tokenizer                   <- 词法分析器竟然也是从 BaseParser 继承下来的
        UtilParser
          NodeUtils
            LValParser
              ExpressionParser
                StatementParser
                  Parser          <- babel 的默认 parser,即 plugin mixin 的那个 parser

                    ESTreeParser  <- plugin 必须按顺序 mixin,每次 mixin 产生一个新的子类
                      JSXParser
                        FlowParser
                          TypeScriptParser
                            V8IntrinsicParser
                              PlaceHoldersParser  

我们知道手工编写的递归下降解析器,会包含很多互相调用的 parseXXX 函数,
这些 parseXXX 经常会放到一个文件中,导致这个文件非常的长。

babel 通过类继承的方式,巧妙的把这些 parseXXX 函数拆分开了,
每个子类都能访问父类定义的 parseXXX 函数,
同时为了避免文件之间互相引用,有互递归调用的 parseXXX 函数,放到了一起,构成了一个类层次。

我们来看看 babel 的默认 parser babel-parser/src/parser/index.js#L57 能简洁到什么程度吧,

export default class Parser extends StatementParser {
  constructor(options: ?Options, input: string) {
    ...
  }
  ...
  parse(): File {
    ...
    const file = this.startNode();
    const program = this.startNode();
    this.nextToken();
    ...
    this.parseTopLevel(file, program);
    ...
    return file;
  }
}

它继承了 StatementParser,然后只是调用了父类的 parseTopLevel babel-parser/src/parser/statement.js#L50 方法就解决问题了。

3.4 parseTopLevel

babel 的具体 parse 过程,跟 TypeScript 大同小异。
这里就不再详细介绍了。

总之,babel 使用了“合适的”(被 plugins mixin 后的) parser,对源码进行了解析,得到了一棵 AST。
完成了 normalizeFile babel-core/src/transformation/normalize-file.js#L23 的过程。

export default function* normalizeFile(
  ...
): ... {
  ...
  if (ast) {
    ...
  } else {
    ast = yield* parser(pluginPasses, options, code);
  }
  ...
}

总结

本文介绍了 babel @babel/preset-typescript 解析 .ts 文件的过程,
值得注意的是,babel 并没有使用 TypeScript 官方的 typescript 模块进行解析。
而是,手工实现了一个 parser。

这个手工实现的 parser 设计十分精巧,挂载不同的 babel plugin 之后,
就会在原始的 ECMAScript parser 之上,包装出能解析更宽泛语言集合的新 parser。

这一点还是非常值得学习的。


参考

github: debug-babel
@babel/cli v7.10.0
@babel/core v7.10.0
@babel/parser v7.10.0

相关文章

网友评论

      本文标题:[Node] 随遇而安 TypeScript(六):babel

      本文链接:https://www.haomeiwen.com/subject/eovwahtx.html