vite-plugin-vue-template-inspector 0.1.2 → 0.1.3

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 CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  Vite 开发插件:在页面上 **Alt + 左键** 点击 Vue 模板节点,弹出源码路径,可复制或在本地编辑器中打开。
6
6
 
7
- 仅在 `vite serve` 下生效,不会进入生产构建。
7
+ 仅在开发态生效(Vite `serve` / Webpack `mode !== 'production'`),不会进入生产构建。
8
8
 
9
9
  ## 安装
10
10
 
@@ -44,11 +44,34 @@ export default defineConfig({
44
44
  | `enable` | `true` | 为 `false` 时完全关闭插件 |
45
45
  | `start` | 自动检测 | 传给 [launch-editor](https://github.com/yyx990803/launch-editor) 的编辑器。可用 `code`、`trae`、`webstorm` 等命令名;Windows 上若命令找不到,可改成 exe 绝对路径 |
46
46
 
47
+ ## Webpack 5
48
+
49
+ 需要 `vue-loader` 17、`html-webpack-plugin` 和 `webpack-dev-server`。不必手写 loader rule,插件会在 `vue-loader` 前插入 pre-loader。
50
+
51
+ ```js
52
+ const { VueLoaderPlugin } = require('vue-loader')
53
+ const HtmlWebpackPlugin = require('html-webpack-plugin')
54
+ const { VueTemplateInspectorWebpackPlugin } =
55
+ require('vite-plugin-vue-template-inspector/webpack')
56
+
57
+ module.exports = {
58
+ plugins: [
59
+ new VueLoaderPlugin(),
60
+ new HtmlWebpackPlugin(),
61
+ new VueTemplateInspectorWebpackPlugin({
62
+ enable: true,
63
+ start: 'trae',
64
+ }),
65
+ ],
66
+ }
67
+ ```
68
+
47
69
  ## 本地调试
48
70
 
49
71
  ```bash
50
72
  pnpm install
51
- pnpm dev
73
+ pnpm dev # Vite playground,http://localhost:5175
74
+ pnpm dev:webpack # Webpack playground,http://localhost:5176
52
75
  ```
53
76
 
54
77
  打开 playground 后,按住 Alt 点击页面上的元素即可验证注入、复制路径和打开编辑器。
package/dist/client.js CHANGED
@@ -1,4 +1,4 @@
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=`
1
+ "use strict";(()=>{const x=window.__VUE_INSPECTOR_CONFIG__||{},i=x.ATTR||"data-v-inspector",f=x.REQ_PATH||"/__vue-template-inspector/open-in-editor",g=Symbol.for("vue-template-inspector.initialized");if(window[g])return;window[g]=!0;const b=new WeakMap;let n=null;const s=t=>{if(!(t instanceof Element))return;const e=t.getAttribute(i);e&&(b.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(b.has(e)||e.hasAttribute(i))return e;e=e.parentElement}return null},v=t=>{const e=b.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},L=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 r=document.execCommand("copy");return e.remove(),r}},R=async(t,e,r)=>{const h=new URLSearchParams({file:t,line:e,column:r}),c=await fetch(I()+"?"+h,{method:"POST"});if(!c.ok)throw new Error(await c.text())};function I(){const t=window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__||x.origin||"";if(!t)return f;try{const e=t.startsWith("//")?location.protocol+t:t;return new URL(f,e).href}catch{return f}}function N(t,e,r){l();const h=t.match(/^(.*):(\d+):(\d+)$/),[,c,T,_]=h||[null,t,"-","-"];n=document.createElement("div"),n.style.cssText=`
2
2
  position:fixed;
3
3
  z-index:2147483647;
4
4
  max-width:min(520px,calc(100vw - 24px));
@@ -8,29 +8,29 @@
8
8
  color:#f9fafb;
9
9
  font:13px/1.5 ui-sans-serif,system-ui,sans-serif;
10
10
  box-shadow:0 10px 30px rgba(0,0,0,.35);
11
- `;const p=document.createElement("div");p.textContent=r,p.title=r,p.style.cssText=`
11
+ `;const p=document.createElement("div");p.textContent=c,p.title=c,p.style.cssText=`
12
12
  word-break:break-all;
13
13
  margin-bottom:10px;
14
14
  color:#e5e7eb;
15
- `;const b=document.createElement("div");b.style.cssText=`
15
+ `;const w=document.createElement("div");w.style.cssText=`
16
16
  display:flex;
17
17
  gap:8px;
18
18
  justify-content:flex-end;
19
- `;const h=document.createElement("div");h.style.cssText=`
19
+ `;const y=document.createElement("div");y.style.cssText=`
20
20
  display:flex;
21
21
  align-items:center;
22
22
  gap:6px;
23
23
  margin-right:auto;
24
- `;for(const o of["\u884C "+E,"\u5217 "+T]){const d=document.createElement("span");d.textContent=o,d.style.cssText=`padding:2px 7px;
24
+ `;for(const o of["\u884C "+T,"\u5217 "+_]){const d=document.createElement("span");d.textContent=o,d.style.cssText=`padding:2px 7px;
25
25
  border:1px solid #4b5563;
26
26
  border-radius:4px;
27
27
  background:#1f2937;
28
28
  color:#d1d5db;
29
29
  font-size:12px;
30
- 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;
30
+ white-space:nowrap`,y.append(d)}const E=(o,d)=>{const m=document.createElement("button");return m.type="button",m.textContent=o,m.style.cssText=`cursor:pointer;
31
31
  border:0;
32
32
  border-radius:6px;
33
33
  padding:6px 10px;
34
34
  background:${d};
35
35
  color:#fff;
36
- `,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()})})();
36
+ `,m},u=E("\u590D\u5236\u8DEF\u5F84","#2563eb"),a=E("\u6253\u5F00\u7F16\u8F91\u5668","#059669"),k=E("\u5173\u95ED","#374151");u.onclick=async o=>{o.stopPropagation(),u.textContent=await L(c)?"\u5DF2\u590D\u5236":"\u590D\u5236\u5931\u8D25",setTimeout(()=>{u.textContent="\u590D\u5236\u8DEF\u5F84"},1200)},a.onclick=async o=>{o.stopPropagation(),a.disabled=!0;try{await R(c,T,_),a.textContent="\u5DF2\u6253\u5F00"}catch{a.textContent="\u6253\u5F00\u5931\u8D25"}setTimeout(()=>{a.textContent="\u6253\u5F00\u7F16\u8F91\u5668",a.disabled=!1},1200)},k.onclick=o=>{o.stopPropagation(),l()},w.append(y,u,a,k),n.append(p,w),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,r+8),window.innerHeight-C.height-8)}px`}window.addEventListener("click",t=>{if(t.button!==0||!t.altKey)return;const e=P(t.target),r=e&&v(e);r&&(t.preventDefault(),t.stopPropagation(),N(r,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()})})();
package/dist/index.d.ts CHANGED
@@ -6,6 +6,7 @@ interface VueTemplateInspectorOptions {
6
6
  /** launch-editor 使用的编辑器:命令名(如 `code`、`trae`)或编辑器可执行文件的绝对路径 */
7
7
  start?: string;
8
8
  }
9
+
9
10
  declare function vueTemplateInspector(options?: VueTemplateInspectorOptions): Plugin;
10
11
 
11
12
  export { type VueTemplateInspectorOptions, vueTemplateInspector };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import S from"fs";import p from"path";import $ from"process";import{fileURLToPath as E}from"url";import{NodeTypes as h,parse as C}from"@vue/compiler-dom";import I from"launch-editor";import R from"magic-string";import{parse as O}from"vue/compiler-sfc";var v="data-v-inspector",N="/__vue-template-inspector/open-in-editor",_=E(new URL("./client.js",import.meta.url));function g(e,n){for(let o of e)if(o.type===h.ELEMENT)n(o),g(o.children,n);else if(o.type===h.IF)for(let l of o.branches)g(l.children,n);else o.type===h.FOR&&g(o.children,n)}function w(e,n,o){let{descriptor:l}=O(e,{filename:n});if(!l.template)return null;let i=l.template,r=C(i.content),s=p.relative(o,n).replace(/\\/g,"/"),t=i.loc.start.line-1,m=i.loc.start.offset,a=new R(e),u=!1;return g(r.children,c=>{if(c.props.some(d=>d.type===h.ATTRIBUTE&&d.name===v))return;let b=c.loc.start.line+t,T=c.loc.start.column,y=m+c.loc.start.offset+1+c.tag.length;a.appendLeft(y,` ${v}="${s}:${b}:${T}"`),u=!0}),u?{code:a.toString(),map:a.generateMap({source:n,includeContent:!0,hires:!0})}:null}function M(){let e=S.readFileSync(_,"utf8");return`<script>
