vite-plugin-vue-template-inspector 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,40 @@
1
+ # vite-plugin-vue-template-inspector
2
+
3
+ Vite 开发插件:在页面上 **Alt + 左键** 点击 Vue 模板节点,弹出源码路径,可复制或在本地编辑器中打开。
4
+
5
+ 仅在 `vite serve` 下生效,不会进入生产构建。
6
+
7
+ ## 安装
8
+
9
+ ```bash
10
+ pnpm add -D vite-plugin-vue-template-inspector
11
+ ```
12
+
13
+ 本地未发布时,从业务项目指向这个目录:
14
+
15
+ ```bash
16
+ pnpm add -D ../vite-plugin-vue-template-inspector
17
+ ```
18
+
19
+ ## 使用
20
+
21
+ 插件需要放在 `@vitejs/plugin-vue` **之前**,才能在编译前给模板打上定位属性。
22
+
23
+ ```ts
24
+ import { defineConfig } from 'vite'
25
+ import vue from '@vitejs/plugin-vue'
26
+ import { vueTemplateInspector } from 'vite-plugin-vue-template-inspector'
27
+
28
+ export default defineConfig({
29
+ plugins: [vueTemplateInspector(), vue()],
30
+ })
31
+ ```
32
+
33
+ ## 本地调试
34
+
35
+ ```bash
36
+ pnpm install
37
+ pnpm dev
38
+ ```
39
+
40
+ 打开 playground 后,按住 Alt 点击页面上的元素即可验证注入、复制路径和打开编辑器。
package/dist/client.js ADDED
@@ -0,0 +1 @@
1
+ "use strict";(()=>{const y=window.__VUE_INSPECTOR_CONFIG__||{},i=y.ATTR||"data-v-inspector",v=y.REQ_PATH||"/__vue-template-inspector/open-in-editor",g=Symbol.for("vue-template-inspector.initialized");if(window[g])return;window[g]=!0;const x=new WeakMap;let n=null;const s=t=>{if(!(t instanceof Element))return;const e=t.getAttribute(i);e&&(x.set(t,e),t.removeAttribute(i))},A=t=>{t instanceof Element&&(s(t),t.querySelectorAll("["+i+"]").forEach(s))},P=t=>{let e=t instanceof Element?t:null;for(;e;){if(x.has(e)||e.hasAttribute(i))return e;e=e.parentElement}return null},_=t=>{const e=x.get(t)||t.getAttribute(i);return t.hasAttribute(i)&&s(t),e};document.querySelectorAll("["+i+"]").forEach(s),new MutationObserver(t=>t.forEach(e=>{e.type==="attributes"?s(e.target):e.addedNodes.forEach(A)})).observe(document.documentElement,{attributes:!0,attributeFilter:[i],childList:!0,subtree:!0});const l=()=>{n?.remove(),n=null},R=async t=>{try{return await navigator.clipboard.writeText(t),!0}catch{const e=document.createElement("textarea");e.value=t,e.style.cssText="position:fixed;left:-9999px;opacity:0",document.body.appendChild(e),e.select();const a=document.execCommand("copy");return e.remove(),a}},L=async(t,e,a)=>{const f=new URLSearchParams({file:t,line:e,column:a}),r=await fetch(v+"?"+f,{method:"POST"});if(!r.ok)throw new Error(await r.text())};function M(t,e,a){l();const f=t.match(/^(.*):(\d+):(\d+)$/),[,r,E,T]=f||[null,t,"-","-"];n=document.createElement("div"),n.style.cssText="position:fixed;z-index:2147483647;max-width:min(520px,calc(100vw - 24px));padding:12px 14px;border-radius:8px;background:#111827;color:#f9fafb;font:13px/1.5 ui-sans-serif,system-ui,sans-serif;box-shadow:0 10px 30px rgba(0,0,0,.35)";const p=document.createElement("div");p.textContent=r,p.title=r,p.style.cssText="word-break:break-all;margin-bottom:10px;color:#e5e7eb";const b=document.createElement("div");b.style.cssText="display:flex;gap:8px;justify-content:flex-end";const h=document.createElement("div");h.style.cssText="display:flex;align-items:center;gap:6px;margin-right:auto";for(const o of["\u884C "+E,"\u5217 "+T]){const d=document.createElement("span");d.textContent=o,d.style.cssText="padding:2px 7px;border:1px solid #4b5563;border-radius:4px;background:#1f2937;color:#d1d5db;font-size:12px;white-space:nowrap",h.append(d)}const w=(o,d)=>{const m=document.createElement("button");return m.type="button",m.textContent=o,m.style.cssText=`cursor:pointer;border:0;border-radius:6px;padding:6px 10px;background:${d};color:#fff`,m},u=w("\u590D\u5236\u8DEF\u5F84","#2563eb"),c=w("\u6253\u5F00\u7F16\u8F91\u5668","#059669"),k=w("\u5173\u95ED","#374151");u.onclick=async o=>{o.stopPropagation(),u.textContent=await R(r)?"\u5DF2\u590D\u5236":"\u590D\u5236\u5931\u8D25",setTimeout(()=>{u.textContent="\u590D\u5236\u8DEF\u5F84"},1200)},c.onclick=async o=>{o.stopPropagation(),c.disabled=!0;try{await L(r,E,T),c.textContent="\u5DF2\u6253\u5F00"}catch{c.textContent="\u6253\u5F00\u5931\u8D25"}setTimeout(()=>{c.textContent="\u6253\u5F00\u7F16\u8F91\u5668",c.disabled=!1},1200)},k.onclick=o=>{o.stopPropagation(),l()},b.append(h,u,c,k),n.append(p,b),document.body.appendChild(n);const C=n.getBoundingClientRect();n.style.left=`${Math.min(Math.max(8,e+8),window.innerWidth-C.width-8)}px`,n.style.top=`${Math.min(Math.max(8,a+8),window.innerHeight-C.height-8)}px`}window.addEventListener("click",t=>{if(t.button!==0||!t.altKey)return;const e=P(t.target),a=e&&_(e);a&&(t.preventDefault(),t.stopPropagation(),M(a,t.clientX,t.clientY))},!0),window.addEventListener("keydown",t=>{t.key==="Escape"&&l()}),window.addEventListener("click",t=>{n&&!t.altKey&&!n.contains(t.target)&&l()})})();
@@ -0,0 +1,5 @@
1
+ import { Plugin } from 'vite';
2
+
3
+ declare function vueTemplateInspector(): Plugin;
4
+
5
+ export { vueTemplateInspector };
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ import N from"fs";import a from"path";import C from"process";import{fileURLToPath as E}from"url";import{NodeTypes as f,parse as R}from"@vue/compiler-dom";import S from"launch-editor";import _ from"magic-string";import{parse as b}from"vue/compiler-sfc";var g="data-v-inspector",v="/__vue-template-inspector/open-in-editor",w=E(new URL("./client.js",import.meta.url));function d(n,e){for(let t of n)if(t.type===f.ELEMENT)e(t),d(t.children,e);else if(t.type===f.IF)for(let o of t.branches)d(o.children,e);else t.type===f.FOR&&d(t.children,e)}function I(n,e,t){let{descriptor:o}=b(n,{filename:e});if(!o.template)return null;let s=o.template,i=R(s.content),l=a.relative(t,e).replace(/\\/g,"/"),p=s.loc.start.line-1,c=s.loc.start.offset,m=new _(n),u=!1;return d(i.children,r=>{if(r.props.some(y=>y.type===f.ATTRIBUTE&&y.name===g))return;let h=r.loc.start.line+p,P=r.loc.start.column,$=c+r.loc.start.offset+1+r.tag.length;m.appendLeft($,` ${g}="${l}:${h}:${P}"`),u=!0}),u?{code:m.toString(),map:m.generateMap({source:e,includeContent:!0,hires:!0})}:null}function M(){let n=N.readFileSync(w,"utf8");return`<script>
2
+ window.__VUE_INSPECTOR_CONFIG__=${JSON.stringify({ATTR:g,REQ_PATH:v}).replace(/</g,"\\u003c")};
3
+ ${n};
4
+ </script>`}function W(){let n=C.cwd();return{name:"vue-template-inspector",apply:"serve",enforce:"pre",configResolved(e){n=e.root},configureServer(e){e.middlewares.use((t,o,s)=>{if(!t.url)return s();let i=new URL(t.url,"http://localhost");if(i.pathname!==v)return s();if(t.method!=="POST"){o.statusCode=405,o.end("Method Not Allowed");return}let l=i.searchParams.get("file");if(!l){o.statusCode=400,o.end("Missing file");return}let p=a.resolve(n,l),c=a.relative(n,p);if(c===".."||c.startsWith(`..${a.sep}`)||a.isAbsolute(c)){o.statusCode=403,o.end("Forbidden");return}let u=Math.max(1,Number.parseInt(i.searchParams.get("line")??"",10)||1),r=Math.max(1,Number.parseInt(i.searchParams.get("column")??"",10)||1);S(`${p}:${u}:${r}`,void 0,(T,h)=>{console.error(`[vue-template-inspector] \u65E0\u6CD5\u6253\u5F00 ${T}: ${h}`)}),o.statusCode=204,o.end()})},transform(e,t){let o=t.split("?")[0]??t;if(!(t.includes("node_modules")||!o.endsWith(".vue")))return I(e,o,n)||void 0},transformIndexHtml(e){return e.replace("</body>",`${M()}</body>`)}}}export{W as vueTemplateInspector};
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import fs from 'node:fs'\r\nimport path from 'node:path'\r\nimport process from 'node:process'\r\nimport { fileURLToPath } from 'node:url'\r\nimport { NodeTypes, parse as parseTemplate } from '@vue/compiler-dom'\r\nimport type { ElementNode, TemplateChildNode } from '@vue/compiler-dom'\r\nimport launchEditor from 'launch-editor'\r\nimport MagicString from 'magic-string'\r\nimport type { Plugin } from 'vite'\r\nimport { parse as parseSFC } from 'vue/compiler-sfc'\r\n\r\nconst ATTR = 'data-v-inspector'\r\nconst REQ_PATH = '/__vue-template-inspector/open-in-editor'\r\nconst clientScriptPath = fileURLToPath(new URL('./client.js', import.meta.url))\r\n\r\nfunction walkElements(nodes: TemplateChildNode[], visit: (el: ElementNode) => void) {\r\n for (const node of nodes) {\r\n if (node.type === NodeTypes.ELEMENT) {\r\n visit(node)\r\n walkElements(node.children, visit)\r\n } else if (node.type === NodeTypes.IF) {\r\n for (const branch of node.branches) walkElements(branch.children, visit)\r\n } else if (node.type === NodeTypes.FOR) {\r\n walkElements(node.children, visit)\r\n }\r\n }\r\n}\r\n\r\nfunction injectInspector(code: string, filename: string, root: string) {\r\n const { descriptor } = parseSFC(code, { filename })\r\n if (!descriptor.template) return null\r\n\r\n const template = descriptor.template\r\n const ast = parseTemplate(template.content)\r\n const rel = path.relative(root, filename).replace(/\\\\/g, '/')\r\n const lineOffset = template.loc.start.line - 1\r\n const templateOffset = template.loc.start.offset\r\n const magicString = new MagicString(code)\r\n let changed = false\r\n\r\n walkElements(ast.children, (el) => {\r\n const already = el.props.some(\r\n (prop) => prop.type === NodeTypes.ATTRIBUTE && prop.name === ATTR,\r\n )\r\n if (already) return\r\n const line = el.loc.start.line + lineOffset\r\n const column = el.loc.start.column\r\n const insertPosition = templateOffset + el.loc.start.offset + 1 + el.tag.length\r\n magicString.appendLeft(insertPosition, ` ${ATTR}=\"${rel}:${line}:${column}\"`)\r\n changed = true\r\n })\r\n\r\n if (!changed) return null\r\n return {\r\n code: magicString.toString(),\r\n map: magicString.generateMap({\r\n source: filename,\r\n includeContent: true,\r\n hires: true,\r\n }),\r\n }\r\n}\r\n\r\nfunction clientScript() {\r\n const clientCode = fs.readFileSync(clientScriptPath, 'utf8')\r\n const config = JSON.stringify({ ATTR, REQ_PATH }).replace(/</g, '\\\\u003c')\r\n return `<script>\r\n window.__VUE_INSPECTOR_CONFIG__=${config};\r\n ${clientCode};\r\n </script>`\r\n}\r\n\r\nexport function vueTemplateInspector(): Plugin {\r\n let root = process.cwd()\r\n\r\n return {\r\n name: 'vue-template-inspector',\r\n apply: 'serve',\r\n enforce: 'pre',\r\n configResolved(config) {\r\n root = config.root\r\n },\r\n configureServer(server) {\r\n server.middlewares.use((req, res, next) => {\r\n if (!req.url) return next()\r\n\r\n const url = new URL(req.url, 'http://localhost')\r\n if (url.pathname !== REQ_PATH) return next()\r\n\r\n if (req.method !== 'POST') {\r\n res.statusCode = 405\r\n res.end('Method Not Allowed')\r\n return\r\n }\r\n\r\n const file = url.searchParams.get('file')\r\n if (!file) {\r\n res.statusCode = 400\r\n res.end('Missing file')\r\n return\r\n }\r\n\r\n const absolutePath = path.resolve(root, file)\r\n const relativePath = path.relative(root, absolutePath)\r\n const outsideRoot =\r\n relativePath === '..' ||\r\n relativePath.startsWith(`..${path.sep}`) ||\r\n path.isAbsolute(relativePath)\r\n\r\n if (outsideRoot) {\r\n res.statusCode = 403\r\n res.end('Forbidden')\r\n return\r\n }\r\n\r\n const line = Math.max(1, Number.parseInt(url.searchParams.get('line') ?? '', 10) || 1)\r\n const column = Math.max(1, Number.parseInt(url.searchParams.get('column') ?? '', 10) || 1)\r\n\r\n launchEditor(`${absolutePath}:${line}:${column}`, undefined, (fileName, errorMessage) => {\r\n console.error(`[vue-template-inspector] 无法打开 ${fileName}: ${errorMessage}`)\r\n })\r\n res.statusCode = 204\r\n res.end()\r\n })\r\n },\r\n transform(code, id) {\r\n const filename = id.split('?')[0] ?? id\r\n if (id.includes('node_modules') || !filename.endsWith('.vue')) return\r\n return injectInspector(code, filename, root) || undefined\r\n },\r\n transformIndexHtml(html) {\r\n return html.replace('</body>', `${clientScript()}</body>`)\r\n },\r\n }\r\n}\r\n"],"mappings":"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","names":["fs","path","process","fileURLToPath","NodeTypes","parseTemplate","launchEditor","MagicString","parseSFC","ATTR","REQ_PATH","clientScriptPath","walkElements","nodes","visit","node","branch","injectInspector","code","filename","root","descriptor","template","ast","rel","lineOffset","templateOffset","magicString","changed","el","prop","line","column","insertPosition","clientScript","clientCode","vueTemplateInspector","config","server","req","res","next","url","file","absolutePath","relativePath","fileName","errorMessage","id","html"]}
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "vite-plugin-vue-template-inspector",
3
+ "version": "0.1.0",
4
+ "description": "Vite plugin: Alt+click Vue template elements to copy the source path or open it in your editor.",
5
+ "keywords": [
6
+ "vite",
7
+ "vite-plugin",
8
+ "vue",
9
+ "inspector",
10
+ "devtools"
11
+ ],
12
+ "license": "MIT",
13
+ "type": "module",
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "main": "./dist/index.js",
18
+ "module": "./dist/index.js",
19
+ "types": "./dist/index.d.ts",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "import": "./dist/index.js",
24
+ "default": "./dist/index.js"
25
+ }
26
+ },
27
+ "scripts": {
28
+ "dev": "vite --config playground/vite.config.ts",
29
+ "build": "tsup && node scripts/copy-client.mjs",
30
+ "prepublishOnly": "pnpm build"
31
+ },
32
+ "peerDependencies": {
33
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0",
34
+ "vue": "^3.3.0"
35
+ },
36
+ "dependencies": {
37
+ "@vue/compiler-dom": "^3.5.0",
38
+ "launch-editor": "^2.10.0",
39
+ "magic-string": "^0.30.0"
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "^24.3.0",
43
+ "@vitejs/plugin-vue": "^6.0.0",
44
+ "esbuild": "^0.28.2",
45
+ "tsup": "^8.5.0",
46
+ "typescript": "~5.9.2",
47
+ "vite": "^8.2.0",
48
+ "vue": "^3.5.0"
49
+ }
50
+ }