index.mjs 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. // src/index.ts
  2. import path2 from "node:path";
  3. import { fileURLToPath } from "node:url";
  4. import fs from "node:fs";
  5. import process2 from "node:process";
  6. import { bold, dim, green, yellow } from "kolorist";
  7. import { normalizePath as normalizePath2 } from "vite";
  8. import MagicString2 from "magic-string";
  9. // src/compiler/template.ts
  10. import path from "node:path";
  11. import MagicString from "magic-string";
  12. import { parse as vueParse, transform as vueTransform } from "@vue/compiler-dom";
  13. import { parse as babelParse, traverse as babelTraverse } from "@babel/core";
  14. import vueJsxPlugin from "@vue/babel-plugin-jsx";
  15. import typescriptPlugin from "@babel/plugin-transform-typescript";
  16. import importMeta from "@babel/plugin-syntax-import-meta";
  17. import decoratorsPlugin from "@babel/plugin-proposal-decorators";
  18. import importAttributesPlugin from "@babel/plugin-syntax-import-attributes";
  19. import { normalizePath } from "vite";
  20. var EXCLUDE_TAG = ["template", "script", "style"];
  21. var KEY_DATA = "data-v-inspector";
  22. async function compileSFCTemplate({ code, id, type }) {
  23. const s = new MagicString(code);
  24. const relativePath = normalizePath(path.relative(process.cwd(), id));
  25. const result = await new Promise((resolve) => {
  26. switch (type) {
  27. case "template": {
  28. const ast = vueParse(code, { comments: true });
  29. vueTransform(ast, {
  30. nodeTransforms: [
  31. (node) => {
  32. if (node.type === 1) {
  33. if ((node.tagType === 0 || node.tagType === 1) && !EXCLUDE_TAG.includes(node.tag)) {
  34. if (node.loc.source.includes(KEY_DATA))
  35. return;
  36. const insertPosition = node.props.length ? Math.max(...node.props.map((i) => i.loc.end.offset)) : node.loc.start.offset + node.tag.length + 1;
  37. const { line, column } = node.loc.start;
  38. const content = ` ${KEY_DATA}="${relativePath}:${line}:${column}"`;
  39. s.prependLeft(
  40. insertPosition,
  41. content
  42. );
  43. }
  44. }
  45. }
  46. ]
  47. });
  48. break;
  49. }
  50. case "jsx": {
  51. const ast = babelParse(code, {
  52. babelrc: false,
  53. configFile: false,
  54. comments: true,
  55. plugins: [
  56. importMeta,
  57. [vueJsxPlugin, {}],
  58. [
  59. typescriptPlugin,
  60. { isTSX: true, allowExtensions: true }
  61. ],
  62. [
  63. decoratorsPlugin,
  64. { legacy: true }
  65. ],
  66. [
  67. importAttributesPlugin,
  68. { deprecatedAssertSyntax: true }
  69. ]
  70. ]
  71. });
  72. babelTraverse(ast, {
  73. enter({ node }) {
  74. if (node.type === "JSXElement" && !EXCLUDE_TAG.includes(s.slice(node.openingElement.name.start, node.openingElement.name.end))) {
  75. if (node.openingElement.attributes.some(
  76. (attr) => attr.type !== "JSXSpreadAttribute" && attr.name.name === KEY_DATA
  77. ))
  78. return;
  79. const insertPosition = node.openingElement.end - (node.openingElement.selfClosing ? 2 : 1);
  80. const { line, column } = node.loc.start;
  81. const content = ` ${KEY_DATA}="${relativePath}:${line}:${column}"`;
  82. s.prependLeft(
  83. insertPosition,
  84. content
  85. );
  86. }
  87. }
  88. });
  89. break;
  90. }
  91. default:
  92. break;
  93. }
  94. resolve(s.toString());
  95. });
  96. return result;
  97. }
  98. // src/utils/index.ts
  99. function parseVueRequest(id) {
  100. const [filename] = id.split("?", 2);
  101. const url = new URL(id, "http://domain.inspector");
  102. const query = Object.fromEntries(url.searchParams.entries());
  103. if (query.vue != null)
  104. query.vue = true;
  105. if (query.src != null)
  106. query.src = true;
  107. if (query.index != null)
  108. query.index = Number(query.index);
  109. if (query.raw != null)
  110. query.raw = true;
  111. if (query.hasOwnProperty("lang.tsx") || query.hasOwnProperty("lang.jsx"))
  112. query.isJsx = true;
  113. return {
  114. filename,
  115. query
  116. };
  117. }
  118. var FS_PREFIX = "/@fs/";
  119. var IS_WINDOWS = process.platform === "win32";
  120. var queryRE = /\?.*$/s;
  121. var hashRE = /#.*$/s;
  122. function idToFile(id) {
  123. if (id.startsWith(FS_PREFIX))
  124. id = id = id.slice(IS_WINDOWS ? FS_PREFIX.length : FS_PREFIX.length - 1);
  125. return id.replace(hashRE, "").replace(queryRE, "");
  126. }
  127. // src/index.ts
  128. var toggleComboKeysMap = {
  129. control: process2.platform === "darwin" ? "Control(^)" : "Ctrl(^)",
  130. meta: "Command(\u2318)",
  131. shift: "Shift(\u21E7)"
  132. };
  133. function getInspectorPath() {
  134. const pluginPath = normalizePath2(path2.dirname(fileURLToPath(import.meta.url)));
  135. return pluginPath.replace(/\/dist$/, "/src");
  136. }
  137. function normalizeComboKeyPrint(toggleComboKey) {
  138. return toggleComboKey.split("-").map((key) => toggleComboKeysMap[key] || key[0].toUpperCase() + key.slice(1)).join(dim("+"));
  139. }
  140. var DEFAULT_INSPECTOR_OPTIONS = {
  141. vue: 3,
  142. enabled: false,
  143. toggleComboKey: process2.platform === "darwin" ? "meta-shift" : "control-shift",
  144. toggleButtonVisibility: "active",
  145. toggleButtonPos: "top-right",
  146. appendTo: "",
  147. lazyLoad: false,
  148. launchEditor: process2.env.LAUNCH_EDITOR ?? "code",
  149. reduceMotion: false
  150. };
  151. function VitePluginInspector(options = DEFAULT_INSPECTOR_OPTIONS) {
  152. const inspectorPath = getInspectorPath();
  153. const normalizedOptions = {
  154. ...DEFAULT_INSPECTOR_OPTIONS,
  155. ...options
  156. };
  157. let config;
  158. const {
  159. vue,
  160. appendTo,
  161. cleanHtml = vue === 3
  162. // Only enabled for Vue 3 by default
  163. } = normalizedOptions;
  164. if (normalizedOptions.launchEditor)
  165. process2.env.LAUNCH_EDITOR = normalizedOptions.launchEditor;
  166. return [
  167. {
  168. name: "vite-plugin-vue-inspector",
  169. enforce: "pre",
  170. apply(_, { command }) {
  171. return command === "serve" && process2.env.NODE_ENV !== "test";
  172. },
  173. async resolveId(importee) {
  174. if (importee.startsWith("virtual:vue-inspector-options")) {
  175. return importee;
  176. } else if (importee.startsWith("virtual:vue-inspector-path:")) {
  177. const resolved = importee.replace("virtual:vue-inspector-path:", `${inspectorPath}/`);
  178. return resolved;
  179. }
  180. },
  181. async load(id) {
  182. if (id === "virtual:vue-inspector-options") {
  183. return `export default ${JSON.stringify({ ...normalizedOptions, base: config.base })}`;
  184. } else if (id.startsWith(inspectorPath)) {
  185. const { query } = parseVueRequest(id);
  186. if (query.type)
  187. return;
  188. const file = idToFile(id);
  189. if (fs.existsSync(file))
  190. return await fs.promises.readFile(file, "utf-8");
  191. else
  192. console.error(`failed to find file for vue-inspector: ${file}, referenced by id ${id}.`);
  193. }
  194. },
  195. transform(code, id) {
  196. const { filename, query } = parseVueRequest(id);
  197. const isJsx = filename.endsWith(".jsx") || filename.endsWith(".tsx") || filename.endsWith(".vue") && query.isJsx;
  198. const isTpl = filename.endsWith(".vue") && query.type !== "style" && !query.raw;
  199. if (isJsx || isTpl)
  200. return compileSFCTemplate({ code, id: filename, type: isJsx ? "jsx" : "template" });
  201. if (!appendTo)
  202. return;
  203. if (typeof appendTo === "string" && filename.endsWith(appendTo) || appendTo instanceof RegExp && appendTo.test(filename))
  204. return { code: `${code}
  205. import 'virtual:vue-inspector-path:load.js'` };
  206. },
  207. configureServer(server) {
  208. const _printUrls = server.printUrls;
  209. const { toggleComboKey } = normalizedOptions;
  210. toggleComboKey && (server.printUrls = () => {
  211. const keys = normalizeComboKeyPrint(toggleComboKey);
  212. _printUrls();
  213. console.log(` ${green("\u279C")} ${bold("Vue Inspector")}: ${green(`Press ${yellow(keys)} in App to toggle the Inspector`)}
  214. `);
  215. });
  216. },
  217. transformIndexHtml(html) {
  218. if (appendTo)
  219. return;
  220. return {
  221. html,
  222. tags: [
  223. {
  224. tag: "script",
  225. injectTo: "head",
  226. attrs: {
  227. type: "module",
  228. src: `${config.base || "/"}@id/virtual:vue-inspector-path:load.js`
  229. }
  230. }
  231. ]
  232. };
  233. },
  234. configResolved(resolvedConfig) {
  235. config = resolvedConfig;
  236. }
  237. },
  238. {
  239. name: "vite-plugin-vue-inspector:post",
  240. enforce: "post",
  241. apply(_, { command }) {
  242. return cleanHtml && vue === 3 && command === "serve" && process2.env.NODE_ENV !== "test";
  243. },
  244. transform(code) {
  245. if (code.includes("_interopVNode"))
  246. return;
  247. if (!code.includes("data-v-inspector"))
  248. return;
  249. const fn = /* @__PURE__ */ new Set();
  250. const s = new MagicString2(code);
  251. s.replace(/(createElementVNode|createVNode|createElementBlock|createBlock) as _\1,?/g, (_, name) => {
  252. fn.add(name);
  253. return "";
  254. });
  255. if (!fn.size)
  256. return;
  257. s.appendLeft(0, `/* Injection by vite-plugin-vue-inspector Start */
  258. import { ${Array.from(fn.values()).map((i) => `${i} as __${i}`).join(",")} } from 'vue'
  259. function _interopVNode(vnode) {
  260. if (vnode && vnode.props && 'data-v-inspector' in vnode.props) {
  261. const data = vnode.props['data-v-inspector']
  262. delete vnode.props['data-v-inspector']
  263. Object.defineProperty(vnode.props, '__v_inspector', { value: data, enumerable: false })
  264. }
  265. return vnode
  266. }
  267. ${Array.from(fn.values()).map((i) => `function _${i}(...args) { return _interopVNode(__${i}(...args)) }`).join("\n")}
  268. /* Injection by vite-plugin-vue-inspector End */
  269. `);
  270. return {
  271. code: s.toString(),
  272. map: s.generateMap({ hires: "boundary" })
  273. };
  274. }
  275. }
  276. ];
  277. }
  278. var src_default = VitePluginInspector;
  279. export {
  280. DEFAULT_INSPECTOR_OPTIONS,
  281. src_default as default,
  282. normalizeComboKeyPrint
  283. };