1.0.0 • Published 10 months ago

rollup-plugin-consolelogplus v1.0.0

Weekly downloads
-
License
ISC
Repository
-
Last release
10 months ago

使用方式

注册插件

注册插件之后就会自动改造 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。image-20230608151658345

思路

获取精简版文件地址

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 阶段

  1. 判断到 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 };
    }
  };
}