vite_react 插件 find_code 最终版本

发布于:2025-09-06 ⋅ 阅读:(23) ⋅ 点赞:(0)

vite_react 插件 find_code 最终版本

当初在开发一个大型项目的时候,第一次接触 vite 构建,由于系统功能很庞大,在问题排查上和模块开发上比较耗时,然后就开始找解决方案,find-code 插件方案就这样实现出来了,当时觉得很好使,开发也很方便,于是自己开始琢磨自己开发一下整个流程 现如今也是零碎花费了两天时间做出了初版本的 find_code 插件

源码如下

// index.ts
import fs from "fs/promises";
import parser from "@babel/parser";
import traverse from "@babel/traverse";
import generate from "@babel/generator";
export const processFile = async (filePath: string, filePathIndexMap: any) => {
  try {
    // 读取文件内容
    const code = await fs.readFile(filePath, "utf8");
    // 解析代码生成 AST
    const ast = parser.parse(code, {
      sourceType: "module",
      plugins: ["jsx", "typescript"],
    });
    // 遍历 AST
    (traverse as any).default(ast, {
      JSXOpeningElement(path: any) {
        const line = path?.node?.loc?.start?.line;
        const value = `${filePath}:${line}`;
        const index = `${Object.keys(filePathIndexMap)?.length || 0}`;
        filePathIndexMap[index] = value;
        const pathAttribute = {
          type: "JSXAttribute",
          name: { type: "JSXIdentifier", name: "data-path" },
          value: {
            type: "StringLiteral",
            value: index,
          },
        };

        // 检查是否已经存在 path 属性,如果不存在则添加
        const existingPathAttribute = path.node.attributes.find((attr: any) => {
          return (
            attr?.name &&
            attr?.name.type === "JSXIdentifier" &&
            attr?.name.name === "data-path"
          );
        });

        if (!existingPathAttribute) {
          path.node.attributes.push(pathAttribute);
        }
      },
    });

    // 生成新代码,设置 retainLines 为 true 避免生成不必要的转义序列
    const { code: newCode } = (generate as any).default(ast, {
      retainLines: true,
      jsescOption: {
        minimal: true,
      },
    });
    return newCode;
  } catch (error) {
    console.error("处理文件时出错:", error);
  }
};

// vite-plugin-react-line-column.ts
import { createFilter } from "@rollup/pluginutils";
import { execSync } from "child_process";
import type { Plugin } from "vite";
import { processFile } from "./index";
import { parse } from "url";

const vitePluginReactLineColumn = (): Plugin => {
  const filePathIndexMap = {} as any;
  return {
    // 定义插件名称
    name: "vite-plugin-react-line-column",
    // 设置插件执行顺序为 'post',在其他插件之后执行
    enforce: "pre",
    // 仅在开发环境执行
    apply: "serve",

    // 转换代码的 hook
    async transform(code, id) {
      const filter = createFilter(/\.(js|jsx|ts|tsx)$/);
      if (!filter(id)) {
        return null;
      }
      const transformedCode = (await processFile(id, filePathIndexMap)) as any;
      return {
        code: transformedCode,
        map: null,
      };
    },
    async configureServer(server) {
      // 提供接口获取文件路径和索引的映射
      server.middlewares.use("/getPathIndexMap", (req, res) => {
        res.setHeader("Content-Type", "application/json");
        res.end(JSON.stringify(filePathIndexMap));
      });

      // 提供接口给一个路径跳转到 vscode
      server.middlewares.use("/jumpToVscode", (req, res) => {
        const query = parse(req?.url as string, true).query;
        const filePath = query.path;
        console.log(filePath, "filePath");
        if (!filePath || filePath == "undefined") {
          res.statusCode = 400;
          return res.end(
            JSON.stringify({ success: false, message: "缺少路径参数" })
          );
        }
        try {
          // 构建打开文件的命令
          const command = `code -g "${filePath}"`;
          // 同步执行命令
          execSync(command);
          res.setHeader("Content-Type", "application/json");
          res.end(JSON.stringify({ success: true }));
        } catch (error) {
          res.statusCode = 500;
          res.end(JSON.stringify({ success: false, message: "打开文件失败" }));
        }
      });
    },
  };
};

export default vitePluginReactLineColumn;

