Open xiaoxiangmoe opened 5 years ago
我之前也尝试在 TypeScript 里用 babel-plugin-macro ,不过当时想的是用一种取巧的办法,就是用 babel-presets-typescript 来编译 TS ,然后提供一个 .d.ts 文件给宏的使用者,可惜这样子的话就没办法做类似 transformer-keys.tsmacro 这个宏做的事情了
不过这个事情后来因为在研究一些别的事情一直耽搁了,这次看完了你们的讨论,感觉很有意思
虽然心有余,但是感觉我近期可能没办法参与进来帮忙,不管怎么样,还是要谢谢你的努力,我觉得不论最后成功与否,这个尝试本身也是非常有价值的,如果能完成的话那应该会对非常多的人产生帮助(至少对我非常有帮助哈哈🤪)
如何实现一个 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)
样例的编写
为了证明我的宏引擎的确有效,我按时间顺序大致写了这么几个宏
uppercase.tsmacro 证明的确能在编译期做事情
console-scope.tsmacro 证明能做一些好玩的事情,比如帮忙打出一堆 log
hooks.tsmacro 证明能写有用的代码,比如生成 React Hooks 的一些烦人的需要手填的参数。这个还有个小 bug,你猜猜是啥?
transformer-keys.tsmacro 证明可以把编译期的类型参数放到运行时,我们也许可以做更多,比如这个知乎讨论中说的事情
interop-export-macros.tsmacro 和 lowercase.macro 证明和 babel-plugin-macros 可以和谐共存
后记
其实这还有超级多的坑存在,而且缺乏相应的讨论和反馈,有兴趣的可以来和我吵架。而且我其实已经操着不及格的英语水平在吵架了呢,而且这里的讨论也给我攒了三十几个 star, 见:Let's discuss TypeScript support. · Issue #94 · kentcdodds/babel-plugin-macros · GitHub ,你们有兴趣也可以参与进来啊。