rollup-plugin-consolelogplus
v1.0.0
Published
自动改造 console.log 打印出该 console 所处的文件和代码在原文件(未经过任何编译)中所在行数,如果是变量的话还会加上变量名。兼容rollup和vite。
Downloads
3
Maintainers
Readme
使用方式
注册插件
注册插件之后就会自动改造 console.log 打印出该 console 所处的文件和代码在原文件(未经过任何编译)中所在行数,如果是变量的话还会加上变量名。
vite.config.ts
export default defineConfig({
plugins: [vitePluginConsolelogplus()]
});
配置
interface VitePluginConsolelogplusOptions {
preTip: string; //打印的前缀提示,这样方便快速找到log
splitBy: string; // 每个参数之间加个分隔符
endTip: boolean; //是否打印后缀提示,这样方便快速找到log
}
跳过
在console.log();
分号前加上注释:/*@vite-plugin-consolelogplus-skip*/
console.log() /*@vite-plugin-consolelogplus-skip*/;
用途
介绍
注册插件之后就会自动改造 console.log 打印出该 console 所处的文件和代码在原文件(未经过任何编译)中所在行数,如果是变量的话还会加上变量名。
场景
真机调试和生产环境下看代码在编辑器的位置(未经任何编译之前)。
比如天天领现金的 console 就比较难看 😅,而且这是个两年的项目且我是接手的角色,里面有不少 console,我不可能一个一个去处理这些 console,但是不处理确实影响了我用 console 去看一些问题,所以就用插件去处理清晰每个 console.log。
思路
获取精简版文件地址
const filePath = id.replace(process.cwd(), ''); //获取精简版的文件路径
如果是 vue 文件
先用@vue/compiler-sfc
去解析,然后提取出里面的 script 部分给 babel 去解析(这会导致一个问题:如果解析的 script 标签 前写了其他东西,那么行数就会不正确),如果 setup 语法和 script 同时存在,那我只解析 setup 语法里面的。
let toAstCode = ''; // 要解析成ast的代码
let vueSource = ''; // 记录vue文件里的源码,最后要返回出去给@vitejs/plugin-vue插件使用
const isVue = id.endsWith('.vue');
if (isVue) {
const { descriptor } = vueParse(code, {
filename: id,
sourceMap: false
});
if (descriptor) {
// 有setup语法的时候就只解析setup语法
toAstCode = descriptor.scriptSetup?.content || descriptor.script?.content;
vueSource = descriptor.source;
}
} else {
toAstCode = code;
}
遍历处理完代码之后,应该将 vue 的模版返回出去而不只是 script 里的代码(因为后面还要给@vitejs/plugin-vue
插件处理),所以要将改过的 script 代码在 vue 模版里替换然后返回替换后的 vue 模版
const { code: generatedCode, map } = generate(ast);
let resultCode = generatedCode;
if (isVue) {
resultCode = vueSource.replace(toAstCode, generatedCode);
}
return { code: resultCode, map };
遍历 ast 阶段
- 判断到 console.log 的 ast
if (
calleeCode.type === 'MemberExpression' &&
calleeCode.object.name === 'console' &&
calleeCode.property.name === 'log'
)
2.如果 用户通过注释的形式表示这个 console.log 不需要处理则跳过这个 console.log
const { trailingComments } = path.node;
const shouldSkip = (trailingComments || []).some((item) => {
return item.type === 'CommentBlock' && item.value === SKIP_KEY;
});
if (shouldSkip) return;
3.拿到 console.log 的 arguments,也就是 log 的参数。
const nodeArguments = path.node.arguments;
4.遍历 path.node.arguments 每个参数
字面量的,则无须添加变量名
变量的,添加变量名前缀,如
a =
根据传入的分隔符插入到原始参数的后面
5.拿到 console.log 的开始行数,创建一个包含行数的 StringLiteral,同时加上 preTip,比如上面的 🚀🚀🚀,然后 unshift,放在第一个参数的位置。
6.拿到 console.log 的结束行数,过程跟第 5 点类似,通过 push 放到最后一个参数的位置
存在的问题&todo
- [ ] 如果解析的 script 标签 前写了其他东西,那么行数就会不正确(因为我是取出 vue 里面的 script 部分给 babel 去解析)
源码
import { parse as vueParse } from '@vue/compiler-sfc';
import { parse } from '@babel/parser';
import * as t from '@babel/types';
import _traverse from '@babel/traverse';
import _generate from '@babel/generator';
const traverse = _traverse.default;
const generate = _generate.default; //根据ast节点生成代码字符串
interface VitePluginConsolelogplusOptions {
preTip: string; //打印的前缀提示,这样方便快速找到log
splitBy: string; // 每个参数之间加个分隔符
endTip: boolean; //是否打印后缀提示,这样方便快速找到log
}
const DEFAULT_PRE_TIP = '🚀🚀🚀';
const DEFAULT_SPLIT_BY = ';';
const SKIP_KEY = '@vite-plugin-consolelogplus-skip';
export default function vitePluginConsolelogplus(
opts: VitePluginConsolelogplusOptions = {
preTip: DEFAULT_PRE_TIP,
splitBy: DEFAULT_SPLIT_BY,
endTip: false
}
) {
const splitNode = t.stringLiteral(opts.splitBy);
return {
name: 'vite-plugin-consolelogplus',
enforce: 'pre' as const, //在esbuild执行之前
transform(code, id) {
if (/(node_modules)/.test(id)) return null; //过滤node_modules
const filePath = id.replace(process.cwd(), ''); //获取精简版的文件路径
let toAstCode = ''; // 要解析成ast的代码
let vueSource = ''; // 记录vue文件里的源码(@vue/compiler-sfc的parse之后能拿到),最后要返回出去给@vitejs/plugin-vue插件使用
const isVue = id.endsWith('.vue');
if (isVue) {
const { descriptor } = vueParse(code, {
filename: id,
sourceMap: false
});
if (descriptor) {
// 有setup语法的时候就只解析setup语法
toAstCode =
descriptor.scriptSetup?.content || descriptor.script?.content;
vueSource = descriptor.source;
}
} else {
toAstCode = code;
}
const ast = parse(toAstCode, {
sourceType: 'module',
plugins: ['typescript', 'jsx'] // 若要处理 TypeScript或jsx 代码,请启用插件
});
traverse(ast, {
CallExpression(path) {
// const calleeCode = generate(path.node.callee).code;
const calleeCode = path.node.callee;
if (
calleeCode.type === 'MemberExpression' &&
calleeCode.object.name === 'console' &&
calleeCode.property.name === 'log'
) {
// add comment to skip if enter next time
const { trailingComments } = path.node;
const shouldSkip = (trailingComments || []).some((item) => {
return item.type === 'CommentBlock' && item.value === SKIP_KEY;
});
if (shouldSkip) return;
// t.addComment(path.node, 'trailing', SKIP_KEY);
const nodeArguments = path.node.arguments;
const newNodeArguments = [...nodeArguments];
for (let i = 0, j = 0; i < nodeArguments.length; i++, j++) {
//i 遍历原数组,j遍历新数组
const argument = nodeArguments[i];
if (!t.isLiteral(argument)) {
if (t.isIdentifier(argument) && argument.name === 'undefined') {
//特殊case:console.log(undefined)的时候也不添加变量名(变量名会是‘undefined’)
newNodeArguments.splice(j + 1, 0, splitNode);
j++;
continue;
}
const node = t.stringLiteral(`${generate(argument).code} =`);
newNodeArguments.splice(j, 0, node);
j++;
newNodeArguments.splice(j + 1, 0, splitNode);
j++;
} else {
newNodeArguments.splice(j + 1, 0, splitNode);
j++;
}
}
// the last needn't split
// if (newNodeArguments[newNodeArguments.length - 1] === splitNode)
// newNodeArguments.pop();
const { loc } = path.node;
if (loc) {
const startLine = loc.start.line;
const startLineTipNode = t.stringLiteral(
`${opts.preTip}${filePath}${opts.preTip}line of ${startLine} :\n`
);
newNodeArguments.unshift(startLineTipNode);
if (opts.endTip) {
const endLine = loc.end.line;
const endLineTipNode = t.stringLiteral(
`\n${opts.preTip}line of ${endLine}:\n`
);
newNodeArguments.push(endLineTipNode);
}
}
path.node.arguments = newNodeArguments;
}
}
});
const { code: generatedCode, map } = generate(ast);
let resultCode = generatedCode;
if (isVue) {
resultCode = vueSource.replace(toAstCode, generatedCode);
}
return { code: resultCode, map };
}
};
}