/** * Shared Vite Plugins for Burdenoff Microfrontends and App Shells * * @description Common plugins used across all Vite configurations * @version Vite 8.0.0-beta.8+ */ import type { Plugin } from 'vite'; import { resolve } from 'path'; interface ContextualAliasOptions { /** Path to fe-libs/src */ feLibsPath: string; /** Path to the current project's src directory */ srcPath: string; /** Map of microfrontend names to their src paths (for app shells) */ microfePaths?: Record; } /** * Creates a plugin that contextually resolves @/ imports based on the importer location. * * This allows: * - Files in fe-libs to use @/ to reference fe-libs/src * - Files in a microfrontend to use @/ to reference that microfrontend's src * - Files in an app shell to use @/ to reference the app's src * * @example * ```ts * plugins: [ * createContextualAliasPlugin({ * feLibsPath: resolve(__dirname, '../../fe-libs/src'), * srcPath: resolve(__dirname, './src'), * }), * ] * ``` */ export function createContextualAliasPlugin(options: ContextualAliasOptions): Plugin { const { feLibsPath, srcPath, microfePaths = {} } = options; return { name: 'burdenoff:contextual-alias', enforce: 'pre', async resolveId(source, importer, resolveOptions) { // Only handle @/ imports if (!source.startsWith('@/')) { return null; } // Need an importer to determine context if (!importer) { return null; } const importPath = source.slice(2); // Remove '@/' let basePath: string; // Determine base path based on importer location if (importer.includes('fe-libs')) { // Importer is from fe-libs, resolve to fe-libs/src basePath = feLibsPath; } else { // Check if importer is from any microfrontend let foundMicrofe = false; for (const [name, path] of Object.entries(microfePaths)) { if (importer.includes(name)) { basePath = path; foundMicrofe = true; break; } } // Otherwise resolve to project src if (!foundMicrofe) { basePath = srcPath; } } const resolvedPath = resolve(basePath!, importPath); // Let Vite resolve the actual file (handles .ts, .tsx, /index.ts etc.) const resolution = await this.resolve(resolvedPath, importer, { skipSelf: true, ...resolveOptions, }); return resolution; }, }; } /** * Creates a plugin that fixes adaptive-expressions browser bundle issues. * Only needed for bigconsole-app and similar apps using adaptive cards. * * @example * ```ts * plugins: [createAdaptiveExpressionsFix()] * ``` */ export function createAdaptiveExpressionsFix(): Plugin { return { name: 'burdenoff:adaptive-expressions-fix', enforce: 'pre', resolveId(source) { // Force correct resolution for adaptive-expressions if (source === 'adaptive-expressions') { return { id: 'adaptive-expressions/lib/index.js', external: false, }; } return null; }, }; } /** * Plugin to ensure single React instance across linked workspaces. * Prevents "Invalid hook call" errors in monorepo setups. * * @param nodeModulesPath - Path to the app's node_modules directory * * @example * ```ts * plugins: [createReactSingletonPlugin(resolve(__dirname, 'node_modules'))] * ``` */ export function createReactSingletonAliases(nodeModulesPath: string): Record { return { react: resolve(nodeModulesPath, 'react'), 'react-dom': resolve(nodeModulesPath, 'react-dom'), 'react-dom/client': resolve(nodeModulesPath, 'react-dom/client'), 'react/jsx-runtime': resolve(nodeModulesPath, 'react/jsx-runtime'), 'react/jsx-dev-runtime': resolve(nodeModulesPath, 'react/jsx-dev-runtime'), }; } /** * Creates aliases for Node.js polyfill shims from fe-libs. * Used by app shells that need browser shims for assert, process, util, etc. * * @param feLibsShimsPath - Path to fe-libs/src/shared/shims directory * @param srcPath - Path to the app's src directory (for use-sync-external-store overrides) */ export function createNodeShimAliases( feLibsShimsPath: string, srcPath?: string ): Record { const shimsPath = srcPath ?? feLibsShimsPath; return { assert: resolve(feLibsShimsPath, 'assert.ts'), util: resolve(feLibsShimsPath, 'util.ts'), process: resolve(feLibsShimsPath, 'process.ts'), 'use-sync-external-store/shim': resolve(shimsPath, 'use-sync-external-store-shim.ts'), 'use-sync-external-store/shim/index': resolve(shimsPath, 'use-sync-external-store-shim.ts'), 'use-sync-external-store/shim/index.js': resolve(shimsPath, 'use-sync-external-store-shim.ts'), 'use-sync-external-store/with-selector': resolve( shimsPath, 'use-sync-external-store-with-selector.ts' ), 'use-sync-external-store/with-selector.js': resolve( shimsPath, 'use-sync-external-store-with-selector.ts' ), 'use-sync-external-store/shim/with-selector': resolve( shimsPath, 'use-sync-external-store-with-selector.ts' ), 'use-sync-external-store/shim/with-selector.js': resolve( shimsPath, 'use-sync-external-store-with-selector.ts' ), }; } /** * Zustand ESM fix plugin. * Maps zustand imports to ESM entries since zustand v5 ships as CJS. * * @param nodeModulesPath - Path to the app's node_modules * @param shimsPath - Path to the shims directory for use-sync-external-store */ export function createZustandEsmPlugin(nodeModulesPath: string, shimsPath: string): Plugin { const zustandEsmMap: Record = { zustand: resolve(nodeModulesPath, 'zustand/esm/index.mjs'), 'zustand/vanilla': resolve(nodeModulesPath, 'zustand/esm/vanilla.mjs'), 'zustand/middleware': resolve(nodeModulesPath, 'zustand/esm/middleware.mjs'), 'zustand/middleware/immer': resolve(nodeModulesPath, 'zustand/esm/middleware/immer.mjs'), 'zustand/shallow': resolve(nodeModulesPath, 'zustand/esm/shallow.mjs'), 'zustand/vanilla/shallow': resolve(nodeModulesPath, 'zustand/esm/vanilla/shallow.mjs'), 'zustand/react': resolve(nodeModulesPath, 'zustand/esm/react.mjs'), 'zustand/react/shallow': resolve(nodeModulesPath, 'zustand/esm/react/shallow.mjs'), 'zustand/traditional': resolve(nodeModulesPath, 'zustand/esm/traditional.mjs'), 'use-sync-external-store/shim': resolve(shimsPath, 'use-sync-external-store-shim.ts'), 'use-sync-external-store/shim/index': resolve(shimsPath, 'use-sync-external-store-shim.ts'), 'use-sync-external-store/shim/index.js': resolve(shimsPath, 'use-sync-external-store-shim.ts'), 'use-sync-external-store/with-selector': resolve( shimsPath, 'use-sync-external-store-with-selector.ts' ), 'use-sync-external-store/with-selector.js': resolve( shimsPath, 'use-sync-external-store-with-selector.ts' ), 'use-sync-external-store/shim/with-selector': resolve( shimsPath, 'use-sync-external-store-with-selector.ts' ), 'use-sync-external-store/shim/with-selector.js': resolve( shimsPath, 'use-sync-external-store-with-selector.ts' ), }; return { name: 'burdenoff:zustand-esm', enforce: 'pre', resolveId(source) { if (source in zustandEsmMap) { return zustandEsmMap[source]; } return null; }, }; } /** * Standard manual chunks function for app shell builds. * Splits vendor dependencies for optimal caching. */ export function createManualChunks(id: string): string | undefined { if ( id.includes('node_modules/react-dom') || id.includes('node_modules/react/') || id.includes('node_modules/react-router') || id.includes('node_modules/react-error-boundary') ) { return 'vendor-react'; } if ( id.includes('node_modules/@apollo/client') || id.includes('node_modules/graphql') || id.includes('node_modules/graphql-ws') ) { return 'vendor-apollo'; } if ( id.includes('node_modules/lucide-react') || id.includes('node_modules/sonner') || id.includes('node_modules/cmdk') ) { return 'vendor-ui'; } if (id.includes('node_modules/@radix-ui')) { return 'vendor-radix'; } if ( id.includes('node_modules/react-hook-form') || id.includes('node_modules/@hookform') || id.includes('node_modules/zod') ) { return 'vendor-forms'; } if (id.includes('node_modules/date-fns')) { return 'vendor-datefns'; } return undefined; }