2
- window.__VUE_INSPECTOR_CONFIG__=${JSON.stringify({ATTR:v,REQ_PATH:N}).replace(/</g,"\\u003c")};
1
+ import F from"process";import E from"fs";import p from"path";import{fileURLToPath as x}from"url";import{NodeTypes as d,parse as M}from"@vue/compiler-dom";import w from"launch-editor";import A from"magic-string";import{parse as $}from"@vue/compiler-sfc";var g="vue-template-inspector",S="data-v-inspector",N="/__vue-template-inspector/open-in-editor",b=x(new URL("./client.js",import.meta.url));function f(e,n){for(let o of e)if(o.type===d.ELEMENT)n(o),f(o.children,n);else if(o.type===d.IF)for(let r of o.branches)f(r.children,n);else o.type===d.FOR&&f(o.children,n)}function _(e,n,o){let{descriptor:r}=$(e,{filename:n});if(!r.template)return null;let t=r.template,s=M(t.content),i=p.relative(o,n).replace(/\\/g,"/"),u=t.loc.start.line-1,m=t.loc.start.offset,c=new A(e),l=!1;return f(s.children,a=>{if(a.props.some(O=>O.type===d.ATTRIBUTE&&O.name===S))return;let h=a.loc.start.line+u,I=a.loc.start.column,T=m+a.loc.start.offset+1+a.tag.length;c.appendLeft(T,` ${S}="${i}:${h}:${I}"`),l=!0}),l?{code:c.toString(),map:c.generateMap({source:n,includeContent:!0,hires:!0})}:null}function H(e){let n=e.split("?")[0]??e;return e.includes("node_modules")||!n.endsWith(".vue")?null:n}function R(e,n,o){let r=H(n);return r?_(e,r,o):null}function V(){let e=E.readFileSync(b,"utf8");return`<script>
2
+ window.__VUE_INSPECTOR_CONFIG__=${JSON.stringify({ATTR:S,REQ_PATH:N}).replace(/</g,"\\u003c")};
3
3
  ${e};
4
- </script>`}function A(e){if(e)return p.isAbsolute(e)?JSON.stringify(e):e}function W(e={}){let{enable:n=!0,start:o=""}=e,l=A(o),i=$.cwd();return n?{name:"vue-template-inspector",apply:"serve",enforce:"pre",configResolved(r){i=r.root},configureServer(r){r.middlewares.use((s,t,m)=>{if(!s.url)return m();let a=new URL(s.url,"http://localhost");if(a.pathname!==N)return m();if(s.method!=="POST"){t.statusCode=405,t.end("Method Not Allowed");return}let u=a.searchParams.get("file");if(!u){t.statusCode=400,t.end("Missing file");return}let c=p.resolve(i,u),f=p.relative(i,c);if(f===".."||f.startsWith(`..${p.sep}`)||p.isAbsolute(f)){t.statusCode=403,t.end("Forbidden");return}let T=Math.max(1,Number.parseInt(a.searchParams.get("line")??"",10)||1),y=Math.max(1,Number.parseInt(a.searchParams.get("column")??"",10)||1);I(`${c}:${T}:${y}`,l,(d,P)=>{console.error(`[vue-template-inspector] \u65E0\u6CD5\u6253\u5F00 ${d}: ${P}`)}),t.statusCode=204,t.end()})},transform(r,s){let t=s.split("?")[0]??s;if(!(s.includes("node_modules")||!t.endsWith(".vue")))return w(r,t,i)||void 0},transformIndexHtml(r){return r.replace("</body>",`${M()}</body>`)}}:{name:"vue-template-inspector",apply:"serve"}}export{W as vueTemplateInspector};
4
+ </script>`}function P(e){return e.replace("</body>",`${V()}</body>`)}function v(e){if(e)return p.isAbsolute(e)?JSON.stringify(e):e}function C(e){let{getRoot:n,editor:o}=e;return(r,t,s)=>{if(!r.url)return s();let i=new URL(r.url,"http://localhost");if(i.pathname!==N)return s();if(t.setHeader("Access-Control-Allow-Origin","*"),t.setHeader("Access-Control-Allow-Methods","POST, OPTIONS"),t.setHeader("Access-Control-Allow-Headers","*"),r.method==="OPTIONS"){t.statusCode=204,t.end();return}if(r.method!=="POST"){t.statusCode=405,t.end("Method Not Allowed");return}let u=i.searchParams.get("file");if(!u){t.statusCode=400,t.end("Missing file");return}let m=n(),c=p.resolve(m,u),l=p.relative(m,c);if(l===".."||l.startsWith(`..${p.sep}`)||p.isAbsolute(l)){t.statusCode=403,t.end("Forbidden");return}let y=Math.max(1,Number.parseInt(i.searchParams.get("line")??"",10)||1),h=Math.max(1,Number.parseInt(i.searchParams.get("column")??"",10)||1);w(`${c}:${y}:${h}`,o,(I,T)=>{console.error(`[${g}] \u65E0\u6CD5\u6253\u5F00 ${I}: ${T}`)}),t.statusCode=204,t.end()}}function D(e={}){let{enable:n=!0,start:o=""}=e,r=v(o),t=F.cwd();return n?{name:g,apply:"serve",enforce:"pre",configResolved(s){t=s.root},configureServer(s){s.middlewares.use(C({getRoot:()=>t,editor:r}))},transform(s,i){return R(s,i,t)||void 0},transformIndexHtml(s){return P(s)}}:{name:g,apply:"serve"}}export{D as vueTemplateInspector};
5
5
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +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 interface VueTemplateInspectorOptions {\r\n /** 是否启用,默认 true。为 false 时不注入属性、不挂编辑器中间件 */\r\n enable?: boolean\r\n /** launch-editor 使用的编辑器:命令名(如 `code`、`trae`)或编辑器可执行文件的绝对路径 */\r\n start?: string\r\n}\r\n\r\nfunction normalizeEditorStart(start: string | undefined) {\r\n if (!start) return undefined\r\n return path.isAbsolute(start) ? JSON.stringify(start) : start\r\n}\r\n\r\nexport function vueTemplateInspector(options: VueTemplateInspectorOptions = {}): Plugin {\r\n const { enable = true, start = '' } = options\r\n const editor = normalizeEditorStart(start)\r\n let root = process.cwd()\r\n\r\n if (!enable) {\r\n return {\r\n name: 'vue-template-inspector',\r\n apply: 'serve',\r\n }\r\n }\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}`, editor, (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","normalizeEditorStart","start","vueTemplateInspector","options","enable","editor","config","server","req","res","next","url","file","absolutePath","relativePath","fileName","errorMessage","id","html"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/core.ts"],"sourcesContent":["import process from 'node:process'\r\nimport type { Plugin } from 'vite'\r\nimport {\r\n PLUGIN_NAME,\r\n createOpenInEditorMiddleware,\r\n injectClientIntoHtml,\r\n normalizeEditorStart,\r\n transformVueSfc,\r\n type VueTemplateInspectorOptions,\r\n} from './core'\r\n\r\nexport type { VueTemplateInspectorOptions }\r\n\r\nexport function vueTemplateInspector(options: VueTemplateInspectorOptions = {}): Plugin {\r\n const { enable = true, start = '' } = options\r\n const editor = normalizeEditorStart(start)\r\n let root = process.cwd()\r\n\r\n if (!enable) {\r\n return {\r\n name: PLUGIN_NAME,\r\n apply: 'serve',\r\n }\r\n }\r\n\r\n return {\r\n name: PLUGIN_NAME,\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(\r\n createOpenInEditorMiddleware({\r\n getRoot: () => root,\r\n editor,\r\n }),\r\n )\r\n },\r\n transform(code, id) {\r\n return transformVueSfc(code, id, root) || undefined\r\n },\r\n transformIndexHtml(html) {\r\n return injectClientIntoHtml(html)\r\n },\r\n }\r\n}\r\n","import fs from 'node:fs'\r\nimport type { IncomingMessage, ServerResponse } from 'node:http'\r\nimport path from 'node:path'\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 { parse as parseSFC } from '@vue/compiler-sfc'\r\n\r\nexport const PLUGIN_NAME = 'vue-template-inspector'\r\nexport const ATTR = 'data-v-inspector'\r\nexport const REQ_PATH = '/__vue-template-inspector/open-in-editor'\r\n\r\nconst clientScriptPath = fileURLToPath(new URL('./client.js', import.meta.url))\r\n\r\nexport interface VueTemplateInspectorOptions {\r\n /** 是否启用,默认 true。为 false 时不注入属性、不挂编辑器中间件 */\r\n enable?: boolean\r\n /** launch-editor 使用的编辑器:命令名(如 `code`、`trae`)或编辑器可执行文件的绝对路径 */\r\n start?: string\r\n}\r\n\r\nexport interface InspectorTransformResult {\r\n code: string\r\n map: ReturnType<MagicString['generateMap']>\r\n}\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\nexport function injectInspector(\r\n code: string,\r\n filename: string,\r\n root: string,\r\n): InspectorTransformResult | null {\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\nexport function resolveVueFilename(id: string) {\r\n const filename = id.split('?')[0] ?? id\r\n if (id.includes('node_modules') || !filename.endsWith('.vue')) return null\r\n return filename\r\n}\r\n\r\nexport function transformVueSfc(code: string, id: string, root: string) {\r\n const filename = resolveVueFilename(id)\r\n if (!filename) return null\r\n return injectInspector(code, filename, root)\r\n}\r\n\r\nexport function 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 injectClientIntoHtml(html: string) {\r\n return html.replace('</body>', `${clientScript()}</body>`)\r\n}\r\n\r\nexport function normalizeEditorStart(start: string | undefined) {\r\n if (!start) return undefined\r\n return path.isAbsolute(start) ? JSON.stringify(start) : start\r\n}\r\n\r\nexport function createOpenInEditorMiddleware(options: {\r\n getRoot: () => string\r\n editor?: string\r\n}) {\r\n const { getRoot, editor } = options\r\n\r\n return (req: IncomingMessage, res: ServerResponse, next: () => void) => {\r\n if (!req.url) return next()\r\n\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 res.setHeader('Access-Control-Allow-Origin', '*')\r\n res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS')\r\n res.setHeader('Access-Control-Allow-Headers', '*')\r\n\r\n if (req.method === 'OPTIONS') {\r\n res.statusCode = 204\r\n res.end()\r\n return\r\n }\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 root = getRoot()\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}`, editor, (fileName, errorMessage) => {\r\n console.error(`[${PLUGIN_NAME}] 无法打开 ${fileName}: ${errorMessage}`)\r\n })\r\n res.statusCode = 204\r\n res.end()\r\n }\r\n}\r\n"],"mappings":"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","names":["process","fs","path","fileURLToPath","NodeTypes","parseTemplate","launchEditor","MagicString","parseSFC","PLUGIN_NAME","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","resolveVueFilename","id","transformVueSfc","clientScript","clientCode","injectClientIntoHtml","html","normalizeEditorStart","start","createOpenInEditorMiddleware","options","getRoot","editor","req","res","next","url","file","absolutePath","relativePath","fileName","errorMessage","vueTemplateInspector","options","enable","start","editor","normalizeEditorStart","root","process","PLUGIN_NAME","config","server","createOpenInEditorMiddleware","code","id","transformVueSfc","html","injectClientIntoHtml"]}
@@ -0,0 +1,3 @@
1
+ "use strict";var M=Object.create;var c=Object.defineProperty;var $=Object.getOwnPropertyDescriptor;var v=Object.getOwnPropertyNames;var w=Object.getPrototypeOf,A=Object.prototype.hasOwnProperty;var _=(t,e)=>{for(var n in e)c(t,n,{get:e[n],enumerable:!0})},f=(t,e,n,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let o of v(e))!A.call(t,o)&&o!==n&&c(t,o,{get:()=>e[o],enumerable:!(r=$(e,o))||r.enumerable});return t};var a=(t,e,n)=>(n=t!=null?M(w(t)):{},f(e||!t||!t.__esModule?c(n,"default",{value:t,enumerable:!0}):n,t)),L=t=>f(c({},"__esModule",{value:!0}),t);var V={};_(V,{default:()=>C});module.exports=L(V);var U=()=>typeof document>"u"?new URL(`file:${__filename}`).href:document.currentScript&&document.currentScript.tagName.toUpperCase()==="SCRIPT"?document.currentScript.src:new URL("main.js",document.baseURI).href,u=U();var F=a(require("fs"),1),h=a(require("path"),1),T=require("url"),s=require("@vue/compiler-dom"),H=a(require("launch-editor"),1),S=a(require("magic-string"),1),I=require("@vue/compiler-sfc");var g="data-v-inspector";var z=(0,T.fileURLToPath)(new URL("./client.js",u));function l(t,e){for(let n of t)if(n.type===s.NodeTypes.ELEMENT)e(n),l(n.children,e);else if(n.type===s.NodeTypes.IF)for(let r of n.branches)l(r.children,e);else n.type===s.NodeTypes.FOR&&l(n.children,e)}function j(t,e,n){let{descriptor:r}=(0,I.parse)(t,{filename:e});if(!r.template)return null;let o=r.template,b=(0,s.parse)(o.content),N=h.default.relative(n,e).replace(/\\/g,"/"),P=o.loc.start.line-1,x=o.loc.start.offset,p=new S.default(t),d=!1;return l(b.children,i=>{if(i.props.some(m=>m.type===s.NodeTypes.ATTRIBUTE&&m.name===g))return;let y=i.loc.start.line+P,O=i.loc.start.column,E=x+i.loc.start.offset+1+i.tag.length;p.appendLeft(E,` ${g}="${N}:${y}:${O}"`),d=!0}),d?{code:p.toString(),map:p.generateMap({source:e,includeContent:!0,hires:!0})}:null}function k(t){let e=t.split("?")[0]??t;return t.includes("node_modules")||!e.endsWith(".vue")?null:e}function R(t,e,n){let r=k(e);return r?j(t,r,n):null}function C(t){let e=R(t,this.resourcePath,this.rootContext);if(!e)return t;this.callback(null,e.code,e.map)}
2
+ module.exports = module.exports.default || module.exports;
3
+ //# sourceMappingURL=webpack-loader.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/webpack-loader.ts","../node_modules/.pnpm/tsup@8.5.1_postcss@8.5.28_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js","../src/core.ts"],"sourcesContent":["import { transformVueSfc } from './core'\r\n\r\ninterface WebpackLoaderThis {\r\n resourcePath: string\r\n rootContext: string\r\n callback: (error: Error | null, source?: string, map?: object) => void\r\n}\r\n\r\nexport default function vueTemplateInspectorLoader(this: WebpackLoaderThis, code: string) {\r\n const result = transformVueSfc(code, this.resourcePath, this.rootContext)\r\n if (!result) return code\r\n this.callback(null, result.code, result.map)\r\n}\r\n","// Shim globals in cjs bundle\n// There's a weird bug that esbuild will always inject importMetaUrl\n// if we export it as `const importMetaUrl = ... __filename ...`\n// But using a function will not cause this issue\n\nconst getImportMetaUrl = () => \n typeof document === \"undefined\" \n ? new URL(`file:${__filename}`).href \n : (document.currentScript && document.currentScript.tagName.toUpperCase() === 'SCRIPT') \n ? document.currentScript.src \n : new URL(\"main.js\", document.baseURI).href;\n\nexport const importMetaUrl = /* @__PURE__ */ getImportMetaUrl()\n","import fs from 'node:fs'\r\nimport type { IncomingMessage, ServerResponse } from 'node:http'\r\nimport path from 'node:path'\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 { parse as parseSFC } from '@vue/compiler-sfc'\r\n\r\nexport const PLUGIN_NAME = 'vue-template-inspector'\r\nexport const ATTR = 'data-v-inspector'\r\nexport const REQ_PATH = '/__vue-template-inspector/open-in-editor'\r\n\r\nconst clientScriptPath = fileURLToPath(new URL('./client.js', import.meta.url))\r\n\r\nexport interface VueTemplateInspectorOptions {\r\n /** 是否启用,默认 true。为 false 时不注入属性、不挂编辑器中间件 */\r\n enable?: boolean\r\n /** launch-editor 使用的编辑器:命令名(如 `code`、`trae`)或编辑器可执行文件的绝对路径 */\r\n start?: string\r\n}\r\n\r\nexport interface InspectorTransformResult {\r\n code: string\r\n map: ReturnType<MagicString['generateMap']>\r\n}\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\nexport function injectInspector(\r\n code: string,\r\n filename: string,\r\n root: string,\r\n): InspectorTransformResult | null {\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\nexport function resolveVueFilename(id: string) {\r\n const filename = id.split('?')[0] ?? id\r\n if (id.includes('node_modules') || !filename.endsWith('.vue')) return null\r\n return filename\r\n}\r\n\r\nexport function transformVueSfc(code: string, id: string, root: string) {\r\n const filename = resolveVueFilename(id)\r\n if (!filename) return null\r\n return injectInspector(code, filename, root)\r\n}\r\n\r\nexport function 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 injectClientIntoHtml(html: string) {\r\n return html.replace('</body>', `${clientScript()}</body>`)\r\n}\r\n\r\nexport function normalizeEditorStart(start: string | undefined) {\r\n if (!start) return undefined\r\n return path.isAbsolute(start) ? JSON.stringify(start) : start\r\n}\r\n\r\nexport function createOpenInEditorMiddleware(options: {\r\n getRoot: () => string\r\n editor?: string\r\n}) {\r\n const { getRoot, editor } = options\r\n\r\n return (req: IncomingMessage, res: ServerResponse, next: () => void) => {\r\n if (!req.url) return next()\r\n\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 res.setHeader('Access-Control-Allow-Origin', '*')\r\n res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS')\r\n res.setHeader('Access-Control-Allow-Headers', '*')\r\n\r\n if (req.method === 'OPTIONS') {\r\n res.statusCode = 204\r\n res.end()\r\n return\r\n }\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 root = getRoot()\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}`, editor, (fileName, errorMessage) => {\r\n console.error(`[${PLUGIN_NAME}] 无法打开 ${fileName}: ${errorMessage}`)\r\n })\r\n res.statusCode = 204\r\n res.end()\r\n }\r\n}\r\n"],"mappings":"0jBAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,aAAAE,IAAA,eAAAC,EAAAH,GCKA,IAAMI,EAAmB,IACvB,OAAO,SAAa,IAChB,IAAI,IAAI,QAAQ,UAAU,EAAE,EAAE,KAC7B,SAAS,eAAiB,SAAS,cAAc,QAAQ,YAAY,IAAM,SAC1E,SAAS,cAAc,IACvB,IAAI,IAAI,UAAW,SAAS,OAAO,EAAE,KAEhCC,EAAgCD,EAAiB,ECZ9D,IAAAE,EAAe,mBAEfC,EAAiB,qBACjBC,EAA8B,eAC9BC,EAAkD,6BAElDC,EAAyB,8BACzBC,EAAwB,6BACxBC,EAAkC,6BAG3B,IAAMC,EAAO,mBAGpB,IAAMC,KAAmB,iBAAc,IAAI,IAAI,cAAeC,CAAe,CAAC,EAc9E,SAASC,EAAaC,EAA4BC,EAAkC,CAClF,QAAWC,KAAQF,EACjB,GAAIE,EAAK,OAAS,YAAU,QAC1BD,EAAMC,CAAI,EACVH,EAAaG,EAAK,SAAUD,CAAK,UACxBC,EAAK,OAAS,YAAU,GACjC,QAAWC,KAAUD,EAAK,SAAUH,EAAaI,EAAO,SAAUF,CAAK,OAC9DC,EAAK,OAAS,YAAU,KACjCH,EAAaG,EAAK,SAAUD,CAAK,CAGvC,CAEO,SAASG,EACdC,EACAC,EACAC,EACiC,CACjC,GAAM,CAAE,WAAAC,CAAW,KAAI,EAAAC,OAASJ,EAAM,CAAE,SAAAC,CAAS,CAAC,EAClD,GAAI,CAACE,EAAW,SAAU,OAAO,KAEjC,IAAME,EAAWF,EAAW,SACtBG,KAAM,EAAAC,OAAcF,EAAS,OAAO,EACpCG,EAAM,EAAAC,QAAK,SAASP,EAAMD,CAAQ,EAAE,QAAQ,MAAO,GAAG,EACtDS,EAAaL,EAAS,IAAI,MAAM,KAAO,EACvCM,EAAiBN,EAAS,IAAI,MAAM,OACpCO,EAAc,IAAI,EAAAC,QAAYb,CAAI,EACpCc,EAAU,GAcd,OAZApB,EAAaY,EAAI,SAAWS,GAAO,CAIjC,GAHgBA,EAAG,MAAM,KACtBC,GAASA,EAAK,OAAS,YAAU,WAAaA,EAAK,OAASC,CAC/D,EACa,OACb,IAAMC,EAAOH,EAAG,IAAI,MAAM,KAAOL,EAC3BS,EAASJ,EAAG,IAAI,MAAM,OACtBK,EAAiBT,EAAiBI,EAAG,IAAI,MAAM,OAAS,EAAIA,EAAG,IAAI,OACzEH,EAAY,WAAWQ,EAAgB,IAAIH,CAAI,KAAKT,CAAG,IAAIU,CAAI,IAAIC,CAAM,GAAG,EAC5EL,EAAU,EACZ,CAAC,EAEIA,EACE,CACL,KAAMF,EAAY,SAAS,EAC3B,IAAKA,EAAY,YAAY,CAC3B,OAAQX,EACR,eAAgB,GAChB,MAAO,EACT,CAAC,CACH,EARqB,IASvB,CAEO,SAASoB,EAAmBC,EAAY,CAC7C,IAAMrB,EAAWqB,EAAG,MAAM,GAAG,EAAE,CAAC,GAAKA,EACrC,OAAIA,EAAG,SAAS,cAAc,GAAK,CAACrB,EAAS,SAAS,MAAM,EAAU,KAC/DA,CACT,CAEO,SAASsB,EAAgBvB,EAAcsB,EAAYpB,EAAc,CACtE,IAAMD,EAAWoB,EAAmBC,CAAE,EACtC,OAAKrB,EACEF,EAAgBC,EAAMC,EAAUC,CAAI,EADrB,IAExB,CFlFe,SAARsB,EAAqEC,EAAc,CACxF,IAAMC,EAASC,EAAgBF,EAAM,KAAK,aAAc,KAAK,WAAW,EACxE,GAAI,CAACC,EAAQ,OAAOD,EACpB,KAAK,SAAS,KAAMC,EAAO,KAAMA,EAAO,GAAG,CAC7C","names":["webpack_loader_exports","__export","vueTemplateInspectorLoader","__toCommonJS","getImportMetaUrl","importMetaUrl","import_node_fs","import_node_path","import_node_url","import_compiler_dom","import_launch_editor","import_magic_string","import_compiler_sfc","ATTR","clientScriptPath","importMetaUrl","walkElements","nodes","visit","node","branch","injectInspector","code","filename","root","descriptor","parseSFC","template","ast","parseTemplate","rel","path","lineOffset","templateOffset","magicString","MagicString","changed","el","prop","ATTR","line","column","insertPosition","resolveVueFilename","id","transformVueSfc","vueTemplateInspectorLoader","code","result","transformVueSfc"]}
@@ -0,0 +1,5 @@
1
+ "use strict";var $=Object.create;var g=Object.defineProperty;var A=Object.getOwnPropertyDescriptor;var H=Object.getOwnPropertyNames;var _=Object.getPrototypeOf,U=Object.prototype.hasOwnProperty;var r=(e,t)=>g(e,"name",{value:t,configurable:!0});var L=(e,t)=>{for(var o in t)g(e,o,{get:t[o],enumerable:!0})},C=(e,t,o,i)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of H(t))!U.call(e,n)&&n!==o&&g(e,n,{get:()=>t[n],enumerable:!(i=A(t,n))||i.enumerable});return e};var w=(e,t,o)=>(o=e!=null?$(_(e)):{},C(t||!e||!e.__esModule?g(o,"default",{value:e,enumerable:!0}):o,e)),j=e=>C(g({},"__esModule",{value:!0}),e);var Q={};L(Q,{VueTemplateInspectorWebpackPlugin:()=>I});module.exports=j(Q);var F=r(()=>typeof document>"u"?new URL(`file:${__filename}`).href:document.currentScript&&document.currentScript.tagName.toUpperCase()==="SCRIPT"?document.currentScript.src:new URL("main.js",document.baseURI).href,"getImportMetaUrl"),y=F();var k=require("url");var T=w(require("fs"),1),a=w(require("path"),1),b=require("url"),v=require("@vue/compiler-dom"),R=w(require("launch-editor"),1),V=w(require("magic-string"),1),W=require("@vue/compiler-sfc");var h="vue-template-inspector",G="data-v-inspector",O="/__vue-template-inspector/open-in-editor",z=(0,b.fileURLToPath)(new URL("./client.js",y));function D(){let e=T.default.readFileSync(z,"utf8");return`<script>
2
+ window.__VUE_INSPECTOR_CONFIG__=${JSON.stringify({ATTR:G,REQ_PATH:O}).replace(/</g,"\\u003c")};
3
+ ${e};
4
+ </script>`}r(D,"clientScript");function P(e){return e.replace("</body>",`${D()}</body>`)}r(P,"injectClientIntoHtml");function E(e){if(e)return a.default.isAbsolute(e)?JSON.stringify(e):e}r(E,"normalizeEditorStart");function N(e){let{getRoot:t,editor:o}=e;return(i,n,S)=>{if(!i.url)return S();let l=new URL(i.url,"http://localhost");if(l.pathname!==O)return S();if(n.setHeader("Access-Control-Allow-Origin","*"),n.setHeader("Access-Control-Allow-Methods","POST, OPTIONS"),n.setHeader("Access-Control-Allow-Headers","*"),i.method==="OPTIONS"){n.statusCode=204,n.end();return}if(i.method!=="POST"){n.statusCode=405,n.end("Method Not Allowed");return}let u=l.searchParams.get("file");if(!u){n.statusCode=400,n.end("Missing file");return}let p=t(),d=a.default.resolve(p,u),m=a.default.relative(p,d);if(m===".."||m.startsWith(`..${a.default.sep}`)||a.default.isAbsolute(m)){n.statusCode=403,n.end("Forbidden");return}let s=Math.max(1,Number.parseInt(l.searchParams.get("line")??"",10)||1),c=Math.max(1,Number.parseInt(l.searchParams.get("column")??"",10)||1);(0,R.default)(`${d}:${s}:${c}`,o,(x,M)=>{console.error(`[${h}] \u65E0\u6CD5\u6253\u5F00 ${x}: ${M}`)}),n.statusCode=204,n.end()}}r(N,"createOpenInEditorMiddleware");function J(){return(0,k.fileURLToPath)(new URL("./webpack-loader.cjs",y))}r(J,"loaderFile");function B(e){for(let t of e.options.plugins??[]){if(!t||typeof t!="object")continue;let o=t.constructor;if(o.name==="HtmlWebpackPlugin"&&typeof o.getHooks=="function")return o}}r(B,"findHtmlWebpackPlugin");var I=class{static{r(this,"VueTemplateInspectorWebpackPlugin")}options;constructor(t={}){this.options=t}apply(t){let{enable:o=!0,start:i=""}=this.options;if(!o||t.options.mode==="production")return;let n=E(i),S=t.context,l=J(),u=t.options.module.rules;Array.isArray(u)&&u.unshift({test:/\.vue$/,enforce:"pre",exclude:/node_modules/,use:[{loader:l}]});let p=t.options;p.devServer??={};let d=p.devServer.setupMiddlewares,m=N({getRoot:r(()=>S,"getRoot"),editor:n});p.devServer.setupMiddlewares=(f,s)=>{let c=d?d(f,s):f;return s.app&&s.app.use(m),c},t.hooks.compilation.tap(h,f=>{let s=B(t);if(!s){console.warn(`[${h}] html-webpack-plugin not found, skip injecting client script`);return}s.getHooks(f).beforeEmit.tap(h,c=>(c.html=P(c.html),c))})}};0&&(module.exports={VueTemplateInspectorWebpackPlugin});
5
+ //# sourceMappingURL=webpack.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/webpack.ts","../node_modules/.pnpm/tsup@8.5.1_postcss@8.5.28_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js","../src/core.ts"],"sourcesContent":["import { fileURLToPath } from 'node:url'\r\nimport type { Compiler } from 'webpack'\r\nimport {\r\n PLUGIN_NAME,\r\n createOpenInEditorMiddleware,\r\n injectClientIntoHtml,\r\n normalizeEditorStart,\r\n type VueTemplateInspectorOptions,\r\n} from './core'\r\n\r\nexport type { VueTemplateInspectorOptions }\r\n\r\ntype HtmlWebpackPluginCtor = {\r\n getHooks: (compilation: unknown) => {\r\n beforeEmit: {\r\n tap: (name: string, fn: (data: { html: string }) => { html: string }) => void\r\n }\r\n }\r\n}\r\n\r\ntype DevServerOptions = {\r\n setupMiddlewares?: (\r\n middlewares: unknown[],\r\n devServer: { app?: { use: (middleware: unknown) => void } },\r\n ) => unknown[]\r\n}\r\n\r\nfunction loaderFile() {\r\n return fileURLToPath(new URL('./webpack-loader.cjs', import.meta.url))\r\n}\r\n\r\nfunction findHtmlWebpackPlugin(compiler: Compiler): HtmlWebpackPluginCtor | undefined {\r\n for (const plugin of compiler.options.plugins ?? []) {\r\n if (!plugin || typeof plugin !== 'object') continue\r\n const ctor = plugin.constructor as unknown as HtmlWebpackPluginCtor & { name?: string }\r\n if (ctor.name === 'HtmlWebpackPlugin' && typeof ctor.getHooks === 'function') return ctor\r\n }\r\n}\r\n\r\nexport class VueTemplateInspectorWebpackPlugin {\r\n private readonly options: VueTemplateInspectorOptions\r\n\r\n constructor(options: VueTemplateInspectorOptions = {}) {\r\n this.options = options\r\n }\r\n\r\n apply(compiler: Compiler) {\r\n const { enable = true, start = '' } = this.options\r\n if (!enable || compiler.options.mode === 'production') return\r\n\r\n const editor = normalizeEditorStart(start)\r\n const root = compiler.context\r\n const loader = loaderFile()\r\n\r\n const rules = compiler.options.module.rules\r\n if (Array.isArray(rules)) {\r\n rules.unshift({\r\n test: /\\.vue$/,\r\n enforce: 'pre',\r\n exclude: /node_modules/,\r\n use: [{ loader }],\r\n })\r\n }\r\n\r\n const options = compiler.options as Compiler['options'] & { devServer?: DevServerOptions }\r\n options.devServer ??= {}\r\n const originalSetup = options.devServer.setupMiddlewares\r\n const openInEditor = createOpenInEditorMiddleware({\r\n getRoot: () => root,\r\n editor,\r\n })\r\n\r\n // 拦截 devServer 的 setupMiddlewares 方法,在中间件数组中插入 openInEditor 中间件\r\n options.devServer.setupMiddlewares = (middlewares, devServer) => {\r\n const next = originalSetup ? originalSetup(middlewares, devServer) : middlewares\r\n if (devServer.app) devServer.app.use(openInEditor)\r\n return next\r\n }\r\n\r\n // 拦截 compilation 钩子,在 beforeEmit 阶段注入 client script\r\n compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {\r\n const HtmlWebpackPlugin = findHtmlWebpackPlugin(compiler)\r\n // 找不到html 就不行,因为要注入 client script 到 html 中\r\n if (!HtmlWebpackPlugin) {\r\n console.warn(`[${PLUGIN_NAME}] html-webpack-plugin not found, skip injecting client script`)\r\n return\r\n }\r\n HtmlWebpackPlugin.getHooks(compilation).beforeEmit.tap(PLUGIN_NAME, (data) => {\r\n data.html = injectClientIntoHtml(data.html)\r\n return data\r\n })\r\n })\r\n }\r\n}\r\n","// Shim globals in cjs bundle\n// There's a weird bug that esbuild will always inject importMetaUrl\n// if we export it as `const importMetaUrl = ... __filename ...`\n// But using a function will not cause this issue\n\nconst getImportMetaUrl = () => \n typeof document === \"undefined\" \n ? new URL(`file:${__filename}`).href \n : (document.currentScript && document.currentScript.tagName.toUpperCase() === 'SCRIPT') \n ? document.currentScript.src \n : new URL(\"main.js\", document.baseURI).href;\n\nexport const importMetaUrl = /* @__PURE__ */ getImportMetaUrl()\n","import fs from 'node:fs'\r\nimport type { IncomingMessage, ServerResponse } from 'node:http'\r\nimport path from 'node:path'\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 { parse as parseSFC } from '@vue/compiler-sfc'\r\n\r\nexport const PLUGIN_NAME = 'vue-template-inspector'\r\nexport const ATTR = 'data-v-inspector'\r\nexport const REQ_PATH = '/__vue-template-inspector/open-in-editor'\r\n\r\nconst clientScriptPath = fileURLToPath(new URL('./client.js', import.meta.url))\r\n\r\nexport interface VueTemplateInspectorOptions {\r\n /** 是否启用,默认 true。为 false 时不注入属性、不挂编辑器中间件 */\r\n enable?: boolean\r\n /** launch-editor 使用的编辑器:命令名(如 `code`、`trae`)或编辑器可执行文件的绝对路径 */\r\n start?: string\r\n}\r\n\r\nexport interface InspectorTransformResult {\r\n code: string\r\n map: ReturnType<MagicString['generateMap']>\r\n}\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\nexport function injectInspector(\r\n code: string,\r\n filename: string,\r\n root: string,\r\n): InspectorTransformResult | null {\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\nexport function resolveVueFilename(id: string) {\r\n const filename = id.split('?')[0] ?? id\r\n if (id.includes('node_modules') || !filename.endsWith('.vue')) return null\r\n return filename\r\n}\r\n\r\nexport function transformVueSfc(code: string, id: string, root: string) {\r\n const filename = resolveVueFilename(id)\r\n if (!filename) return null\r\n return injectInspector(code, filename, root)\r\n}\r\n\r\nexport function 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 injectClientIntoHtml(html: string) {\r\n return html.replace('</body>', `${clientScript()}</body>`)\r\n}\r\n\r\nexport function normalizeEditorStart(start: string | undefined) {\r\n if (!start) return undefined\r\n return path.isAbsolute(start) ? JSON.stringify(start) : start\r\n}\r\n\r\nexport function createOpenInEditorMiddleware(options: {\r\n getRoot: () => string\r\n editor?: string\r\n}) {\r\n const { getRoot, editor } = options\r\n\r\n return (req: IncomingMessage, res: ServerResponse, next: () => void) => {\r\n if (!req.url) return next()\r\n\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 res.setHeader('Access-Control-Allow-Origin', '*')\r\n res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS')\r\n res.setHeader('Access-Control-Allow-Headers', '*')\r\n\r\n if (req.method === 'OPTIONS') {\r\n res.statusCode = 204\r\n res.end()\r\n return\r\n }\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 root = getRoot()\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}`, editor, (fileName, errorMessage) => {\r\n console.error(`[${PLUGIN_NAME}] 无法打开 ${fileName}: ${errorMessage}`)\r\n })\r\n res.statusCode = 204\r\n res.end()\r\n }\r\n}\r\n"],"mappings":"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","names":["webpack_exports","__export","VueTemplateInspectorWebpackPlugin","__toCommonJS","getImportMetaUrl","__name","importMetaUrl","import_node_url","import_node_fs","import_node_path","import_node_url","import_compiler_dom","import_launch_editor","import_magic_string","import_compiler_sfc","PLUGIN_NAME","ATTR","REQ_PATH","clientScriptPath","importMetaUrl","clientScript","clientCode","fs","clientScriptPath","ATTR","REQ_PATH","__name","injectClientIntoHtml","html","normalizeEditorStart","start","path","createOpenInEditorMiddleware","options","getRoot","editor","req","res","next","url","file","root","absolutePath","relativePath","line","column","launchEditor","fileName","errorMessage","PLUGIN_NAME","loaderFile","importMetaUrl","__name","findHtmlWebpackPlugin","compiler","plugin","ctor","VueTemplateInspectorWebpackPlugin","options","enable","start","editor","normalizeEditorStart","root","loader","rules","originalSetup","openInEditor","createOpenInEditorMiddleware","middlewares","devServer","next","PLUGIN_NAME","compilation","HtmlWebpackPlugin","data","injectClientIntoHtml"]}
@@ -0,0 +1,16 @@
1
+ import { Compiler } from 'webpack';
2
+
3
+ interface VueTemplateInspectorOptions {
4
+ /** 是否启用,默认 true。为 false 时不注入属性、不挂编辑器中间件 */
5
+ enable?: boolean;
6
+ /** launch-editor 使用的编辑器:命令名(如 `code`、`trae`)或编辑器可执行文件的绝对路径 */
7
+ start?: string;
8
+ }
9
+
10
+ declare class VueTemplateInspectorWebpackPlugin {
11
+ private readonly options;
12
+ constructor(options?: VueTemplateInspectorOptions);
13
+ apply(compiler: Compiler): void;
14
+ }
15
+
16
+ export { type VueTemplateInspectorOptions, VueTemplateInspectorWebpackPlugin };
@@ -0,0 +1,16 @@
1
+ import { Compiler } from 'webpack';
2
+
3
+ interface VueTemplateInspectorOptions {
4
+ /** 是否启用,默认 true。为 false 时不注入属性、不挂编辑器中间件 */
5
+ enable?: boolean;
6
+ /** launch-editor 使用的编辑器:命令名(如 `code`、`trae`)或编辑器可执行文件的绝对路径 */
7
+ start?: string;
8
+ }
9
+
10
+ declare class VueTemplateInspectorWebpackPlugin {
11
+ private readonly options;
12
+ constructor(options?: VueTemplateInspectorOptions);
13
+ apply(compiler: Compiler): void;
14
+ }
15
+
16
+ export { type VueTemplateInspectorOptions, VueTemplateInspectorWebpackPlugin };
package/package.json CHANGED
@@ -1,10 +1,12 @@
1
1
  {
2
2
  "name": "vite-plugin-vue-template-inspector",
3
- "version": "0.1.2",
4
- "description": "Vite plugin: Alt+click Vue template elements to copy the source path or open it in your editor.",
3
+ "version": "0.1.3",
4
+ "description": "Vite/Webpack plugin: Alt+click Vue template elements to copy the source path or open it in your editor.",
5
5
  "keywords": [
6
6
  "vite",
7
7
  "vite-plugin",
8
+ "webpack",
9
+ "webpack-plugin",
8
10
  "vue",
9
11
  "inspector",
10
12
  "devtools"
@@ -31,29 +33,62 @@
31
33
  "types": "./dist/index.d.ts",
32
34
  "import": "./dist/index.js",
33
35
  "default": "./dist/index.js"
36
+ },
37
+ "./webpack": {
38
+ "types": "./dist/webpack.d.ts",
39
+ "require": "./dist/webpack.cjs",
40
+ "import": "./dist/webpack.cjs",
41
+ "default": "./dist/webpack.cjs"
34
42
  }
35
43
  },
