用 AI 工具写个 webpack 插件吧

189 阅读2分钟

伴随 AI 的崛起以及 vibe coding 的流行。AI 开发者工具是越来越多了。对于我的使用感受而言,AI 编辑器相比搜索引擎来说更加精准。

刚好呢,最近在迁移代码,业务逻辑比较麻烦,然后函数链调用比较深,且之前也没有写参数类型,基本上算是摸着石头过河了。然后几乎每个调用都打上了 log 去看输入。于是呢,我就想着有点麻烦了,能不能有个插件来自动添加log。基于前几天我用 AI 去写 next 项目的体验,我就决定用 AI 试着写一写了。这也是我为啥不直接搜一个插件的原因。。。。

那下面就直接开始吧

开始

话不多说,直接开问吧

  1. 直接发问

Q: 写一个 webpack 插件,实现函数的名称以及参数打印,且只针对有添加 @log 注释的函数生效。

A:

const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const types = require('@babel/types');
const generate = require('@babel/generator').default;

class FunctionLoggerPlugin {
  apply(compiler) {
    compiler.hooks.emit.tapAsync('FunctionLoggerPlugin', (compilation, callback) => {
      const assets = compilation.assets;

      // Process each asset
      Object.keys(assets).forEach(filename => {
        if (!filename.endsWith('.js')) return;

        let source = assets[filename].source();
        const ast = parser.parse(source, {
          sourceType: 'module',
          plugins: ['jsx'],
          sourceFilename: filename
        });

        // Track functions that need logging
        const functionsToLog = new Set();

        // First pass: identify functions with @log comment
        traverse(ast, {
          Function(path) {
            const comments = path.node.leadingComments || [];
            if (comments.some(comment => comment.value.trim() === '@log')) {
              functionsToLog.add(path.node);
            }
          }
        });

        // Second pass: inject logging code
        traverse(ast, {
          Function(path) {
            if (!functionsToLog.has(path.node)) return;

            const functionName = path.node.id ? path.node.id.name : 'anonymous';
            const params = path.node.params;

            // Create logging statement
            const logStatement = types.expressionStatement(
              types.callExpression(
                types.memberExpression(
                  types.identifier('console'),
                  types.identifier('log')
                ),
                [
                  types.stringLiteral(`Function ${functionName} called with params:`),
                  ...params.map(param => param.name ? types.identifier(param.name) : param)
                ]
              )
            );

            // Insert at the beginning of the function body
            if (path.node.body.type === 'BlockStatement') {
              path.node.body.body.unshift(logStatement);
            }
          }
        });

        // Generate the transformed code
        const output = generate(ast, { sourceMaps: true }, source);
        const transformedCode = output.code;

        // Update the asset with transformed code
        compilation.assets[filename] = {
          source: () => transformedCode,
          size: () => transformedCode.length
        };
      });

      callback();
    });
  }
}

module.exports = FunctionLoggerPlugin;

好的,答案出来了,那就开用吧。我默默打开了那尘封已久的 cra 创建的 react 项目。诶,没有地方改 webpack 配置呢。那就 eject 一下吧。然后呢,link 一下,OK 准备工作完成,npm start 启动!!

又双叒叕报错了

  1. 为什么在类组件中没有函数名称/参数打印呢?
  • 本地代码
class CompLife extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      msg: "hello world",
    };
    console.log("constructor构造函数");
    this.callpluginLog.bind(this);
  }
  componentWillMount() {
    console.log("CompontWillMount: 组件将要渲染");
  }
  componentDidMount() {
    console.log("CompontDidMount:组件渲染完毕");
  }
  componentWillReceiveProps() {
    console.log("CompontWillReceiveProps:组件将要接受props数据");
  }
  componentWillUpdate() {
    console.log("组件接受到新的state或者props");
  }
  componentDidUpdate() {
    console.log("组件已经更新");
  }
  componentWillUnmount() {
    console.log("组件将要卸载");
  }
  render() {
    console.log("render渲染函数");
    return (
      <div>
        <h1>{this.state.msg}</h1>
        <button onClick={this.passMsg}>改变标题</button>
      </div>
    );
  }
  // @log
  callpluginLog() {}

  // 按钮点击事件
  passMsg = () => {
    this.callpluginLog("hello");
    this.setState({
      msg: "我变了!",
    });
  };
}

ReactDOM.render(<CompLife></CompLife>, document.querySelector("#root"));

