vite-plugin-vendure-dashboard.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. import { lingui } from '@lingui/vite-plugin';
  2. import tailwindcss from '@tailwindcss/vite';
  3. import { tanstackRouter } from '@tanstack/router-plugin/vite';
  4. import react from '@vitejs/plugin-react';
  5. import path from 'path';
  6. import { PluginOption } from 'vite';
  7. import { PathAdapter } from './types.js';
  8. import { PackageScannerConfig } from './utils/compiler.js';
  9. import { adminApiSchemaPlugin } from './vite-plugin-admin-api-schema.js';
  10. import { configLoaderPlugin } from './vite-plugin-config-loader.js';
  11. import { viteConfigPlugin } from './vite-plugin-config.js';
  12. import { dashboardMetadataPlugin } from './vite-plugin-dashboard-metadata.js';
  13. import { gqlTadaPlugin } from './vite-plugin-gql-tada.js';
  14. import { hmrPlugin } from './vite-plugin-hmr.js';
  15. import { linguiBabelPlugin } from './vite-plugin-lingui-babel.js';
  16. import { dashboardTailwindSourcePlugin } from './vite-plugin-tailwind-source.js';
  17. import { themeVariablesPlugin, ThemeVariablesPluginOptions } from './vite-plugin-theme.js';
  18. import { transformIndexHtmlPlugin } from './vite-plugin-transform-index.js';
  19. import { translationsPlugin } from './vite-plugin-translations.js';
  20. import { uiConfigPlugin, UiConfigPluginOptions } from './vite-plugin-ui-config.js';
  21. /**
  22. * @description
  23. * Options for the {@link vendureDashboardPlugin} Vite plugin.
  24. *
  25. * @docsCategory vite-plugin
  26. * @docsPage vendureDashboardPlugin
  27. * @since 3.4.0
  28. * @docsWeight 1
  29. */
  30. export type VitePluginVendureDashboardOptions = {
  31. /**
  32. * @description
  33. * The path to the Vendure server configuration file.
  34. */
  35. vendureConfigPath: string | URL;
  36. /**
  37. * @description
  38. * The {@link PathAdapter} allows you to customize the resolution of paths
  39. * in the compiled Vendure source code which is used as part of the
  40. * introspection step of building the dashboard.
  41. *
  42. * It enables support for more complex repository structures, such as
  43. * monorepos, where the Vendure server configuration file may not
  44. * be located in the root directory of the project.
  45. *
  46. * If you get compilation errors like "Error loading Vendure config: Cannot find module",
  47. * you probably need to provide a custom `pathAdapter` to resolve the paths correctly.
  48. *
  49. * @example
  50. * ```ts
  51. * vendureDashboardPlugin({
  52. * tempCompilationDir: join(__dirname, './__vendure-dashboard-temp'),
  53. * pathAdapter: {
  54. * getCompiledConfigPath: ({ inputRootDir, outputPath, configFileName }) => {
  55. * const projectName = inputRootDir.split('/libs/')[1].split('/')[0];
  56. * const pathAfterProject = inputRootDir.split(`/libs/${projectName}`)[1];
  57. * const compiledConfigFilePath = `${outputPath}/${projectName}${pathAfterProject}`;
  58. * return path.join(compiledConfigFilePath, configFileName);
  59. * },
  60. * transformTsConfigPathMappings: ({ phase, patterns }) => {
  61. * // "loading" phase is when the compiled Vendure code is being loaded by
  62. * // the plugin, in order to introspect the configuration of your app.
  63. * if (phase === 'loading') {
  64. * return patterns.map((p) =>
  65. * p.replace('libs/', '').replace(/.ts$/, '.js'),
  66. * );
  67. * }
  68. * return patterns;
  69. * },
  70. * },
  71. * // ...
  72. * }),
  73. * ```
  74. */
  75. pathAdapter?: PathAdapter;
  76. /**
  77. * @description
  78. * The name of the exported variable from the Vendure server configuration file, e.g. `config`.
  79. * This is only required if the plugin is unable to auto-detect the name of the exported variable.
  80. */
  81. vendureConfigExport?: string;
  82. /**
  83. * @description
  84. * The path to the directory where the generated GraphQL Tada files will be output.
  85. */
  86. gqlOutputPath?: string;
  87. tempCompilationDir?: string;
  88. /**
  89. * @description
  90. * Allows you to customize the location of node_modules & glob patterns used to scan for potential
  91. * Vendure plugins installed as npm packages. If not provided, the compiler will attempt to guess
  92. * the location based on the location of the `@vendure/core` package.
  93. */
  94. pluginPackageScanner?: PackageScannerConfig;
  95. /**
  96. * @description
  97. * Allows you to specify the module system to use when compiling and loading your Vendure config.
  98. * By default, the compiler will use CommonJS, but you can set it to `esm` if you are using
  99. * ES Modules in your Vendure project.
  100. *
  101. * **Status** Developer preview. If you are using ESM please try this out and provide us with feedback!
  102. *
  103. * @since 3.5.1
  104. * @default 'commonjs'
  105. */
  106. module?: 'commonjs' | 'esm';
  107. /**
  108. * @description
  109. * Allows you to selectively disable individual plugins.
  110. * @example
  111. * ```ts
  112. * vendureDashboardPlugin({
  113. * vendureConfigPath: './config.ts',
  114. * disablePlugins: {
  115. * react: true,
  116. * lingui: true,
  117. * }
  118. * })
  119. * ```
  120. */
  121. disablePlugins?: {
  122. tanstackRouter?: boolean;
  123. linguiBabel?: boolean;
  124. react?: boolean;
  125. lingui?: boolean;
  126. themeVariables?: boolean;
  127. tailwindSource?: boolean;
  128. tailwindcss?: boolean;
  129. configLoader?: boolean;
  130. viteConfig?: boolean;
  131. adminApiSchema?: boolean;
  132. dashboardMetadata?: boolean;
  133. uiConfig?: boolean;
  134. gqlTada?: boolean;
  135. transformIndexHtml?: boolean;
  136. translations?: boolean;
  137. hmr?: boolean;
  138. };
  139. } & UiConfigPluginOptions &
  140. ThemeVariablesPluginOptions;
  141. /**
  142. * @description
  143. * This is a Vite plugin which configures a set of plugins required to build the Vendure Dashboard.
  144. */
  145. type PluginKey = keyof NonNullable<VitePluginVendureDashboardOptions['disablePlugins']>;
  146. type PluginMapEntry = {
  147. key: PluginKey;
  148. plugin: () => PluginOption | PluginOption[] | false | '';
  149. };
  150. /**
  151. * @description
  152. * This is the Vite plugin which powers the Vendure Dashboard, including:
  153. *
  154. * - Configuring routing, styling and React support
  155. * - Analyzing your VendureConfig file and introspecting your schema
  156. * - Loading your custom Dashboard extensions
  157. *
  158. * @docsCategory vite-plugin
  159. * @docsPage vendureDashboardPlugin
  160. * @since 3.4.0
  161. * @docsWeight 0
  162. */
  163. export function vendureDashboardPlugin(options: VitePluginVendureDashboardOptions): PluginOption[] {
  164. const tempDir = options.tempCompilationDir ?? path.join(import.meta.dirname, './.vendure-dashboard-temp');
  165. const normalizedVendureConfigPath = getNormalizedVendureConfigPath(options.vendureConfigPath);
  166. const packageRoot = getDashboardPackageRoot();
  167. const linguiConfigPath = path.join(packageRoot, 'lingui.config.js');
  168. const disabled = options.disablePlugins ?? {};
  169. if (process.env.IS_LOCAL_DEV !== 'true') {
  170. process.env.LINGUI_CONFIG = linguiConfigPath;
  171. }
  172. const pluginMap: PluginMapEntry[] = [
  173. {
  174. key: 'tanstackRouter',
  175. plugin: () =>
  176. tanstackRouter({
  177. autoCodeSplitting: true,
  178. routeFileIgnorePattern: '.graphql.ts|components|hooks|utils',
  179. routesDirectory: path.join(packageRoot, 'src/app/routes'),
  180. generatedRouteTree: path.join(packageRoot, 'src/app/routeTree.gen.ts'),
  181. }),
  182. },
  183. {
  184. // Custom plugin that transforms Lingui macros using Babel instead of SWC.
  185. // This runs BEFORE the react plugin to ensure macros are transformed first.
  186. // Using Babel eliminates the SWC binary compatibility issues that caused
  187. // "failed to invoke plugin" errors in external projects.
  188. // See: https://github.com/vendurehq/vendure/issues/3929
  189. key: 'linguiBabel',
  190. plugin: () => linguiBabelPlugin(),
  191. },
  192. {
  193. key: 'react',
  194. plugin: () => react(),
  195. },
  196. {
  197. key: 'lingui',
  198. plugin: () => lingui({}),
  199. },
  200. {
  201. key: 'themeVariables',
  202. plugin: () => themeVariablesPlugin({ theme: options.theme }),
  203. },
  204. {
  205. key: 'tailwindSource',
  206. plugin: () => dashboardTailwindSourcePlugin(),
  207. },
  208. {
  209. key: 'tailwindcss',
  210. plugin: () => tailwindcss(),
  211. },
  212. {
  213. key: 'configLoader',
  214. plugin: () =>
  215. configLoaderPlugin({
  216. vendureConfigPath: normalizedVendureConfigPath,
  217. outputPath: tempDir,
  218. pathAdapter: options.pathAdapter,
  219. pluginPackageScanner: options.pluginPackageScanner,
  220. module: options.module,
  221. }),
  222. },
  223. {
  224. key: 'viteConfig',
  225. plugin: () => viteConfigPlugin({ packageRoot }),
  226. },
  227. {
  228. key: 'adminApiSchema',
  229. plugin: () => adminApiSchemaPlugin(),
  230. },
  231. {
  232. key: 'dashboardMetadata',
  233. plugin: () => dashboardMetadataPlugin(),
  234. },
  235. {
  236. key: 'uiConfig',
  237. plugin: () => uiConfigPlugin(options),
  238. },
  239. {
  240. key: 'gqlTada',
  241. plugin: () =>
  242. options.gqlOutputPath &&
  243. gqlTadaPlugin({ gqlTadaOutputPath: options.gqlOutputPath, tempDir, packageRoot }),
  244. },
  245. {
  246. key: 'transformIndexHtml',
  247. plugin: () => transformIndexHtmlPlugin(),
  248. },
  249. {
  250. key: 'translations',
  251. plugin: () =>
  252. translationsPlugin({
  253. packageRoot,
  254. }),
  255. },
  256. {
  257. key: 'hmr',
  258. plugin: () => hmrPlugin(),
  259. },
  260. ];
  261. const plugins: PluginOption[] = [];
  262. for (const entry of pluginMap) {
  263. if (!disabled[entry.key]) {
  264. const plugin = entry.plugin();
  265. if (plugin) {
  266. if (Array.isArray(plugin)) {
  267. plugins.push(...plugin);
  268. } else {
  269. plugins.push(plugin);
  270. }
  271. }
  272. }
  273. }
  274. return plugins;
  275. }
  276. /**
  277. * @description
  278. * Returns the path to the root of the `@vendure/dashboard` package.
  279. */
  280. function getDashboardPackageRoot(): string {
  281. const fileUrl = import.meta.resolve('@vendure/dashboard');
  282. const packagePath = fileUrl.startsWith('file:') ? new URL(fileUrl).pathname : fileUrl;
  283. return fixWindowsPath(path.join(packagePath, '../../../'));
  284. }
  285. /**
  286. * Get the normalized path to the Vendure config file given either a string or URL.
  287. */
  288. export function getNormalizedVendureConfigPath(vendureConfigPath: string | URL): string {
  289. const stringPath = typeof vendureConfigPath === 'string' ? vendureConfigPath : vendureConfigPath.href;
  290. if (stringPath.startsWith('file:')) {
  291. return fixWindowsPath(new URL(stringPath).pathname);
  292. }
  293. return fixWindowsPath(stringPath);
  294. }
  295. function fixWindowsPath(filePath: string): string {
  296. // Fix Windows paths that might start with a leading slash
  297. if (process.platform === 'win32') {
  298. // Remove leading slash before drive letter on Windows
  299. if (/^[/\\][A-Za-z]:/.test(filePath)) {
  300. return filePath.substring(1);
  301. }
  302. }
  303. return filePath;
  304. }