36
44
  "scripts": {
37
45
  "dev": "vite --config playground/vite.config.ts",
38
- "build": "tsup && node scripts/copy-client.mjs",
46
+ "dev:webpack": "pnpm build && webpack serve --config playground-webpack/webpack.config.cjs",
47
+ "build": "tsup && tsup --config tsup.webpack.config.ts && node scripts/copy-client.mjs",
39
48
  "prepublishOnly": "pnpm build"
40
49
  },
41
50
  "peerDependencies": {
51
+ "html-webpack-plugin": "^5.0.0",
42
52
  "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0",
43
- "vue": "^3.3.0"
53
+ "vue": "^2.6.0 || ^3.3.0",
54
+ "webpack": "^5.0.0",
55
+ "webpack-dev-server": "^4.0.0 || ^5.0.0"
56
+ },
57
+ "peerDependenciesMeta": {
58
+ "html-webpack-plugin": {
59
+ "optional": true
60
+ },
61
+ "vite": {
62
+ "optional": true
63
+ },
64
+ "webpack": {
65
+ "optional": true
66
+ },
67
+ "webpack-dev-server": {
68
+ "optional": true
69
+ }
44
70
  },
45
71
  "dependencies": {
46
72
  "@vue/compiler-dom": "^3.5.0",
73
+ "@vue/compiler-sfc": "^3.5.0",
47
74
  "launch-editor": "^2.10.0",
48
75
  "magic-string": "^0.30.0"
49
76
  },
50
77
  "devDependencies": {
51
78
  "@types/node": "^24.3.0",
52
79
  "@vitejs/plugin-vue": "^6.0.0",
80
+ "css-loader": "^7.1.2",
53
81
  "esbuild": "^0.28.2",
82
+ "html-webpack-plugin": "^5.6.3",
83
+ "style-loader": "^4.0.0",
84
+ "ts-loader": "^9.5.2",
54
85
  "tsup": "^8.5.0",
55
86
  "typescript": "~5.9.2",
56
87
  "vite": "^8.2.0",
57
- "vue": "^3.5.0"
88
+ "vue": "^3.5.0",
89
+ "vue-loader": "^17.4.2",
90
+ "webpack": "^5.97.1",
91
+ "webpack-cli": "^6.0.1",
92
+ "webpack-dev-server": "^5.2.0"
58
93
  }
59
94
  }