ZRGJ4{_M8FDHH_8GW4H4_CB.png

接着问 AI 吧!

Q: 在 react 类组件中,打印的方法名称以及参数是错误的,是因为类组件中会 使用 bind 的问题吗

A: Yes, the issue is related to how class methods are bound in React components and how the function names are transformed. Let me modify the plugin to handle class methods correctly:

The key changes made to handle class methods correctly:

  1. Added support for ClassMethod and ClassProperty in the visitor pattern
  2. Added 'classProperties' plugin to the parser options
  3. Improved function name detection for class methods and arrow functions
  4. Enhanced parameter handling for different types of parameters (object destructuring, array destructuring)
  5. Added proper handling of class method bodies and arrow function bodies

Now the plugin should correctly log function names and parameters for both regular functions and class methods, including arrow functions defined as class properties.

这里一看,主要改了下遍历 AST 的两处

  // First pass: identify functions with @log comment
  traverse(ast, {
    "ClassMethod|ClassProperty|Function"(path) {
      const comments = path.node.leadingComments || [];
      if (comments.some((comment) => comment.value.trim() === "@log")) {
        functionsToLog.add(path.node);
      }
    },
  });

  // Second pass: inject logging code
  traverse(ast, {
    "ClassMethod|ClassProperty|Function"(path) {
      if (!functionsToLog.has(path.node)) return;

      let functionName;
      if (path.node.type === "ClassMethod") {
        functionName = types.stringLiteral(path.node.key.name);
      } else if (
        path.node.type === "ClassProperty" &&
        path.node.value &&
        path.node.value.type === "ArrowFunction"
      ) {
        functionName = types.stringLiteral(path.node.key.name);
      } else {
        functionName = types.stringLiteral(
          path.node.id ? path.node.id.name : "anonymous"
        );
      }

      const params = path.node.params || [];
      const paramNames = params.map((param) => {
        if (param.type === "Identifier") {
          return types.identifier(param.name);
        } else if (param.type === "ObjectPattern") {
          return types.objectExpression(
            param.properties.map((prop) =>
              types.objectProperty(
                types.identifier(prop.key.name),
                types.identifier(prop.value.name || prop.key.name),
                false,
                prop.key.name === prop.value.name
              )
            )
          );
        } else if (param.type === "ArrayPattern") {
          return types.arrayExpression(
            param.elements.map((element) =>
              element
                ? types.identifier(element.name)
                : types.nullLiteral()
            )
          );
        }
        return types.identifier("undefined");
      });

      // Create logging statement
      const logStatement = types.expressionStatement(
        types.callExpression(
          types.memberExpression(
            types.identifier("console"),
            types.identifier("log")
          ),
          [
            types.stringLiteral("Function "),
            functionName,
            types.stringLiteral(" called with args:"),
            ...paramNames,
          ]
        )
      );

      // Insert at the beginning of the function body
      if (path.node.body && path.node.body.type === "BlockStatement") {
        path.node.body.body.unshift(logStatement);
      } else if (
        path.node.value &&
        path.node.value.body &&
        path.node.value.body.type === "BlockStatement"
      ) {
        path.node.value.body.body.unshift(logStatement);
      }
    },
  });
  1. 函数名称出来了,为什么没有参数呢?而且为啥还打印了两次?

_3IDGGVG0K.png

继续问 AI 吧。。。猜想是不支持运行时的动态参数

Q: 在调用 类组件的方法时,有打印了函数名称,但是打印了两次,类组件中有用 bind 绑定方法。但是为啥还是没有打印参数,是因为函数在定义时,没有形参,而调用时传入了参数吗,怎样解决运行时传参,能否直接用 arguments

A: I understand the issue - the class methods are being logged twice due to binding, and the parameters aren't being logged correctly. Let's modify the plugin to handle runtime parameters using arguments and fix the double logging issue:

The key changes made to fix the issues:

  1. Replaced parameter-based logging with arguments object to capture runtime parameters
  2. Added special handling for class methods to prevent double logging
  3. Preserved this context in class methods using _originalThis
  4. Simplified the logging statement to use spread operator with arguments

Now the plugin should:

  • Log function calls only once
  • Correctly capture all runtime parameters using arguments
  • Maintain proper this binding in class methods
  • Show actual parameter values passed during function calls

同样,修改一下两处 traverse

  // First pass: identify functions with @log comment
  traverse(ast, {
    "ClassMethod|ClassProperty|Function"(path) {
      const comments = path.node.leadingComments || [];
      if (comments.some((comment) => comment.value.trim() === "@log")) {
        functionsToLog.add(path.node);
      }
    },
  });

  // Second pass: inject logging code
  traverse(ast, {
    "ClassMethod|ClassProperty|Function"(path) {
      if (!functionsToLog.has(path.node)) return;

      let functionName;
      if (path.node.type === "ClassMethod") {
        functionName = types.stringLiteral(path.node.key.name);
      } else if (
        path.node.type === "ClassProperty" &&
        path.node.value &&
        path.node.value.type === "ArrowFunction"
      ) {
        functionName = types.stringLiteral(path.node.key.name);
      } else {
        functionName = types.stringLiteral(
          path.node.id ? path.node.id.name : "anonymous"
        );
      }

      // Create logging statement using arguments object
      const logStatement = types.expressionStatement(
        types.callExpression(
          types.memberExpression(
            types.identifier("console"),
            types.identifier("log")
          ),
          [
            types.stringLiteral("Function "),
            functionName,
            types.stringLiteral(" called with args:"),
            types.spreadElement(types.identifier("arguments")),
          ]
        )
      );

      // Create a wrapper function to handle binding
      if (path.node.type === "ClassMethod") {
        const originalBody = path.node.body.body;
        path.node.body.body = [
          types.variableDeclaration("const", [
            types.variableDeclarator(
              types.identifier("_originalThis"),
              types.thisExpression()
            ),
          ]),
          logStatement,
          ...originalBody,
        ];
      } else {
        // Insert at the beginning of the function body
        if (
          path.node.body &&
          path.node.body.type === "BlockStatement"
        ) {
          path.node.body.body.unshift(logStatement);
        } else if (
          path.node.value &&
          path.node.value.body &&
          path.node.value.body.type === "BlockStatement"
        ) {
          path.node.value.body.body.unshift(logStatement);
        }
      }
    },
  });

运行一下呢。oh 寄了,报错了 Uncaught SyntaxError: Identifier '_originalThis' has already been declared

那回滚下,只修改 logStatement 的改动吧。参数有了,但还是打印了两次。继续问

_FGKWEQBDOYAIZ%4AH6.png

  1. 解决下打印两次

Q: 会出现 Uncaught SyntaxError: Identifier '_originalThis' has already been declared,打印两次是否是 包含了 ClassMethod 以及 ClassProperty 的问题

A: I understand the issue - we're getting a duplicate declaration error for _originalThis and double logging because we're matching both ClassMethod and ClassProperty. Let's fix this by improving our visitor pattern and removing the redundant variable declaration:

// First pass: identify functions with @log comment
  traverse(ast, {
    Function(path) {
      const comments = path.node.leadingComments || [];
      if (comments.some((comment) => comment.value.trim() === "@log")) {
        functionsToLog.add(path.node);
      }
    },
    ClassMethod(path) {
      const comments = path.node.leadingComments || [];
      if (comments.some((comment) => comment.value.trim() === "@log")) {
        functionsToLog.add(path.node);
      }
    },
  });

  // Second pass: inject logging code
  traverse(ast, {
    Function(path) {
      if (!functionsToLog.has(path.node)) return;

      const functionName = types.stringLiteral(
        path.node.id ? path.node.id.name : "anonymous"
      );

      // Create logging statement using arguments object
      const logStatement = types.expressionStatement(
        types.callExpression(
          types.memberExpression(
            types.identifier("console"),
            types.identifier("log")
          ),
          [
            types.stringLiteral("Function "),
            functionName,
            types.stringLiteral(" called with args:"),
            types.spreadElement(types.identifier("arguments")),
          ]
        )
      );

      // Insert at the beginning of the function body
      if (path.node.body && path.node.body.type === "BlockStatement") {
        path.node.body.body.unshift(logStatement);
      }
    },
    ClassMethod(path) {
      if (!functionsToLog.has(path.node)) return;

      const functionName = types.stringLiteral(path.node.key.name);

      // Create logging statement using arguments object
      const logStatement = types.expressionStatement(
        types.callExpression(
          types.memberExpression(
            types.identifier("console"),
            types.identifier("log")
          ),
          [
            types.stringLiteral("Method "),
            functionName,
            types.stringLiteral(" called with args:"),
            types.spreadElement(types.identifier("arguments")),
          ]
        )
      );

      // Insert at the beginning of the method body
      path.node.body.body.unshift(logStatement);
    },
  });

还是会有两次,啊哈只不过这两次有些许差异,函数获取的名称是空的。

R$AIC192CX.png

那这不简单了吗,可能是函数会包含属性,但是从 ast 获取名称的路径不对啊,那这次就不问 AI 了,自己改下吧😂. 从上面可见,类方法获取名称是根据 path.node.key.name,而函数是根据 path.node.id.name。所以多加几个判断应该行了吧。然后只保留函数的 traverse。Function 也确实包含了类属性方法。

const functionName = types.stringLiteral(
    path.node?.key?.name || path.node?.id?.name || "anonymous"
);

Function 也确实包含了类属性方法。

G~BNHCS2TKSYAW

可以了

J0R94

热更新以及其他类型参数也可以了,芜湖✈

_XQ1.png

其他

这里呢,是用的 webpack4 去做测试, webpack5 很大层度上应该会有一些兼容问题,因为我在使用 webpack5 构建时,会有一些 warning 跟错误吧。

总结

AI 真的是非常给力的好助手啊。基本上给定的代码方向是没有问题的。但可能一些细枝末节需要自己去调整。而它的知识广度是非常广的。从这个插件来讲,可以看出 AI 算是非常熟悉 babel 以及 webpack 的插件编写了,确实是比我自己还强😂。