index.mjs 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. import { createHash } from 'node:crypto';
  2. import path from 'node:path';
  3. import * as babel from '@babel/core';
  4. import jsx from '@vue/babel-plugin-jsx';
  5. import { createFilter, normalizePath } from 'vite';
  6. const ssrRegisterHelperId = "/__vue-jsx-ssr-register-helper";
  7. const ssrRegisterHelperCode = `import { useSSRContext } from "vue"
  8. export ${ssrRegisterHelper.toString()}`;
  9. function ssrRegisterHelper(comp, filename) {
  10. const setup = comp.setup;
  11. comp.setup = (props, ctx) => {
  12. const ssrContext = useSSRContext();
  13. (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add(filename);
  14. if (setup) {
  15. return setup(props, ctx);
  16. }
  17. };
  18. }
  19. function vueJsxPlugin(options = {}) {
  20. let root = "";
  21. let needHmr = false;
  22. let needSourceMap = true;
  23. const { include, exclude, babelPlugins = [], ...babelPluginOptions } = options;
  24. const filter = createFilter(include || /\.[jt]sx$/, exclude);
  25. return {
  26. name: "vite:vue-jsx",
  27. config(config) {
  28. return {
  29. // only apply esbuild to ts files
  30. // since we are handling jsx and tsx now
  31. esbuild: {
  32. include: /\.ts$/
  33. },
  34. define: {
  35. __VUE_OPTIONS_API__: config.define?.__VUE_OPTIONS_API__ ?? true,
  36. __VUE_PROD_DEVTOOLS__: config.define?.__VUE_PROD_DEVTOOLS__ ?? false
  37. }
  38. };
  39. },
  40. configResolved(config) {
  41. needHmr = config.command === "serve" && !config.isProduction;
  42. needSourceMap = config.command === "serve" || !!config.build.sourcemap;
  43. root = config.root;
  44. },
  45. resolveId(id) {
  46. if (id === ssrRegisterHelperId) {
  47. return id;
  48. }
  49. },
  50. load(id) {
  51. if (id === ssrRegisterHelperId) {
  52. return ssrRegisterHelperCode;
  53. }
  54. },
  55. async transform(code, id, opt) {
  56. const ssr = opt?.ssr === true;
  57. const [filepath] = id.split("?");
  58. if (filter(id) || filter(filepath)) {
  59. const plugins = [[jsx, babelPluginOptions], ...babelPlugins];
  60. if (id.endsWith(".tsx") || filepath.endsWith(".tsx")) {
  61. plugins.push([
  62. // @ts-ignore missing type
  63. await import('@babel/plugin-transform-typescript').then(
  64. (r) => r.default
  65. ),
  66. // @ts-ignore
  67. { isTSX: true, allowExtensions: true }
  68. ]);
  69. }
  70. if (!ssr && !needHmr) {
  71. plugins.push(() => {
  72. return {
  73. visitor: {
  74. CallExpression: {
  75. enter(_path) {
  76. if (isDefineComponentCall(_path.node)) {
  77. const callee = _path.node.callee;
  78. callee.name = `/* @__PURE__ */ ${callee.name}`;
  79. }
  80. }
  81. }
  82. }
  83. };
  84. });
  85. }
  86. const result = babel.transformSync(code, {
  87. babelrc: false,
  88. ast: true,
  89. plugins,
  90. sourceMaps: needSourceMap,
  91. sourceFileName: id,
  92. configFile: false
  93. });
  94. if (!ssr && !needHmr) {
  95. if (!result.code)
  96. return;
  97. return {
  98. code: result.code,
  99. map: result.map
  100. };
  101. }
  102. const declaredComponents = [];
  103. const hotComponents = [];
  104. let hasDefault = false;
  105. for (const node of result.ast.program.body) {
  106. if (node.type === "VariableDeclaration") {
  107. const names = parseComponentDecls(node);
  108. if (names.length) {
  109. declaredComponents.push(...names);
  110. }
  111. }
  112. if (node.type === "ExportNamedDeclaration") {
  113. if (node.declaration && node.declaration.type === "VariableDeclaration") {
  114. hotComponents.push(
  115. ...parseComponentDecls(node.declaration).map((name) => ({
  116. local: name,
  117. exported: name,
  118. id: getHash(id + name)
  119. }))
  120. );
  121. } else if (node.specifiers.length) {
  122. for (const spec of node.specifiers) {
  123. if (spec.type === "ExportSpecifier" && spec.exported.type === "Identifier") {
  124. const matched = declaredComponents.find(
  125. (name) => name === spec.local.name
  126. );
  127. if (matched) {
  128. hotComponents.push({
  129. local: spec.local.name,
  130. exported: spec.exported.name,
  131. id: getHash(id + spec.exported.name)
  132. });
  133. }
  134. }
  135. }
  136. }
  137. }
  138. if (node.type === "ExportDefaultDeclaration") {
  139. if (node.declaration.type === "Identifier") {
  140. const _name = node.declaration.name;
  141. const matched = declaredComponents.find((name) => name === _name);
  142. if (matched) {
  143. hotComponents.push({
  144. local: _name,
  145. exported: "default",
  146. id: getHash(id + "default")
  147. });
  148. }
  149. } else if (isDefineComponentCall(node.declaration)) {
  150. hasDefault = true;
  151. hotComponents.push({
  152. local: "__default__",
  153. exported: "default",
  154. id: getHash(id + "default")
  155. });
  156. }
  157. }
  158. }
  159. if (hotComponents.length) {
  160. if (hasDefault && (needHmr || ssr)) {
  161. result.code = result.code.replace(
  162. /export default defineComponent/g,
  163. `const __default__ = defineComponent`
  164. ) + `
  165. export default __default__`;
  166. }
  167. if (needHmr && !ssr && !/\?vue&type=script/.test(id)) {
  168. let code2 = result.code;
  169. let callbackCode = ``;
  170. for (const { local, exported, id: id2 } of hotComponents) {
  171. code2 += `
  172. ${local}.__hmrId = "${id2}"
  173. __VUE_HMR_RUNTIME__.createRecord("${id2}", ${local})`;
  174. callbackCode += `
  175. __VUE_HMR_RUNTIME__.reload("${id2}", __${exported})`;
  176. }
  177. const newCompNames = hotComponents.map((c) => `${c.exported}: __${c.exported}`).join(",");
  178. code2 += `
  179. import.meta.hot.accept(({${newCompNames}}) => {${callbackCode}
  180. })`;
  181. result.code = code2;
  182. }
  183. if (ssr) {
  184. const normalizedId = normalizePath(path.relative(root, id));
  185. let ssrInjectCode = `
  186. import { ssrRegisterHelper } from "${ssrRegisterHelperId}"
  187. const __moduleId = ${JSON.stringify(normalizedId)}`;
  188. for (const { local } of hotComponents) {
  189. ssrInjectCode += `
  190. ssrRegisterHelper(${local}, __moduleId)`;
  191. }
  192. result.code += ssrInjectCode;
  193. }
  194. }
  195. if (!result.code)
  196. return;
  197. return {
  198. code: result.code,
  199. map: result.map
  200. };
  201. }
  202. }
  203. };
  204. }
  205. function parseComponentDecls(node) {
  206. const names = [];
  207. for (const decl of node.declarations) {
  208. if (decl.id.type === "Identifier" && isDefineComponentCall(decl.init)) {
  209. names.push(decl.id.name);
  210. }
  211. }
  212. return names;
  213. }
  214. function isDefineComponentCall(node) {
  215. return node && node.type === "CallExpression" && node.callee.type === "Identifier" && node.callee.name === "defineComponent";
  216. }
  217. function getHash(text) {
  218. return createHash("sha256").update(text).digest("hex").substring(0, 8);
  219. }
  220. export { vueJsxPlugin as default };