// 创建选择框
function createSelector() {
  const selector = document.createElement("div");
  selector.style.cssText = `
              position: fixed;
              border: 2px solid #007AFF;
              background: rgba(0, 122, 255, 0.1);
              pointer-events: none;
              z-index: 999999;
              display: none;
            `;
  document.body.appendChild(selector);
  return selector;
}

// 初始化选择器
const selector = createSelector();
let isSelecting = false;
let selectedElement = null;
let pathIndexMap = {};

const init = async () => {
  const response = await fetch("/getPathIndexMap");
  pathIndexMap = await response.json();
};

/* 根据当前元素递归查找 他的parentNode 是否有 data-path 没有就继续 直到 查到 body 标签结束 */
function findParentDataPath(element) {
  if (!element) return null;
  if (element.nodeType !== 1 || element.tagName == "body") return null; // 确保是元素节点
  if (element.hasAttribute("data-path")) {
    return element.getAttribute("data-path");
  }
  return findParentDataPath(element.parentNode);
}

document.addEventListener("click", (e) => {
  if (isSelecting && selectedElement) {
    console.log("[VSCode跳转插件] 回车键触发跳转");
    const dataIndex = selectedElement.getAttribute("data-path");
    const vscodePath = pathIndexMap[dataIndex];
    if (vscodePath) {
      fetch(`/jumpToVscode?path=${vscodePath}`);
    } else {
      /* 如果没有vscodePath 即没有找到data-path属性 */
      const dataIndex = findParentDataPath(selectedElement);
      const vscodePath = pathIndexMap[dataIndex];
      if (vscodePath) {
        fetch(`/jumpToVscode?path=${vscodePath}`);
      }
    }
    console.log("[VSCode跳转插件] vscodePath", vscodePath);
    isSelecting = false;
    selector.style.display = "none";
    selectedElement = null;
  }
});
// 监听快捷键
document.addEventListener("keydown", (e) => {
  if (e.altKey && e.metaKey) {
    console.log("[VSCode跳转插件] 选择模式已激活");
    isSelecting = true;
    selector.style.display = "block";
    document.body.style.cursor = "pointer";
    init();
  }
  // 添加回车键触发
  if (e.key === "Enter" && isSelecting && selectedElement) {
    console.log("[VSCode跳转插件] 回车键触发跳转");
    const dataIndex = selectedElement.getAttribute("data-path");
    const vscodePath = pathIndexMap[dataIndex];
    if (vscodePath) {
      fetch(`/jumpToVscode?path=${vscodePath}`);
    }
    console.log("[VSCode跳转插件] vscodePath", vscodePath);
    isSelecting = false;
    selector.style.display = "none";
    selectedElement = null;
  }
});

document.addEventListener("keyup", (e) => {
  if (!e.altKey && !e.metaKey) {
    console.log("[VSCode跳转插件] 选择模式已关闭");
    isSelecting = false;
    selector.style.display = "none";
    selectedElement = null;
  }
});

// 监听鼠标移动
document.addEventListener("mousemove", (e) => {
  if (!isSelecting) return;
  const element = document.elementFromPoint(e.clientX, e.clientY);
  if (element && element !== selectedElement) {
    selectedElement = element;
    const rect = element.getBoundingClientRect();
    selector.style.left = rect.left + "px";
    selector.style.top = rect.top + "px";
    selector.style.width = rect.width + "px";
    selector.style.height = rect.height + "px";

    console.log("[VSCode跳转插件] 当前选中元素:", element);
  }
});
// package.json 对应版本
{
  "name": "vite",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "1": "node ./node/parser.js",
    "2": "node ./node/index.ts",
    "dev": "vite",
    "build": "tsc -b && vite build",
    "lint": "eslint .",
    "preview": "vite preview"
  },
  "dependencies": {
    "@babel/traverse": "^7.28.3",
    "@types/antd": "^0.12.32",
    "antd": "^5.27.2",
    "fs": "^0.0.1-security",
    "path": "^0.12.7",
    "react": "^19.1.1",
    "react-dom": "^19.1.1",
    "url": "^0.11.4"
  },
  "devDependencies": {
    "@babel/generator": "^7.28.3",
    "@babel/parser": "^7.28.3",
    "@eslint/js": "^9.33.0",
    "@rollup/pluginutils": "^5.2.0",
    "@types/node": "^24.3.0",
    "@types/react": "^19.1.10",
    "@types/react-dom": "^19.1.7",
    "@vitejs/plugin-react": "^5.0.0",
    "eslint": "^9.33.0",
    "eslint-plugin-react-hooks": "^5.2.0",
    "eslint-plugin-react-refresh": "^0.4.20",
    "globals": "^16.3.0",
    "typescript": "~5.8.3",
    "typescript-eslint": "^8.39.1",
    "vite": "^7.1.2"
  }
}
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import vitePluginReactLineColumn from "./plugin/vite-plugin-react-line-column.ts";
export default defineConfig({
  plugins: [react(), vitePluginReactLineColumn()],
});

