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 实现事件监听和命令执行
- 监听 快捷键 option + command 开启我们的选择模式 并调用接口获取映射关系
- 监听 鼠标移动 获取当前元素宽、高设置给这个 createSelector 的样式 让他展示出来
- 监听 鼠标点击事件 如果选择模式开启了 切 选中元素 获取这个元素的 data-path 属性然后根据映射关系调用 vscode 跳转接口 跳转到对应的代码即可