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");// 解析代码生成 ASTconst 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",// 转换代码的 hookasync 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));});// 提供接口给一个路径跳转到 vscodeserver.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 来进行我们数据处理
// 转换代码的 hookasync 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 实现事件监听和命令执行
- 监听 快捷键 option + command 开启我们的选择模式 并调用接口获取映射关系
- 监听 鼠标移动 获取当前元素宽、高设置给这个 createSelector 的样式 让他展示出来
- 监听 鼠标点击事件 如果选择模式开启了 切 选中元素 获取这个元素的 data-path 属性然后根据映射关系调用 vscode 跳转接口 跳转到对应的代码即可