之前前端代码覆盖率一直用的 babel-istanbul-plugin, 但是这种方式的弊端非常的多。需要跟开发的编译逻辑结合在一起,一旦编译的框架有区别,就得花很大的时间精力在上边,并且还得去适配不同的 babel 的版本。所以这次乘着调研 react-native, 尝试通过 nyc 的方式进行代码插桩。
nyc 的插桩使用非常的方便, 如下即可
nyc instrument <input> [output]
所以我们通过 ./node_modules/.bin/nyc instrument ./src ./src --complete-copy --in-place
直接进行插桩处理。
嗯,看上去一切都很顺利。那我们直接就进入到编译的环节吧。
出错了,从错误提示上看错误的地方在第 4 行的 2238 列
这个地方报错了,对比源码的话, 会发现一个很奇怪的现象。我们可以看下源码 interface.ts
export type NamedStyles<T> = {
[P in keyof T]: ViewStyle | TextStyle | ImageStyle;
};
然而插桩后的代码结果是
[P inkeyofT]: ViewStyle|TextStyle|ImageStyle;
keyof 竟然跟前后的字符连接在了一起。
这是个很诡异的现象,重新检查了下其他插桩的文件,发现同样都有 keyof 的关键字的 都会出现这样子的问题。导致编译失败。
这个问题就比较棘手了,总不能插桩完后手动去修改 keyof 的。所以我们先得确定这个到底是什么原因,但是在确定什么原因前,可能还得先分析下这个问题到底是谁的问题,nyc? 还是 babel。
所以我们来做一个实验。进入到 nyc 的 instrumenter 的源码中,去掉插桩的逻辑,只保留 babel 的解析跟生成,去除掉中间的转换,其实就是相当于 babel 讲代码转换成 ast, 又转换回来了。我们来看看怎么验证这个。
const ast = parser.parse(code, {
allowReturnOutsideFunction: opts.autoWrap,
sourceType: opts.esModules ? 'module' : 'script',
plugins: opts.parserPlugins
});
const ee = (0, _visitor.default)(t, filename, {
coverageVariable: opts.coverageVariable,
coverageGlobalScope: opts.coverageGlobalScope,
coverageGlobalScopeFunc: opts.coverageGlobalScopeFunc,
ignoreClassMethods: opts.ignoreClassMethods,
inputSourceMap
});
let output = {};
const visitor = {
Program: {
enter: ee.enter,
exit(path) {
output = ee.exit(path);
}
}
};
<!--(0, _traverse.default)(ast, visitor);-->
const generateOptions = {
compact: opts.compact,
comments: opts.preserveComments,
sourceMaps: opts.produceSourceMap,
sourceFileName: filename
};
const codeMap = (0, _generator.default)(ast, generateOptions, code);
如上所示 我们注释掉 travere 的逻辑,再观察下这个时候的 codeMap 的结果是如何的。
从这里基本能够确定出来 babel 的问题的可能性就非常大了。但是具体是 babel 的什么问题呢?
这里我们可能需要一个比较纯净的环境来验证下 babel 的问题。如下:
let ast = require("@babel/parser").parse("type NamedStyles<T> = { [P in keyof T]: ViewStyle | TextStyle | ImageStyle };", {
// parse in strict mode and allow module declarations
sourceType: "module",
plugins: [
// enable jsx and flow syntax
"jsx",
"typescript"
]
});
var generator = require("@babel/generator")
const output = generator.default(ast, { /* options */})
console.log(output)
我们直接通过 babel api 的方式 parse 后,直接 generator 的这个 demo 来看看效果, 如果对于 babel 的一些 api 不了解的话 强烈推荐去看看
babel-handbook。
结果。。
是正确的。 那问题出在哪里呢? 我们再仔细看下我们这个 demo 跟实际 instrumenter 的差别。 在 demo 里面我们的 options 实际上是空的,但是 instrumenter 的 options 实际上是
const generateOptions = {
compact: opts.compact,
comments: opts.preserveComments,
sourceMaps: opts.produceSourceMap,
sourceFileName: filename
};
我们关注下 compact 这个参数,这个从字面看应该是配置代码转换后是否压缩的意思。官方的说明是
All optional newlines and whitespace will be omitted when generating code in compact mode.
所以我们再尝试修改下我们的 demo 将其中的 options 做一个赋值改成 true
let ast = require("@babel/parser").parse("type NamedStyles<T> = { [P in keyof T]: ViewStyle | TextStyle | ImageStyle };", {
// parse in strict mode and allow module declarations
sourceType: "module",
plugins: [
// enable jsx and flow syntax
"jsx",
"typescript"
]
});
var generator = require("@babel/generator")
const output = generator.default(ast, {compact: true})
console.log(output)
再来看下结果:
问题真的出现了。 那是不是问题跟到这里就结束了呢,其实还没有,我们现在只是分析出来了,babel 有问题,而且问题的地方应该就是开启 compact 后,generator 的生成逻辑出现问题了,但是我们还是具体再确认下。
我们尝试断点调试跟下 generator 的代码。
真正的逻辑就是在这个地方了,左侧我们能看到当前的转换后的 buf 的列表,目前来看一切正常,现在问题就是在解析 typeof 这个关键字的时候了。我们看下 token 的逻辑
token(str) {
if (str === "--" && this.endsWith("!") || str[0] === "+" && this.endsWith("+") || str[0] === "-" && this.endsWith("-") || str[0] === "." && this._endsWithInteger ) {
this._space();
}
this._maybeAddAuxComment();
this._append(str);
}
我们发现 我们传入的 keyof 根本进入不到this._space()
的逻辑中,所以这里就会注定我们的 in 跟 typeof 就会连接在一起了。
这个就是根本的原因了。
既然问题发现了 那就提一个 issue 给到 babel 官方吧
babel parse error with the typescript when use the keyof and enable the compact opts
英文水平有限,只能那么描述了。没想到问题一天左右就得到解决了。真的很速度。
所以整个问题就这么被解决了。