之前前端代码覆盖率一直用的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来看看效果。
结果。。
是正确的。 那问题出在哪里呢? 我们再仔细看下我们这个demo跟实际instrumenter的差别。 在demo里面我们的options实际上是空的,但是instrumenter的options实际上是
const generateOptions = {
compact: opts.compact,
comments: opts.preserveComments,
sourceMaps: opts.produceSourceMap,
sourceFileName: filename
};
我们关注下compact这个参数,这个从字面看应该是配置代码转换后是否压缩的意思。
所以我们再尝试修改下我们的demo将其中的options做一个赋值
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/generator parse error with the typescript when use the keyof and enable the compact opts
英文水平有限,只能那么描述了。没想到问题一天左右就得到解决了。真的很速度。