vite-plugin-lingui-babel.ts 4.0 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. import * as babel from '@babel/core';
  2. import type { Plugin } from 'vite';
  3. /**
  4. * @description
  5. * A custom Vite plugin that transforms Lingui macros in files using Babel instead of SWC.
  6. *
  7. * This plugin solves a critical compatibility issue with SWC plugins:
  8. * - SWC plugins are compiled Wasm binaries that require exact version matching with `@swc/core`
  9. * - When users have different SWC versions in their projects (e.g., from Next.js, Nx, etc.),
  10. * the Lingui SWC plugin fails with "failed to invoke plugin" errors
  11. * - Babel has no such binary compatibility issues, making it much more reliable for library code
  12. *
  13. * The plugin runs BEFORE `@vitejs/plugin-react` and transforms files containing Lingui macros
  14. * (imports from `@lingui/core/macro` or `@lingui/react/macro`) using the Babel-based
  15. * `@lingui/babel-plugin-lingui-macro`.
  16. *
  17. * Files processed:
  18. * - `@vendure/dashboard/src` files (in node_modules for external projects)
  19. * - `packages/dashboard/src` files (in monorepo development)
  20. * - User's dashboard extension files (e.g., custom plugins using Lingui)
  21. *
  22. * Files NOT processed:
  23. * - Other node_modules packages (they shouldn't contain Lingui macros)
  24. *
  25. * @see https://github.com/vendurehq/vendure/issues/3929
  26. * @see https://github.com/lingui/swc-plugin/issues/179
  27. */
  28. export function linguiBabelPlugin(): Plugin {
  29. return {
  30. name: 'vendure:lingui-babel',
  31. // Run BEFORE @vitejs/plugin-react so the macros are already transformed
  32. // when the react plugin processes the file
  33. enforce: 'pre',
  34. async transform(code, id) {
  35. // Strip query params for path matching (Vite adds ?v=xxx for cache busting)
  36. const cleanId = id.split('?')[0];
  37. // Only process TypeScript/JavaScript files
  38. if (!/\.[tj]sx?$/.test(cleanId)) {
  39. return null;
  40. }
  41. // Only process files that actually contain Lingui macro imports
  42. // This is a fast check to avoid running Babel on files that don't need it
  43. if (!code.includes('@lingui/') || !code.includes('/macro')) {
  44. return null;
  45. }
  46. // Skip node_modules files EXCEPT for @vendure/dashboard source
  47. // This ensures:
  48. // 1. Dashboard source files get transformed (both in monorepo and external projects)
  49. // 2. User's extension files get transformed (not in node_modules)
  50. // 3. Other node_modules packages are left alone
  51. if (cleanId.includes('node_modules')) {
  52. const isVendureDashboard =
  53. cleanId.includes('@vendure/dashboard/src') || cleanId.includes('packages/dashboard/src');
  54. if (!isVendureDashboard) {
  55. return null;
  56. }
  57. }
  58. try {
  59. const result = await babel.transformAsync(code, {
  60. filename: id,
  61. presets: [
  62. ['@babel/preset-typescript', { isTSX: true, allExtensions: true }],
  63. ['@babel/preset-react', { runtime: 'automatic' }],
  64. ],
  65. plugins: ['@lingui/babel-plugin-lingui-macro'],
  66. sourceMaps: true,
  67. // Don't look for babel config files - we want to control the config completely
  68. configFile: false,
  69. babelrc: false,
  70. });
  71. if (!result?.code) {
  72. return null;
  73. }
  74. return {
  75. code: result.code,
  76. map: result.map,
  77. };
  78. } catch (error) {
  79. // Log the error but don't crash - let the build continue
  80. // The lingui vite plugin will catch untransformed macros later
  81. // eslint-disable-next-line no-console
  82. console.error(`[vendure:lingui-babel] Failed to transform ${id}:`, error);
  83. return null;
  84. }
  85. },
  86. };
  87. }