index.mjs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500
  1. import path from 'node:path';
  2. import * as babel from '@babel/core';
  3. import { createFilter, normalizePath } from 'vite';
  4. import MagicString from 'magic-string';
  5. import fs from 'node:fs';
  6. import { createRequire } from 'node:module';
  7. const runtimePublicPath = "/@react-refresh";
  8. const _require = createRequire(import.meta.url);
  9. const reactRefreshDir = path.dirname(
  10. _require.resolve("react-refresh/package.json")
  11. );
  12. const runtimeFilePath = path.join(
  13. reactRefreshDir,
  14. "cjs/react-refresh-runtime.development.js"
  15. );
  16. const runtimeCode = `
  17. const exports = {}
  18. ${fs.readFileSync(runtimeFilePath, "utf-8")}
  19. function debounce(fn, delay) {
  20. let handle
  21. return () => {
  22. clearTimeout(handle)
  23. handle = setTimeout(fn, delay)
  24. }
  25. }
  26. exports.performReactRefresh = debounce(exports.performReactRefresh, 16)
  27. export default exports
  28. `;
  29. const preambleCode = `
  30. import RefreshRuntime from "__BASE__${runtimePublicPath.slice(1)}"
  31. RefreshRuntime.injectIntoGlobalHook(window)
  32. window.$RefreshReg$ = () => {}
  33. window.$RefreshSig$ = () => (type) => type
  34. window.__vite_plugin_react_preamble_installed__ = true
  35. `;
  36. const header = `
  37. import RefreshRuntime from "${runtimePublicPath}";
  38. let prevRefreshReg;
  39. let prevRefreshSig;
  40. if (import.meta.hot) {
  41. if (!window.__vite_plugin_react_preamble_installed__) {
  42. throw new Error(
  43. "@vitejs/plugin-react can't detect preamble. Something is wrong. " +
  44. "See https://github.com/vitejs/vite-plugin-react/pull/11#discussion_r430879201"
  45. );
  46. }
  47. prevRefreshReg = window.$RefreshReg$;
  48. prevRefreshSig = window.$RefreshSig$;
  49. window.$RefreshReg$ = (type, id) => {
  50. RefreshRuntime.register(type, __SOURCE__ + " " + id)
  51. };
  52. window.$RefreshSig$ = RefreshRuntime.createSignatureFunctionForTransform;
  53. }`.replace(/[\n]+/gm, "");
  54. const timeout = `
  55. if (!window.__vite_plugin_react_timeout) {
  56. window.__vite_plugin_react_timeout = setTimeout(() => {
  57. window.__vite_plugin_react_timeout = 0;
  58. RefreshRuntime.performReactRefresh();
  59. }, 30);
  60. }
  61. `;
  62. const footer = `
  63. if (import.meta.hot) {
  64. window.$RefreshReg$ = prevRefreshReg;
  65. window.$RefreshSig$ = prevRefreshSig;
  66. __ACCEPT__
  67. }`;
  68. const checkAndAccept = `
  69. function isReactRefreshBoundary(mod) {
  70. if (mod == null || typeof mod !== 'object') {
  71. return false;
  72. }
  73. let hasExports = false;
  74. let areAllExportsComponents = true;
  75. for (const exportName in mod) {
  76. hasExports = true;
  77. if (exportName === '__esModule') {
  78. continue;
  79. }
  80. const desc = Object.getOwnPropertyDescriptor(mod, exportName);
  81. if (desc && desc.get) {
  82. // Don't invoke getters as they may have side effects.
  83. return false;
  84. }
  85. const exportValue = mod[exportName];
  86. if (!RefreshRuntime.isLikelyComponentType(exportValue)) {
  87. areAllExportsComponents = false;
  88. }
  89. }
  90. return hasExports && areAllExportsComponents;
  91. }
  92. import.meta.hot.accept(mod => {
  93. if (isReactRefreshBoundary(mod)) {
  94. ${timeout}
  95. } else {
  96. import.meta.hot.invalidate();
  97. }
  98. });
  99. `;
  100. function addRefreshWrapper(code, id, accept) {
  101. return header.replace("__SOURCE__", JSON.stringify(id)) + code + footer.replace("__ACCEPT__", accept ? checkAndAccept : timeout);
  102. }
  103. function isRefreshBoundary(ast) {
  104. return ast.program.body.every((node) => {
  105. if (node.type !== "ExportNamedDeclaration") {
  106. return true;
  107. }
  108. const { declaration, specifiers } = node;
  109. if (declaration) {
  110. if (declaration.type === "ClassDeclaration")
  111. return false;
  112. if (declaration.type === "VariableDeclaration") {
  113. return declaration.declarations.every(
  114. (variable) => isComponentLikeIdentifier(variable.id)
  115. );
  116. }
  117. if (declaration.type === "FunctionDeclaration") {
  118. return !!declaration.id && isComponentLikeIdentifier(declaration.id);
  119. }
  120. }
  121. return specifiers.every((spec) => {
  122. return isComponentLikeIdentifier(spec.exported);
  123. });
  124. });
  125. }
  126. function isComponentLikeIdentifier(node) {
  127. return node.type === "Identifier" && isComponentLikeName(node.name);
  128. }
  129. function isComponentLikeName(name) {
  130. return typeof name === "string" && name[0] >= "A" && name[0] <= "Z";
  131. }
  132. function babelImportToRequire({ types: t }) {
  133. return {
  134. visitor: {
  135. ImportDeclaration(path) {
  136. const decl = path.node;
  137. const spec = decl.specifiers[0];
  138. path.replaceWith(
  139. t.variableDeclaration("var", [
  140. t.variableDeclarator(
  141. spec.local,
  142. t.memberExpression(
  143. t.callExpression(t.identifier("require"), [decl.source]),
  144. spec.imported
  145. )
  146. )
  147. ])
  148. );
  149. }
  150. }
  151. };
  152. }
  153. let babelRestoreJSX;
  154. const jsxNotFound = [null, false];
  155. async function getBabelRestoreJSX() {
  156. if (!babelRestoreJSX)
  157. babelRestoreJSX = import('./chunks/babel-restore-jsx.mjs').then((r) => {
  158. const fn = r.default;
  159. if ("default" in fn)
  160. return fn.default;
  161. return fn;
  162. });
  163. return babelRestoreJSX;
  164. }
  165. async function restoreJSX(babel, code, filename) {
  166. const [reactAlias, isCommonJS] = parseReactAlias(code);
  167. if (!reactAlias) {
  168. return jsxNotFound;
  169. }
  170. const reactJsxRE = new RegExp(
  171. `\\b${reactAlias}\\.(createElement|Fragment)\\b`,
  172. "g"
  173. );
  174. if (!reactJsxRE.test(code)) {
  175. return jsxNotFound;
  176. }
  177. const result = await babel.transformAsync(code, {
  178. babelrc: false,
  179. configFile: false,
  180. ast: true,
  181. code: false,
  182. filename,
  183. parserOpts: {
  184. plugins: ["jsx"]
  185. },
  186. plugins: [[await getBabelRestoreJSX(), { reactAlias }]]
  187. });
  188. return [result?.ast, isCommonJS];
  189. }
  190. function parseReactAlias(code) {
  191. let match = code.match(
  192. /\b(var|let|const)\s+([^=\{\s]+)\s*=\s*require\(["']react["']\)/
  193. );
  194. if (match) {
  195. return [match[2], true];
  196. }
  197. match = code.match(/^import\s+(?:\*\s+as\s+)?(\w+).+?\bfrom\s*["']react["']/m);
  198. if (match) {
  199. return [match[1], false];
  200. }
  201. return [void 0, false];
  202. }
  203. const prependReactImportCode = "import React from 'react'; ";
  204. function viteReact(opts = {}) {
  205. let devBase = "/";
  206. let resolvedCacheDir;
  207. let filter = createFilter(opts.include, opts.exclude);
  208. let needHiresSourcemap = false;
  209. let isProduction = true;
  210. let projectRoot = process.cwd();
  211. let skipFastRefresh = opts.fastRefresh === false;
  212. let skipReactImport = false;
  213. let runPluginOverrides = (options, context) => false;
  214. let staticBabelOptions;
  215. const useAutomaticRuntime = opts.jsxRuntime !== "classic";
  216. const importReactRE = /(^|\n)import\s+(\*\s+as\s+)?React(,|\s+)/;
  217. const fileExtensionRE = /\.[^\/\s\?]+$/;
  218. const viteBabel = {
  219. name: "vite:react-babel",
  220. enforce: "pre",
  221. config() {
  222. if (opts.jsxRuntime === "classic") {
  223. return {
  224. esbuild: {
  225. logOverride: {
  226. "this-is-undefined-in-esm": "silent"
  227. }
  228. }
  229. };
  230. }
  231. },
  232. configResolved(config) {
  233. devBase = config.base;
  234. projectRoot = config.root;
  235. resolvedCacheDir = normalizePath(path.resolve(config.cacheDir));
  236. filter = createFilter(opts.include, opts.exclude, {
  237. resolve: projectRoot
  238. });
  239. needHiresSourcemap = config.command === "build" && !!config.build.sourcemap;
  240. isProduction = config.isProduction;
  241. skipFastRefresh || (skipFastRefresh = isProduction || config.command === "build");
  242. const jsxInject = config.esbuild && config.esbuild.jsxInject;
  243. if (jsxInject && importReactRE.test(jsxInject)) {
  244. skipReactImport = true;
  245. config.logger.warn(
  246. "[@vitejs/plugin-react] This plugin imports React for you automatically, so you can stop using `esbuild.jsxInject` for that purpose."
  247. );
  248. }
  249. config.plugins.forEach((plugin) => {
  250. const hasConflict = plugin.name === "react-refresh" || plugin !== viteReactJsx && plugin.name === "vite:react-jsx";
  251. if (hasConflict)
  252. return config.logger.warn(
  253. `[@vitejs/plugin-react] You should stop using "${plugin.name}" since this plugin conflicts with it.`
  254. );
  255. });
  256. runPluginOverrides = (babelOptions, context) => {
  257. const hooks = config.plugins.map((plugin) => plugin.api?.reactBabel).filter(Boolean);
  258. if (hooks.length > 0) {
  259. return (runPluginOverrides = (babelOptions2, context2) => {
  260. hooks.forEach((hook) => hook(babelOptions2, context2, config));
  261. return true;
  262. })(babelOptions, context);
  263. }
  264. runPluginOverrides = () => false;
  265. return false;
  266. };
  267. },
  268. async transform(code, id, options) {
  269. const ssr = options?.ssr === true;
  270. const [filepath, querystring = ""] = id.split("?");
  271. const [extension = ""] = querystring.match(fileExtensionRE) || filepath.match(fileExtensionRE) || [];
  272. if (/\.(mjs|[tj]sx?)$/.test(extension)) {
  273. const isJSX = extension.endsWith("x");
  274. const isNodeModules = id.includes("/node_modules/");
  275. const isProjectFile = !isNodeModules && (id[0] === "\0" || id.startsWith(projectRoot + "/"));
  276. let babelOptions = staticBabelOptions;
  277. if (typeof opts.babel === "function") {
  278. const rawOptions = opts.babel(id, { ssr });
  279. babelOptions = createBabelOptions(rawOptions);
  280. runPluginOverrides(babelOptions, { ssr, id });
  281. } else if (!babelOptions) {
  282. babelOptions = createBabelOptions(opts.babel);
  283. if (!runPluginOverrides(babelOptions, { ssr, id })) {
  284. staticBabelOptions = babelOptions;
  285. }
  286. }
  287. const plugins = isProjectFile ? [...babelOptions.plugins] : [];
  288. let useFastRefresh = false;
  289. if (!skipFastRefresh && !ssr && !isNodeModules) {
  290. const isReactModule = isJSX || importReactRE.test(code);
  291. if (isReactModule && filter(id)) {
  292. useFastRefresh = true;
  293. plugins.push([
  294. await loadPlugin("react-refresh/babel"),
  295. { skipEnvCheck: true }
  296. ]);
  297. }
  298. }
  299. let ast;
  300. let prependReactImport = false;
  301. if (!isProjectFile || isJSX) {
  302. if (useAutomaticRuntime) {
  303. const isOptimizedReactDom = id.startsWith(resolvedCacheDir) && id.includes("/react-dom.js");
  304. const [restoredAst, isCommonJS] = !isProjectFile && !isJSX && !isOptimizedReactDom ? await restoreJSX(babel, code, id) : [null, false];
  305. if (isJSX || (ast = restoredAst)) {
  306. plugins.push([
  307. await loadPlugin(
  308. "@babel/plugin-transform-react-jsx" + (isProduction ? "" : "-development")
  309. ),
  310. {
  311. runtime: "automatic",
  312. importSource: opts.jsxImportSource,
  313. pure: opts.jsxPure !== false,
  314. throwIfNamespace: opts.jsxThrowIfNamespace
  315. }
  316. ]);
  317. if (isCommonJS) {
  318. plugins.push(babelImportToRequire);
  319. }
  320. }
  321. } else if (isProjectFile) {
  322. if (!isProduction) {
  323. plugins.push(
  324. await loadPlugin("@babel/plugin-transform-react-jsx-self"),
  325. await loadPlugin("@babel/plugin-transform-react-jsx-source")
  326. );
  327. }
  328. if (!skipReactImport && !importReactRE.test(code)) {
  329. prependReactImport = true;
  330. }
  331. }
  332. }
  333. let inputMap;
  334. if (prependReactImport) {
  335. if (needHiresSourcemap) {
  336. const s = new MagicString(code);
  337. s.prepend(prependReactImportCode);
  338. code = s.toString();
  339. inputMap = s.generateMap({ hires: true, source: id });
  340. } else {
  341. code = prependReactImportCode + code;
  342. }
  343. }
  344. const shouldSkip = !plugins.length && !babelOptions.configFile && !(isProjectFile && babelOptions.babelrc);
  345. if (shouldSkip) {
  346. return {
  347. code,
  348. map: inputMap ?? null
  349. };
  350. }
  351. const parserPlugins = [
  352. ...babelOptions.parserOpts.plugins,
  353. "importMeta",
  354. "topLevelAwait",
  355. "classProperties",
  356. "classPrivateProperties",
  357. "classPrivateMethods"
  358. ];
  359. if (!extension.endsWith(".ts")) {
  360. parserPlugins.push("jsx");
  361. }
  362. if (/\.tsx?$/.test(extension)) {
  363. parserPlugins.push("typescript");
  364. }
  365. const transformAsync = ast ? babel.transformFromAstAsync.bind(babel, ast, code) : babel.transformAsync.bind(babel, code);
  366. const isReasonReact = extension.endsWith(".bs.js");
  367. const result = await transformAsync({
  368. ...babelOptions,
  369. ast: !isReasonReact,
  370. root: projectRoot,
  371. filename: id,
  372. sourceFileName: filepath,
  373. parserOpts: {
  374. ...babelOptions.parserOpts,
  375. sourceType: "module",
  376. allowAwaitOutsideFunction: true,
  377. plugins: parserPlugins
  378. },
  379. generatorOpts: {
  380. ...babelOptions.generatorOpts,
  381. decoratorsBeforeExport: true
  382. },
  383. plugins,
  384. sourceMaps: true,
  385. inputSourceMap: inputMap ?? false
  386. });
  387. if (result) {
  388. let code2 = result.code;
  389. if (useFastRefresh && /\$RefreshReg\$\(/.test(code2)) {
  390. const accept = isReasonReact || isRefreshBoundary(result.ast);
  391. code2 = addRefreshWrapper(code2, id, accept);
  392. }
  393. return {
  394. code: code2,
  395. map: result.map
  396. };
  397. }
  398. }
  399. }
  400. };
  401. const viteReactRefresh = {
  402. name: "vite:react-refresh",
  403. enforce: "pre",
  404. config: () => ({
  405. resolve: {
  406. dedupe: ["react", "react-dom"]
  407. }
  408. }),
  409. resolveId(id) {
  410. if (id === runtimePublicPath) {
  411. return id;
  412. }
  413. },
  414. load(id) {
  415. if (id === runtimePublicPath) {
  416. return runtimeCode;
  417. }
  418. },
  419. transformIndexHtml() {
  420. if (!skipFastRefresh)
  421. return [
  422. {
  423. tag: "script",
  424. attrs: { type: "module" },
  425. children: preambleCode.replace(`__BASE__`, devBase)
  426. }
  427. ];
  428. }
  429. };
  430. const reactJsxRuntimeId = "react/jsx-runtime";
  431. const reactJsxDevRuntimeId = "react/jsx-dev-runtime";
  432. const virtualReactJsxRuntimeId = "\0" + reactJsxRuntimeId;
  433. const virtualReactJsxDevRuntimeId = "\0" + reactJsxDevRuntimeId;
  434. const viteReactJsx = {
  435. name: "vite:react-jsx",
  436. enforce: "pre",
  437. config() {
  438. return {
  439. optimizeDeps: {
  440. include: [reactJsxRuntimeId, reactJsxDevRuntimeId, "react"]
  441. }
  442. };
  443. },
  444. resolveId(id, importer) {
  445. if (id === reactJsxRuntimeId && importer !== virtualReactJsxRuntimeId) {
  446. return virtualReactJsxRuntimeId;
  447. }
  448. if (id === reactJsxDevRuntimeId && importer !== virtualReactJsxDevRuntimeId) {
  449. return virtualReactJsxDevRuntimeId;
  450. }
  451. },
  452. load(id) {
  453. if (id === virtualReactJsxRuntimeId) {
  454. return [
  455. `import * as jsxRuntime from ${JSON.stringify(reactJsxRuntimeId)}`,
  456. `export const Fragment = jsxRuntime.Fragment`,
  457. `export const jsx = jsxRuntime.jsx`,
  458. `export const jsxs = jsxRuntime.jsxs`
  459. ].join("\n");
  460. }
  461. if (id === virtualReactJsxDevRuntimeId) {
  462. return [
  463. `import * as jsxRuntime from ${JSON.stringify(reactJsxDevRuntimeId)}`,
  464. `export const Fragment = jsxRuntime.Fragment`,
  465. `export const jsxDEV = jsxRuntime.jsxDEV`
  466. ].join("\n");
  467. }
  468. }
  469. };
  470. return [viteBabel, viteReactRefresh, useAutomaticRuntime && viteReactJsx];
  471. }
  472. viteReact.preambleCode = preambleCode;
  473. function loadPlugin(path2) {
  474. return import(path2).then((module) => module.default || module);
  475. }
  476. function createBabelOptions(rawOptions) {
  477. var _a;
  478. const babelOptions = {
  479. babelrc: false,
  480. configFile: false,
  481. ...rawOptions
  482. };
  483. babelOptions.plugins || (babelOptions.plugins = []);
  484. babelOptions.presets || (babelOptions.presets = []);
  485. babelOptions.overrides || (babelOptions.overrides = []);
  486. babelOptions.parserOpts || (babelOptions.parserOpts = {});
  487. (_a = babelOptions.parserOpts).plugins || (_a.plugins = []);
  488. return babelOptions;
  489. }
  490. export { viteReact as default };