实现思路

1. 首先我们可以先练习 怎么样将我们的 jsx 代码插入我们想要的一些属性进去
// 1. 解析我们的代码生成 AST
const ast = parser.parse(code, {
  sourceType: "module",
  plugins: ["jsx"],
});
 //  遍历 AST 有一个属性 JSXOpeningElement 就是我们的 jsx 标签

     (traverse as any).default(ast, {
      JSXOpeningElement(path: any) {
        const line = path?.node?.loc?.start?.line;
        const value = `${filePath}:${line}`;
        const index = `${Object.keys(filePathIndexMap)?.length || 0}`;
        filePathIndexMap[index] = value;
        const pathAttribute = {
          type: "JSXAttribute",
          name: { type: "JSXIdentifier", name: "data-path" },
          value: {
            type: "StringLiteral",
            value: index,
          },
        };

        // 检查是否已经存在 path 属性,如果不存在则添加
        const existingPathAttribute = path.node.attributes.find((attr: any) => {
          return (
            attr?.name &&
            attr?.name.type === "JSXIdentifier" &&
            attr?.name.name === "data-path"
          );
        });

        if (!existingPathAttribute) {
          path.node.attributes.push(pathAttribute);
        }
      },
    });
//  生成的新代码 再转回去
    // 生成新代码,设置 retainLines 为 true 避免生成不必要的转义序列
    const { code: newCode } = (generate as any).default(ast, {
      retainLines: true,
      jsescOption: {
        minimal: true,
      },
    });

generate 函数中,我们传入了一个配置对象,其中:
retainLines: true
尽量保留原始代码的行号和格式,减少不必要的换行和格式化。
jsescOption: { minimal: true }
jsesc 是 @babel/generator 内部用于处理字符串转义的工具,
minimal: true 表示只对必要的字符进行转义,避免生成不必要的 Unicode 转义序列。
通过这些配置,可以确保生成的代码中不会出现乱码的 Unicode 转义序列。
请确保已经安装了所需的 Babel 相关依赖,如果没有安装,可以使用以下命令进行安装:

 npm install @babel/parser @babel/traverse @babel/generator
2. 然后我们使用 vite 插件 hook 来进行我们数据处理
    // 转换代码的 hook
     async transform(code, id) {
      const filter = createFilter(/\.(js|jsx|ts|tsx)$/);
      if (!filter(id)) {
        return null;
      }
      const transformedCode = (await processFile(id, filePathIndexMap)) as any;
      return {
        code: transformedCode,
        map: null,
      };
    },

这里可以进行优化,就是已经获取到 code 了 就不需要将这个 path(id)传递给这个函数,可以直接优化这个函数直接接受 code 就行,不需要再读取文件

3. 使用 vite 插件 hook 来提供接口
    1、 收集所有索引和路径的映射接口

    2、 提供接口给一个路径跳转到 vscode
4. 实现 js 代码注入

使用纯 js 实现事件监听和命令执行

  1. 监听 快捷键 option + command 开启我们的选择模式 并调用接口获取映射关系
  2. 监听 鼠标移动 获取当前元素宽、高设置给这个 createSelector 的样式 让他展示出来
  3. 监听 鼠标点击事件 如果选择模式开启了 切 选中元素 获取这个元素的 data-path 属性然后根据映射关系调用 vscode 跳转接口 跳转到对应的代码即可
优化: 4. 由于antd组件只能给最外层添加data-path属性,所以当选择一些内部元素的时候,这边进行递归找寻他的上层元素是否含有data-path属性,直到找到body结束 待优化: 5. 影响到了打包 6. 如果是公共组件 需要换一个颜色进行区分,因为公共组件就算跳到对应位置也很难定位排查我们的问题

请添加图片描述


网站公告

今日签到

点亮在社区的每一天
去签到