xiaoxiangmoe / blog

0 stars 0 forks source link

如何实现一个 TypeScript 的宏 #2

Open xiaoxiangmoe opened 5 years ago

xiaoxiangmoe commented 5 years ago

如何实现一个 TypeScript 的宏

这只是一篇记录自己折腾经历的日记。对大家可能没什么帮助。

想法

Babel 是一个优秀的玩具,我们可以在上面做很多有趣的事情,于是我们有了非常多的 Babel 的 plugin,后来大家黑魔法玩多了,就出现了小伙伴说,我不想配置那么麻烦,就有小伙伴开始写了一个 babel-plugin-macros。(注:我是在 Create-React-App 的更新日志中发现了它,然后就用它写了一些好玩的东西。)

TypeScript 也开放了 transformations 的 API,我们是不是也可以做一些类似的事情呢?

查资料

作为没有学过编译原理的小白,我肯定是先去找资料学习一发。

那个时候,我还在用 create-react-app-typescript 写东西,它是使用 ts-loader 作为配置的,把它的文档仔仔细细看了一下,发现了 getCustomTransformers 的配置,这个配置还有相应的单元测试 ts-loader/test/comparison-tests/customTransformer at 401fc690ed78d9a6915d56a1e2b49dd5e32b69e6 · TypeStrong/ts-loader · GitHub 。 照着单元测试撸了一发,大体就知道了大概。学到的知识我就不细讲了,详见:手把手教写 TypeScript Transformer Plugin - 知乎 毕竟东西都差不多。

感觉还没过瘾,还是有点虚,就去看了一下 babel-plugin-macros 的源码和这个视频 YouTube - Writing custom Babel and ESLint plugins with ASTs (Open West 2017),我是从 babel-plugin-macros 的指南中找到它的。

大致清楚了 babel-plugin-macros 做的事情了(帮助用户找到 import 进来所需要的索引,然后用户去根据索引去找到对应的 ast 节点并替换了它)

计划与实践

基本环境搭建

这里花了很多的事情在 yarn lerna jest,以及各种编译工具的调研使用上。

最后决定了使用 rollup-plugin-typescript2 作为 Transformer 的第一个适配对象(因为 rollup 简单啊,而且可配置)

以及 microbundle 作为最简单的免配置的编译工具(免配置!没特殊需求时候简直不能更棒!)

yarn 的 workspace 很适合我这种需要多个包的构建的项目

jest 的 snapshot 功能我很喜欢

……

在诸多工具都选择好了之后,最后终于搭建了啥代码都没有的空架子。

变量使用收集

我们需要做 babel-plugin-macros 类似的事情。我们第一步需要做到收集所有的 import 进来的 declaration 的变量,被哪些地方引用了。我不知道 TypeScript 是否有这种 API,我就厚着脸皮去问了: API about Identifier's scope · Issue #28026 · Microsoft/TypeScript · GitHub 然后有个好心的哥们把自己的库推荐给我了,感动到哭。

替换节点的 API 设计

babel-plugin-macros 采用了直接提供目标节点的列表,让我们直接去替换掉,但是 TypeScript 的 transform API 是输入一个节点,输出一个节点,也就意味着,我们不能做到改变父节点。

我想来想去,决定给每个自定义的宏传递一个函数 reference。告诉他们,你传进来的节点是否是当下的宏的引用,大体的设计如 NodeTransformParameter

作为宏的作者,只要 export 一个 __typescriptMacroNodeTransformFunction,它的类型签名是 TypeScriptMacroNodeTransformFunction 就好了

遍历的逻辑

参考了 babel-plugin-macros, 我会按照每个宏的 import 顺序去遍历一遍全部节点,如果你在一个文件import了十次宏,那就会遍历十遍。见 transformerFactoryCreator,遍历的时候是一个递归的处理,见于: visitEachChild(ret, visitor)

样例的编写

为了证明我的宏引擎的确有效,我按时间顺序大致写了这么几个宏

后记

其实这还有超级多的存在,而且缺乏相应的讨论和反馈,有兴趣的可以来和我吵架。而且我其实已经操着不及格的英语水平在吵架了呢,而且这里的讨论也给我攒了三十几个 star, 见:Let's discuss TypeScript support. · Issue #94 · kentcdodds/babel-plugin-macros · GitHub ,你们有兴趣也可以参与进来啊。

bolasblack commented 5 years ago

我之前也尝试在 TypeScript 里用 babel-plugin-macro ,不过当时想的是用一种取巧的办法,就是用 babel-presets-typescript 来编译 TS ,然后提供一个 .d.ts 文件给宏的使用者,可惜这样子的话就没办法做类似 transformer-keys.tsmacro 这个宏做的事情了

不过这个事情后来因为在研究一些别的事情一直耽搁了,这次看完了你们的讨论,感觉很有意思

虽然心有余,但是感觉我近期可能没办法参与进来帮忙,不管怎么样,还是要谢谢你的努力,我觉得不论最后成功与否,这个尝试本身也是非常有价值的,如果能完成的话那应该会对非常多的人产生帮助(至少对我非常有帮助哈哈🤪)