import path from 'path'; import { fileURLToPath } from 'url'; import { createRequire } from 'module'; import fs from 'fs'; import os from 'os'; // build-trigger: update to force CI rebuild const require = createRequire(import.meta.url); const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const parsePositiveInt = (value) => { if (value == null) return undefined; const n = Number.parseInt(String(value), 10); return Number.isFinite(n) && n > 0 ? n : undefined; }; const truthyEnv = (value) => { const v = String(value ?? '').trim().toLowerCase(); return v === '1' || v === 'true' || v === 'yes' || v === 'on'; }; let webpack = null; try { webpack = require('next/dist/compiled/webpack/webpack').webpack; } catch (error) { console.warn('[next.config] Webpack runtime not available (likely running Turbopack dev server); skipping NormalModuleReplacementPlugin wiring.', error.message); } // Determine if this is an EE build const isEE = process.env.EDITION === 'ee' || process.env.EDITION === 'enterprise' || process.env.NEXT_PUBLIC_EDITION === 'enterprise'; // When USE_PREBUILT is set (Docker/CI), resolve pre-built packages from dist/ instead of src/. // Local dev always uses src/ — no extra build step required. const usePrebuilt = truthyEnv(process.env.USE_PREBUILT); const prebuiltDir = (pkg) => usePrebuilt ? `../packages/${pkg}/dist` : `../packages/${pkg}/src`; const prebuiltDirAbs = (pkg) => path.join(__dirname, usePrebuilt ? `../packages/${pkg}/dist` : `../packages/${pkg}/src`); // Reusable path to an empty shim for optional/native modules (used by Turbopack aliases) const emptyShim = './src/empty/shims/empty.ts'; const appVersion = (() => { try { const pkgPath = path.join(__dirname, '../packages/core/package.json'); const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); return pkg?.version || 'dev'; } catch { return 'dev'; } })(); const aliasEeEntryVariants = (aliasMap, pairs) => { pairs.forEach(({ fromCandidates = [], to }) => { fromCandidates .filter(Boolean) .forEach((candidate) => { aliasMap[candidate] = to; }); }); }; // Optional verbose module resolution logging (enable with LOG_MODULE_RESOLUTION=1) class LogModuleResolutionPlugin { apply(compiler) { compiler.hooks.normalModuleFactory.tap('LogModuleResolutionPlugin', (nmf) => { nmf.hooks.beforeResolve.tap('LogModuleResolutionPlugin', (data) => { try { if (!data) return; const req = data.request || ''; if (process.env.LOG_MODULE_RESOLUTION === '1' && (req.startsWith('@ee') || req.includes('ee/server/src'))) { console.log('[resolve:before]', { request: req, issuer: data.contextInfo?.issuer, context: data.context, }); } } catch { } }); nmf.hooks.afterResolve.tap('LogModuleResolutionPlugin', (result) => { try { if (!result) return; const req = result.createData?.request || result.request || result.rawRequest || ''; const res = result.resource || ''; const hit = req.startsWith('@ee') || req.includes('ee/server/src') || res.includes('/ee/server/src/') || res.includes('/server/src/empty/'); if (!hit || process.env.LOG_MODULE_RESOLUTION !== '1') return; const mappedTo = res.includes('/ee/server/src/') ? 'EE' : (res.includes('/server/src/empty/') ? 'CE-stub' : 'unknown'); console.log('[resolve:after]', { request: req, resource: res, mappedTo, context: result.context, issuer: result.createData?.issuer || result.contextInfo?.issuer, descriptionFilePath: result.resourceResolveData?.descriptionFilePath, }); } catch { } }); }); } } class EditionBuildDiagnosticsPlugin { constructor(options = {}) { this.options = { watchedRequests: options.watchedRequests || [ '@product/chat/entry', '@product/mcp/entry', '@product/extensions/entry', '@product/settings-extensions/entry', 'ee/server/src/app/msp/chat/page', ], }; } apply(compiler) { const shouldLog = String(process.env.LOG_EDITION_DIAGNOSTICS || '').toLowerCase(); const enabled = shouldLog === '1' || shouldLog === 'true'; if (!enabled) { return; } compiler.hooks.beforeCompile.tap('EditionBuildDiagnosticsPlugin', () => { const editionSnapshot = { EDITION: process.env.EDITION, NEXT_PUBLIC_EDITION: process.env.NEXT_PUBLIC_EDITION, NODE_ENV: process.env.NODE_ENV, cwd: process.cwd(), timestamp: new Date().toISOString(), }; console.log('[edition-diagnostics] build env', editionSnapshot); const eePaths = [ path.join(__dirname, '../ee/server/src/app/msp/chat/page.tsx'), path.join(__dirname, '../ee/server/src/components/chat/Chat.tsx'), ]; eePaths.forEach((candidate) => { console.log('[edition-diagnostics] ee artifact', { path: candidate, exists: fs.existsSync(candidate), }); }); }); compiler.hooks.normalModuleFactory.tap('EditionBuildDiagnosticsPlugin', (nmf) => { nmf.hooks.afterResolve.tap('EditionBuildDiagnosticsPlugin', (result) => { if (!result) return; const request = result.request || result.rawRequest || ''; const matched = this.options.watchedRequests.some((token) => request && request.includes(token)); const resource = result.resource || ''; if (!matched && !resource.includes('/ee/server/src/')) return; const createData = result.createData || {}; console.log('[edition-diagnostics] module resolution', { request, resource, resolvedResource: resource || createData.resource || createData.resolvedModule, resolvedPath: createData.path, userRequest: createData.userRequest, type: createData.type, issuer: result.contextInfo?.issuer, descriptionFilePath: result.resourceResolveData?.descriptionFilePath, }); }); }); } } const serverActionsBodyLimit = process.env.SERVER_ACTIONS_BODY_LIMIT || '20mb'; // Round 2 (memory campaign 2026-06-04): cap the static-gen / page-data worker // pool. The default (== host CPU count) spawns one worker per core (e.g. 31 on a // 32-core box), each loading the full app bundle (~290 MB) — the dominant build // memory peak. Capping to a small number collapses that peak onto the (fixed, // ~4-process) turbopack compilation floor with no wall-clock cost (static-gen is // ~0.1s; compilation, the bulk of the build, is turbopack/Rust and unaffected by // this count). Measured: 31→8 workers cut peak ~13.3→~10 GB and was *faster*. // Capped at available cores so small CI runners aren't oversubscribed. const hostParallelism = (os.availableParallelism?.() ?? os.cpus().length) || 8; const buildCpus = parsePositiveInt(process.env.NEXT_BUILD_CPUS) ?? Math.min(4, hostParallelism); const memoryBasedWorkersCount = truthyEnv(process.env.NEXT_BUILD_MEMORY_BASED_WORKERS_COUNT); const nextConfig = { env: { NEXT_PUBLIC_APP_VERSION: process.env.NEXT_PUBLIC_APP_VERSION || appVersion, // Propagate edition to client-side code // When EDITION=ee, set NEXT_PUBLIC_EDITION=enterprise for client components NEXT_PUBLIC_EDITION: isEE ? 'enterprise' : (process.env.NEXT_PUBLIC_EDITION || 'community'), }, turbopack: { root: path.resolve(__dirname, '..'), // Point to the actual project root // Alias optional DB drivers we don't use to an empty shim for Turbopack resolveAlias: { // Fix for emoji-mart data loading in Turbopack '@emoji-mart/data/sets/15/native.json': path.join(__dirname, '../node_modules/@emoji-mart/data/sets/15/native.json'), // Base app alias '@': './src', 'server/src': './src', // Add explicit alias for server/src imports '@alga-psa/ui': '../packages/ui/src', '@alga-psa/ui/': '../packages/ui/src/', '@alga-psa/clients': '../packages/clients/src', '@alga-psa/clients/': '../packages/clients/src/', // NB: tried switching bare-name aliases to ../packages//dist when // USE_PREBUILT=true. tsup bundles each package into a single // dist/index.js and downstream consumers import many sub-paths // (e.g. @alga-psa/ui/components/Button) which the dist doesn't expose. // Result: module-not-found across the build. Keep src/ aliases. '@alga-psa/auth': '../packages/auth/src', '@alga-psa/auth/': '../packages/auth/src/', '@alga-psa/auth/getCurrentUser': '../packages/auth/src/lib/getCurrentUser.ts', '@alga-psa/auth/session-bridge': '../packages/auth/src/lib/session-bridge.ts', '@alga-psa/auth/withAuth': '../packages/auth/src/lib/withAuth.ts', '@alga-psa/auth/nextAuthOptions': '../packages/auth/src/lib/nextAuthOptions.ts', '@alga-psa/auth/actions': '../packages/auth/src/actions/index.ts', '@alga-psa/auth/components': '../packages/auth/src/components/index.ts', // SSO provider buttons - swap between CE stub and EE implementation '@alga-psa/auth/sso/entry': isEE ? '../ee/server/src/components/auth/SsoProviderButtons.tsx' : '../packages/auth/src/components/SsoProviderButtons.tsx', // Notifications package '@alga-psa/notifications': '../packages/notifications/src', '@alga-psa/notifications/': '../packages/notifications/src/', '@alga-psa/notifications/actions': '../packages/notifications/src/actions/index.ts', '@alga-psa/notifications/components': '../packages/notifications/src/components/index.ts', '@alga-psa/notifications/hooks': '../packages/notifications/src/hooks/index.ts', '@alga-psa/scheduling': '../packages/scheduling/src', '@alga-psa/scheduling/': '../packages/scheduling/src/', '@alga-psa/ee-calendar': '../ee/packages/calendar/src/index.ts', '@alga-psa/ee-calendar/': '../ee/packages/calendar/src/', '@alga-psa/ee-microsoft-teams': isEE ? '../ee/packages/microsoft-teams/src/index.ts' : './src/empty/index.ts', '@alga-psa/ee-microsoft-teams/': isEE ? '../ee/packages/microsoft-teams/src/' : './src/empty/', '@alga-psa/ee-stubs': isEE ? '../ee/server/src' : '../packages/ee/src', '@alga-psa/ee-stubs/': isEE ? '../ee/server/src/' : '../packages/ee/src/', '@alga-psa/tags': '../packages/tags/src', '@alga-psa/tags/': '../packages/tags/src/', '@alga-psa/users': '../packages/users/src', '@alga-psa/users/': '../packages/users/src/', '@alga-psa/users/actions': '../packages/users/src/actions/index.ts', '@alga-psa/users/components': '../packages/users/src/components/index.ts', '@alga-psa/users/hooks': '../packages/users/src/hooks/index.ts', '@alga-psa/teams': '../packages/teams/src', '@alga-psa/teams/': '../packages/teams/src/', '@alga-psa/tenancy': '../packages/tenancy/src', '@alga-psa/tenancy/': '../packages/tenancy/src/', '@alga-psa/event-schemas': '../packages/event-schemas/src', '@alga-psa/event-schemas/': '../packages/event-schemas/src/', // Leaf horizontal packages (resolve to source during local dev) '@alga-psa/types': '../packages/types/src', '@alga-psa/types/': '../packages/types/src/', '@alga-psa/core': '../packages/core/src', '@alga-psa/core/rateLimit': '../packages/core/src/lib/rateLimit/index.ts', '@alga-psa/core/': '../packages/core/src/', '@alga-psa/validation': '../packages/validation/src', '@alga-psa/validation/': '../packages/validation/src/', '@alga-psa/formatting': '../packages/formatting/src', '@alga-psa/formatting/': '../packages/formatting/src/', '@alga-psa/agent-tooling': '../packages/agent-tooling/src', '@alga-psa/agent-tooling/': '../packages/agent-tooling/src/', // Documents package '@alga-psa/documents': '../packages/documents/src', '@alga-psa/documents/': '../packages/documents/src/', '@alga-psa/documents/storage/StorageService': '../packages/documents/src/storage/StorageService.ts', '@alga-psa/documents/actions': '../packages/documents/src/actions/index.ts', // Reference data package '@alga-psa/reference-data': '../packages/reference-data/src', '@alga-psa/reference-data/': '../packages/reference-data/src/', '@alga-psa/reference-data/actions': '../packages/reference-data/src/actions/index.ts', '@alga-psa/reference-data/components': '../packages/reference-data/src/components/index.ts', // Assets package '@alga-psa/assets': '../packages/assets/src', '@alga-psa/assets/': '../packages/assets/src/', '@alga-psa/assets/actions': '../packages/assets/src/actions/index.ts', '@alga-psa/assets/components': '../packages/assets/src/components/index.ts', // MSP Composition package '@alga-psa/msp-composition': '../packages/msp-composition/src', '@alga-psa/msp-composition/': '../packages/msp-composition/src/', // Billing package '@alga-psa/billing': '../packages/billing/src', '@alga-psa/billing/': '../packages/billing/src/', '@alga-psa/billing/actions': '../packages/billing/src/actions/index.ts', '@alga-psa/billing/components': '../packages/billing/src/components/index.ts', '@alga-psa/billing/models': '../packages/billing/src/models/index.ts', '@alga-psa/billing/services': '../packages/billing/src/services/index.ts', // Projects package '@alga-psa/projects': '../packages/projects/src', '@alga-psa/projects/': '../packages/projects/src/', '@alga-psa/projects/actions': '../packages/projects/src/actions/index.ts', '@alga-psa/projects/components': '../packages/projects/src/components/index.ts', // DB package (use source files for Turbopack dev/HMR) '@alga-psa/db': '../packages/db/src/index.ts', '@alga-psa/db/admin': '../packages/db/src/lib/admin.ts', '@alga-psa/db/connection': '../packages/db/src/lib/connection.ts', '@alga-psa/db/tenant': '../packages/db/src/lib/tenant.ts', '@alga-psa/db/models': '../packages/db/src/models/index.ts', '@alga-psa/db/models/user': '../packages/db/src/models/user.ts', '@alga-psa/db/models/userPreferences': '../packages/db/src/models/userPreferences.ts', '@alga-psa/db/models/tenant': '../packages/db/src/models/tenant.ts', '@alga-psa/db/models/UserSession': '../packages/db/src/models/UserSession.ts', // Surveys package '@alga-psa/surveys': '../packages/surveys/src', '@alga-psa/surveys/': '../packages/surveys/src/', '@alga-psa/surveys/actions': '../packages/surveys/src/actions/index.ts', '@alga-psa/surveys/actions/surveyResponseActions': '../packages/surveys/src/actions/surveyResponseActions.ts', '@alga-psa/surveys/actions/surveyTokenService': '../packages/surveys/src/actions/surveyTokenService.ts', '@alga-psa/surveys/components': '../packages/surveys/src/components/index.ts', '@alga-psa/surveys/components/public/SurveyResponsePage': '../packages/surveys/src/components/public/SurveyResponsePage.tsx', // Client Portal package '@alga-psa/client-portal': '../packages/client-portal/src', '@alga-psa/client-portal/': '../packages/client-portal/src/', '@alga-psa/client-portal/actions': '../packages/client-portal/src/actions/index.ts', '@alga-psa/client-portal/components': '../packages/client-portal/src/components/index.ts', // Portal shared package '@alga-psa/portal-shared': '../packages/portal-shared/src', '@alga-psa/portal-shared/': '../packages/portal-shared/src/', '@alga-psa/portal-shared/actions': '../packages/portal-shared/src/actions/index.ts', '@alga-psa/portal-shared/types': '../packages/portal-shared/src/types/index.ts', '@/empty': isEE ? '../ee/server/src' : './src/empty', '@/empty/': isEE ? '../ee/server/src/' : './src/empty/', './src/empty': isEE ? '../ee/server/src' : './src/empty', './src/empty/': isEE ? '../ee/server/src/' : './src/empty/', '@ee': isEE ? '../ee/server/src' : '../packages/ee/src', '@ee/': isEE ? '../ee/server/src/' : '../packages/ee/src/', '@enterprise': isEE ? '../ee/server/src' : '../packages/ee/src', '@enterprise/': isEE ? '../ee/server/src/' : '../packages/ee/src/', 'ee/server/src': isEE ? '../ee/server/src' : './src/empty', 'ee/server/src/': isEE ? '../ee/server/src/' : './src/empty/', // Native DB drivers not used 'better-sqlite3': emptyShim, 'sqlite3': emptyShim, 'mysql': emptyShim, 'mysql2': emptyShim, 'oracledb': emptyShim, 'tedious': emptyShim, // Node.js-only modules that shouldn't be bundled for client 'node-vault': emptyShim, 'postman-request': emptyShim, // Optional ffmpeg dependencies 'ffmpeg-static': emptyShim, 'ffprobe-static': emptyShim, 'ffprobe-static/package.json': './src/empty/shims/ffprobe-package.json', 'ffmpeg-static/package.json': './src/empty/shims/ffprobe-package.json', // sharp tries to conditionally require these optional packages; webpack can't statically resolve them '@img/sharp-libvips-dev/include': emptyShim, '@img/sharp-libvips-dev/cplusplus': emptyShim, '@img/sharp-wasm32/versions': emptyShim, // Knex dialect modules we don't use; alias directly to avoid cascading requires 'knex/lib/dialects/sqlite3': emptyShim, 'knex/lib/dialects/sqlite3/index.js': emptyShim, 'knex/lib/dialects/mysql': emptyShim, 'knex/lib/dialects/mysql/index.js': emptyShim, 'knex/lib/dialects/mysql2': emptyShim, 'knex/lib/dialects/mysql2/index.js': emptyShim, 'knex/lib/dialects/mssql': emptyShim, 'knex/lib/dialects/mssql/index.js': emptyShim, 'knex/lib/dialects/oracledb': emptyShim, 'knex/lib/dialects/oracledb/index.js': emptyShim, 'knex/lib/dialects/oracledb/utils.js': emptyShim, // Ensure Yjs resolves to a single ESM entrypoint to avoid "Yjs was already imported" warnings // caused by mixing CJS + ESM Yjs bundles in the same runtime. 'yjs': '../node_modules/yjs/dist/yjs.mjs', 'yjs/dist/yjs.cjs': '../node_modules/yjs/dist/yjs.mjs', // Product feature aliasing - point stable import paths to OSS or EE implementations '@product/extensions/entry': isEE ? '@product/extensions/ee/entry' : '@product/extensions/oss/entry', '@product/settings-extensions/entry': isEE ? '@product/settings-extensions/ee/entry' : '@product/settings-extensions/oss/entry', '@product/chat/entry': isEE ? '@product/chat/ee/entry' : '@product/chat/oss/entry', '@product/mcp/entry': isEE ? '@product/mcp/ee/entry' : '@product/mcp/oss/entry', '@product/ext-proxy/handler': isEE ? '@product/ext-proxy/ee/handler' : '@product/ext-proxy/oss/handler', '@alga-psa/integrations/email/providers/entry': isEE ? '@alga-psa/integrations/email/providers/ee/entry' : '@alga-psa/integrations/email/providers/oss/entry', '@alga-psa/integrations/email/settings/entry': isEE ? '@alga-psa/integrations/email/settings/ee/entry' : '@alga-psa/integrations/email/settings/oss/entry', '@alga-psa/integrations/email/domains/entry': isEE ? '@alga-psa/integrations/email/domains/ee/entry' : '@alga-psa/integrations/email/domains/oss/entry', '@alga-psa/integrations/entra/components/entry': isEE ? '../packages/integrations/src/entra/components/ee/entry' : '../packages/integrations/src/entra/components/oss/entry', '@alga-psa/integrations/entra/routes/entry': isEE ? '../packages/integrations/src/entra/routes/ee/entry' : '../packages/integrations/src/entra/routes/oss/entry', '@alga-psa/client-portal/domain-settings/entry': isEE ? '@alga-psa/client-portal/domain-settings/ee/entry' : '@alga-psa/client-portal/domain-settings/oss/entry', '@alga-psa/workflows/entry': isEE ? '../ee/server/src/workflows/entry' : './src/empty/workflows/entry', // user-activities workflow-task seam (EE-only source). CE resolves to stubs; EE to // the real implementations. Keeps the base user-activities package free of @alga-psa/workflows. '@alga-psa/user-activities/server/workflow-tasks': isEE ? '../ee/server/src/user-activities/workflowTasks.server' : '../packages/user-activities/src/server/workflow-tasks', '@alga-psa/user-activities/client/workflow-tasks': isEE ? '../ee/server/src/user-activities/workflowTasks.client' : '../packages/user-activities/src/client/workflow-tasks', '@alga-psa/user-activities': '../packages/user-activities/src', '@alga-psa/user-activities/': '../packages/user-activities/src/', '@product/billing/entry': isEE ? '@product/billing/ee/entry' : '@product/billing/oss/entry', '@product/auth-ee/entry': isEE ? '@product/auth-ee/ee/entry' : '@product/auth-ee/oss/entry', '@product/extension-actions': isEE ? '@product/extension-actions/ee' : '@product/extension-actions/oss', '@product/extension-actions/entry': isEE ? '@product/extension-actions/ee/entry' : '@product/extension-actions/oss/entry', '@product/extension-initialization/entry': isEE ? '@product/extension-initialization/ee/entry' : '@product/extension-initialization/oss/entry', // Map stable specifiers to relative sources so Turbopack can resolve them '@alga-psa/product-extension-initialization': isEE ? '../ee/server/src/lib/extensions/initialize' : '../packages/product-extension-initialization/oss/entry', '@alga-psa/product-extension-actions': isEE ? '../packages/product-extension-actions/ee/entry' : '../packages/product-extension-actions/oss/entry', }, }, reactStrictMode: false, // Disabled to prevent double rendering in development // Skip TS at build time — `tsc --noEmit` runs separately as `npm run typecheck`. // Next 16 dropped the in-config `eslint` knob; lint is now run via `npm run lint` // outside the build, so no equivalent setting needed. typescript: { ignoreBuildErrors: true }, transpilePackages: [ '@blocknote/core', '@blocknote/react', '@blocknote/mantine', '@emoji-mart/data', '@alga-psa/ui', '@alga-psa/scheduling', '@alga-psa/agent-tooling', '@alga-psa/users', '@alga-psa/email', '@alga-psa/teams', '@alga-psa/tenancy', '@alga-psa/integrations', '@alga-psa/client-portal', '@alga-psa/portal-shared', '@alga-psa/documents', '@alga-psa/reference-data', '@alga-psa/billing', '@alga-psa/msp-composition', '@alga-psa/user-composition', '@alga-psa/user-activities', '@alga-psa/projects', '@alga-psa/surveys', '@alga-psa/tickets', // Product feature packages (only those needed in this app) '@product/extensions', '@product/settings-extensions', '@product/billing', '@alga-psa/workflows', // New aliasing packages '@alga-psa/product-extension-actions', '@alga-psa/product-auth-ee', '@alga-psa/product-extension-initialization' // Tried trimming this list to only @blocknote/* under turbopack — it // regressed cold builds by +21 s. transpilePackages stays as a hint // that turbopack actually uses for fast-path resolution. ], // Rewrites required for PostHog async rewrites() { return [ { source: '/ingest/static/:path*', destination: 'https://us-assets.i.posthog.com/static/:path*', }, { source: '/ingest/:path*', destination: 'https://us.i.posthog.com/:path*', }, { source: '/ingest/decide', destination: 'https://us.i.posthog.com/decide', }, ]; }, // This is required to support PostHog trailing slash API requests skipTrailingSlashRedirect: true, webpack: (config, { isServer, dev }) => { // Filesystem cache: persists across builds (even after `rm -rf .next`) // so the second cold build reuses module compilation work. Stored under // node_modules/.cache/webpack so it survives `.next` clears. config.cache = { type: 'filesystem', cacheDirectory: path.join(__dirname, 'node_modules/.cache/webpack'), buildDependencies: { config: [__filename], }, // Snapshot all node_modules as immutable by mtime — avoids hash-stat on // every file (huge in this monorepo). managedPaths: [ path.resolve(__dirname, 'node_modules'), path.resolve(__dirname, '../node_modules'), ], }; // Add support for importing from ee/server/src using absolute paths // and ensure packages from root workspace are resolved const isEE = process.env.EDITION === 'ee' || process.env.EDITION === 'enterprise' || process.env.NEXT_PUBLIC_EDITION === 'enterprise'; console.log('[next.config] edition', isEE ? 'enterprise' : 'community', { cwd: process.cwd(), dirname: __dirname, LOG_MODULE_RESOLUTION: process.env.LOG_MODULE_RESOLUTION, }); config.resolve ??= {}; config.resolve.extensionAlias = { ...config.resolve.extensionAlias, '.js': ['.ts', '.tsx', '.js', '.jsx'], '.mjs': ['.mts', '.mjs'], '.jsx': ['.tsx', '.jsx'], }; config.resolve.alias = { ...(config.resolve.alias ?? {}), '@': path.join(__dirname, 'src'), 'server/src': path.join(__dirname, 'src'), // Add explicit alias for server/src imports // sharp tries to conditionally require these optional packages; webpack can't statically resolve them '@img/sharp-libvips-dev/include': path.join(__dirname, 'src/empty/shims/empty.ts'), '@img/sharp-libvips-dev/cplusplus': path.join(__dirname, 'src/empty/shims/empty.ts'), '@img/sharp-wasm32/versions': path.join(__dirname, 'src/empty/shims/empty.ts'), '@alga-psa/ui': path.join(__dirname, '../packages/ui/src'), // Pre-built packages: src/ for local dev, dist/ for production (USE_PREBUILT=true) '@alga-psa/auth': prebuiltDirAbs('auth'), '@alga-psa/auth/': `${prebuiltDirAbs('auth')}/`, '@alga-psa/notifications': prebuiltDirAbs('notifications'), '@alga-psa/notifications/': `${prebuiltDirAbs('notifications')}/`, '@alga-psa/clients': prebuiltDirAbs('clients'), '@alga-psa/clients/': `${prebuiltDirAbs('clients')}/`, '@alga-psa/types': prebuiltDirAbs('types'), '@alga-psa/types/': `${prebuiltDirAbs('types')}/`, '@alga-psa/core': prebuiltDirAbs('core'), '@alga-psa/core/rateLimit': usePrebuilt ? path.join(prebuiltDirAbs('core'), 'lib/rateLimit/index.js') : path.join(__dirname, '../packages/core/src/lib/rateLimit/index.ts'), '@alga-psa/core/': `${prebuiltDirAbs('core')}/`, '@alga-psa/validation': prebuiltDirAbs('validation'), '@alga-psa/validation/': `${prebuiltDirAbs('validation')}/`, '@alga-psa/formatting': prebuiltDirAbs('formatting'), '@alga-psa/formatting/': `${prebuiltDirAbs('formatting')}/`, '@alga-psa/event-schemas': prebuiltDirAbs('event-schemas'), '@alga-psa/event-schemas/': `${prebuiltDirAbs('event-schemas')}/`, '@alga-psa/sla': prebuiltDirAbs('sla'), '@alga-psa/sla/': `${prebuiltDirAbs('sla')}/`, '@alga-psa/assets': prebuiltDirAbs('assets'), '@alga-psa/assets/': `${prebuiltDirAbs('assets')}/`, '@alga-psa/tags': prebuiltDirAbs('tags'), '@alga-psa/tags/': `${prebuiltDirAbs('tags')}/`, // Source-transpiled packages '@alga-psa/scheduling': path.join(__dirname, '../packages/scheduling/src'), '@alga-psa/agent-tooling': path.join(__dirname, '../packages/agent-tooling/src'), '@alga-psa/agent-tooling/': `${path.join(__dirname, '../packages/agent-tooling/src')}/`, '@alga-psa/ee-calendar': path.join(__dirname, '../ee/packages/calendar/src'), '@alga-psa/ee-microsoft-teams': isEE ? path.join(__dirname, '../ee/packages/microsoft-teams/src') : path.join(__dirname, 'src/empty'), '@alga-psa/users': path.join(__dirname, '../packages/users/src'), '@alga-psa/teams': path.join(__dirname, '../packages/teams/src'), '@alga-psa/surveys': path.join(__dirname, '../packages/surveys/src'), '@alga-psa/client-portal': path.join(__dirname, '../packages/client-portal/src'), '@alga-psa/portal-shared': path.join(__dirname, '../packages/portal-shared/src'), '@ee': isEE ? path.join(__dirname, '../ee/server/src') : path.join(__dirname, '../packages/ee/src'), // Point to CE stub implementations '@enterprise': isEE ? path.join(__dirname, '../ee/server/src') : path.join(__dirname, '../packages/ee/src'), // Point to CE stub implementations // Also map deep EE paths used without the @ee alias to CE stubs // This ensures CE builds don't fail when code references ee/server/src directly 'ee/server/src': isEE ? path.join(__dirname, '../ee/server/src') : path.join(__dirname, 'src/empty'), // Feature swap aliases for Webpack (point directly to ts/tsx files) '@product/extensions/entry': (() => { const eePath = path.join(__dirname, '../packages/product-extensions/ee/entry.tsx'); const ossPath = path.join(__dirname, '../packages/product-extensions/oss/entry.tsx'); const selectedPath = isEE ? eePath : ossPath; console.log(`[WEBPACK ALIAS DEBUG] @product/extensions/entry -> ${selectedPath} (isEE: ${isEE})`); return selectedPath; })(), '@product/settings-extensions/entry': (() => { const eePath = path.join(__dirname, '../packages/product-settings-extensions/ee/entry.tsx'); const ossPath = path.join(__dirname, '../packages/product-settings-extensions/oss/entry.tsx'); const selectedPath = isEE ? eePath : ossPath; console.log(`[WEBPACK ALIAS DEBUG] @product/settings-extensions/entry -> ${selectedPath} (isEE: ${isEE})`); return selectedPath; })(), // MCP seam (.ts entries). The bare specifier must be aliased here for the // webpack build — without it, '@product/mcp/entry' falls through to the // package exports field (which only lists ./ee/entry and ./oss/entry) and // fails to resolve. Mirrors the extensions seam above. '@product/mcp/entry': (() => { const eePath = path.join(__dirname, '../packages/product-mcp/ee/entry.ts'); const ossPath = path.join(__dirname, '../packages/product-mcp/oss/entry.ts'); const selectedPath = isEE ? eePath : ossPath; console.log(`[WEBPACK ALIAS DEBUG] @product/mcp/entry -> ${selectedPath} (isEE: ${isEE})`); return selectedPath; })(), // SSO provider buttons - swap between CE stub and EE implementation '@alga-psa/auth/sso/entry': isEE ? path.join(__dirname, '../ee/server/src/components/auth/SsoProviderButtons.tsx') : path.join(__dirname, usePrebuilt ? '../packages/auth/dist/components/SsoProviderButtons.js' : '../packages/auth/src/components/SsoProviderButtons.tsx'), '@alga-psa/ee-stubs': isEE ? path.join(__dirname, '../ee/server/src') : path.join(__dirname, '../packages/ee/src'), '@alga-psa/ee-stubs/': isEE ? path.join(__dirname, '../ee/server/src/') : path.join(__dirname, '../packages/ee/src/'), '@alga-psa/integrations/email/providers/entry': isEE ? path.join(__dirname, '../packages/integrations/src/email/providers/ee/entry.tsx') : path.join(__dirname, '../packages/integrations/src/email/providers/oss/entry.tsx'), '@alga-psa/integrations/email/settings/entry': isEE ? path.join(__dirname, '../packages/integrations/src/email/settings/ee/entry.tsx') : path.join(__dirname, '../packages/integrations/src/email/settings/oss/entry.tsx'), '@alga-psa/integrations/email/domains/entry': isEE ? path.join(__dirname, '../packages/integrations/src/email/domains/ee/entry.ts') : path.join(__dirname, '../packages/integrations/src/email/domains/oss/entry.ts'), '@alga-psa/integrations/entra/components/entry': isEE ? path.join(__dirname, '../packages/integrations/src/entra/components/ee/entry.tsx') : path.join(__dirname, '../packages/integrations/src/entra/components/oss/entry.tsx'), '@alga-psa/integrations/entra/routes/entry': isEE ? path.join(__dirname, '../packages/integrations/src/entra/routes/ee/entry.ts') : path.join(__dirname, '../packages/integrations/src/entra/routes/oss/entry.ts'), '@alga-psa/client-portal/domain-settings/entry': isEE ? path.join(__dirname, '../packages/client-portal/src/domain-settings/ee/entry.tsx') : path.join(__dirname, '../packages/client-portal/src/domain-settings/oss/entry.tsx'), '@alga-psa/workflows/entry': isEE ? path.join(__dirname, '../ee/server/src/workflows/entry.tsx') : path.join(__dirname, 'src/empty/workflows/entry.tsx'), // user-activities workflow-task seam (EE-only source) — CE stubs / EE impls. '@alga-psa/user-activities/server/workflow-tasks': isEE ? path.join(__dirname, '../ee/server/src/user-activities/workflowTasks.server.ts') : path.join(__dirname, '../packages/user-activities/src/server/workflow-tasks.ts'), '@alga-psa/user-activities/client/workflow-tasks': isEE ? path.join(__dirname, '../ee/server/src/user-activities/workflowTasks.client.tsx') : path.join(__dirname, '../packages/user-activities/src/client/workflow-tasks.tsx'), // Base package alias (subpaths like /actions and /components resolve via prefix). // Must come AFTER the workflow-tasks seams so the EE seam wins for those specifiers. '@alga-psa/user-activities': path.join(__dirname, '../packages/user-activities/src'), '@product/billing/entry': isEE ? path.join(__dirname, '../packages/product-billing/ee/entry.tsx') : path.join(__dirname, '../packages/product-billing/oss/entry.tsx'), // Point stable specifiers to exact entry files to avoid conditional exports in package index '@alga-psa/product-extension-initialization': isEE ? path.join(__dirname, '../ee/server/src/lib/extensions/initialize.ts') : path.join(__dirname, '../packages/product-extension-initialization/oss/entry.ts'), '@alga-psa/product-extension-actions': isEE ? path.join(__dirname, '../packages/product-extension-actions/ee/entry.ts') : path.join(__dirname, '../packages/product-extension-actions/oss/entry.ts'), '@alga-psa/product-auth-ee': path.join(__dirname, '../packages/product-auth-ee'), }; const resolveModules = config.resolve.modules ?? ['node_modules']; config.resolve.modules = [...resolveModules, path.join(__dirname, '../node_modules')]; config.resolve.fallback = { ...(config.resolve.fallback ?? {}), querystring: require.resolve('querystring-es3'), }; // In EE mode, also alias any absolute CE-stub path prefix to EE source root if (isEE) { const ceEmptyAbs = path.join(__dirname, 'src', 'empty'); const eeSrcAbs = path.join(__dirname, '../ee/server/src'); config.resolve.alias[ceEmptyAbs] = eeSrcAbs; const pkgSettingsEntry = path.join(__dirname, '../packages/product-settings-extensions/entry.ts'); const pkgSettingsEntryIndex = path.join(__dirname, '../packages/product-settings-extensions/entry.tsx'); const pkgSettingsEeEntry = path.join(__dirname, '../packages/product-settings-extensions/ee/entry.tsx'); config.resolve.alias[pkgSettingsEntry] = pkgSettingsEeEntry; config.resolve.alias[pkgSettingsEntryIndex] = pkgSettingsEeEntry; const pkgExtensionsEntry = path.join(__dirname, '../packages/product-extensions/entry.ts'); const pkgExtensionsEntryIndex = path.join(__dirname, '../packages/product-extensions/entry.tsx'); const pkgExtensionsEeEntry = path.join(__dirname, '../packages/product-extensions/ee/entry.tsx'); config.resolve.alias[pkgExtensionsEntry] = pkgExtensionsEeEntry; config.resolve.alias[pkgExtensionsEntryIndex] = pkgExtensionsEeEntry; const pkgChatEntry = path.join(__dirname, '../packages/product-chat/entry.ts'); const pkgChatEntryIndex = path.join(__dirname, '../packages/product-chat/entry.tsx'); const pkgChatEeEntry = path.join(__dirname, '../packages/product-chat/ee/entry.tsx'); config.resolve.alias[pkgChatEntry] = pkgChatEeEntry; config.resolve.alias[pkgChatEntryIndex] = pkgChatEeEntry; const pkgMcpEntry = path.join(__dirname, '../packages/product-mcp/entry.ts'); const pkgMcpEeEntry = path.join(__dirname, '../packages/product-mcp/ee/entry.ts'); config.resolve.alias[pkgMcpEntry] = pkgMcpEeEntry; const pkgClientPortalEntry = path.join(__dirname, '../packages/client-portal/src/domain-settings/entry.ts'); const pkgClientPortalEntryIndex = path.join(__dirname, '../packages/client-portal/src/domain-settings/entry.tsx'); const pkgClientPortalEeEntry = path.join(__dirname, '../packages/client-portal/src/domain-settings/ee/entry.tsx'); config.resolve.alias[pkgClientPortalEntry] = pkgClientPortalEeEntry; config.resolve.alias[pkgClientPortalEntryIndex] = pkgClientPortalEeEntry; const pkgEmailDomainsEntry = path.join(__dirname, '../packages/integrations/src/email/domains/entry.ts'); const pkgEmailDomainsEeEntry = path.join(__dirname, '../packages/integrations/src/email/domains/ee/entry.ts'); config.resolve.alias[pkgEmailDomainsEntry] = pkgEmailDomainsEeEntry; aliasEeEntryVariants(config.resolve.alias, [ { to: pkgExtensionsEeEntry, fromCandidates: [ path.join(__dirname, '../packages/product-extensions/oss/entry.ts'), path.join(__dirname, '../packages/product-extensions/oss/entry.tsx'), ], }, { to: pkgSettingsEeEntry, fromCandidates: [ path.join(__dirname, '../packages/product-settings-extensions/oss/entry.ts'), path.join(__dirname, '../packages/product-settings-extensions/oss/entry.tsx'), ], }, { to: pkgClientPortalEeEntry, fromCandidates: [ path.join(__dirname, '../packages/client-portal/src/domain-settings/oss/entry.ts'), path.join(__dirname, '../packages/client-portal/src/domain-settings/oss/entry.tsx'), ], }, { to: path.join(__dirname, '../packages/integrations/src/email/providers/ee/entry.tsx'), fromCandidates: [ path.join(__dirname, '../packages/integrations/src/email/providers/entry.ts'), path.join(__dirname, '../packages/integrations/src/email/providers/oss/entry.ts'), path.join(__dirname, '../packages/integrations/src/email/providers/oss/entry.tsx'), ], }, { to: path.join(__dirname, '../packages/integrations/src/email/settings/ee/entry.tsx'), fromCandidates: [ path.join(__dirname, '../packages/integrations/src/email/settings/entry.ts'), path.join(__dirname, '../packages/integrations/src/email/settings/oss/entry.ts'), path.join(__dirname, '../packages/integrations/src/email/settings/oss/entry.tsx'), ], }, { to: path.join(__dirname, '../packages/integrations/src/email/domains/ee/entry.ts'), fromCandidates: [ path.join(__dirname, '../packages/integrations/src/email/domains/entry.ts'), path.join(__dirname, '../packages/integrations/src/email/domains/oss/entry.ts'), ], }, { to: path.join(__dirname, '../packages/integrations/src/entra/components/ee/entry.tsx'), fromCandidates: [ path.join(__dirname, '../packages/integrations/src/entra/components/entry.ts'), path.join(__dirname, '../packages/integrations/src/entra/components/oss/entry.tsx'), ], }, { to: path.join(__dirname, '../packages/integrations/src/entra/routes/ee/entry.ts'), fromCandidates: [ path.join(__dirname, '../packages/integrations/src/entra/routes/entry.ts'), path.join(__dirname, '../packages/integrations/src/entra/routes/oss/entry.ts'), ], }, { to: path.join(__dirname, '../packages/product-billing/ee/entry.tsx'), fromCandidates: [ path.join(__dirname, '../packages/product-billing/entry.ts'), path.join(__dirname, '../packages/product-billing/entry.tsx'), path.join(__dirname, '../packages/product-billing/oss/entry.ts'), path.join(__dirname, '../packages/product-billing/oss/entry.tsx'), ], }, { to: path.join(__dirname, '../packages/product-chat/ee/entry.tsx'), fromCandidates: [ path.join(__dirname, '../packages/product-chat/entry.ts'), path.join(__dirname, '../packages/product-chat/entry.tsx'), path.join(__dirname, '../packages/product-chat/oss/entry.ts'), path.join(__dirname, '../packages/product-chat/oss/entry.tsx'), ], }, { to: path.join(__dirname, '../packages/product-extension-actions/ee/entry.ts'), fromCandidates: [ path.join(__dirname, '../packages/product-extension-actions/entry.ts'), path.join(__dirname, '../packages/product-extension-actions/oss/entry.ts'), ], }, { to: path.join(__dirname, '../packages/product-extension-initialization/ee/entry.ts'), fromCandidates: [ path.join(__dirname, '../packages/product-extension-initialization/entry.ts'), path.join(__dirname, '../packages/product-extension-initialization/oss/entry.ts'), ], }, ]); } console.log('[next.config] aliases', { at: __dirname, '@': config.resolve.alias['@'], '@ee': config.resolve.alias['@ee'], '@enterprise': config.resolve.alias['@enterprise'], 'ee/server/src': config.resolve.alias['ee/server/src'], ceEmptyAbs: isEE ? path.join(__dirname, 'src', 'empty') : undefined, eeSrcAbs: isEE ? path.join(__dirname, '../ee/server/src') : undefined, }); config.plugins = config.plugins || []; config.plugins.push(new LogModuleResolutionPlugin()); config.plugins.push(new EditionBuildDiagnosticsPlugin()); // Exclude database dialects we don't use and heavy dev dependencies config.externals = [ ...config.externals || [], 'oracledb', 'mysql', 'mysql2', 'sqlite3', 'better-sqlite3', 'tedious' ]; // Externalize ts-morph for both client and server to prevent bundling issues // ts-morph is a huge library that shouldn't be bundled config.externals.push('ts-morph'); // Externalize optional ffmpeg dependencies // These are optional runtime dependencies that may not be installed config.externals.push('ffmpeg-static'); config.externals.push('ffprobe-static'); // Externalize expo-server-sdk for server builds. // The SDK uses require('../package.json') internally which breaks when bundled. if (isServer) { config.externals.push('expo-server-sdk'); } // Externalize sharp for server builds to avoid bundling native dependencies. // sharp (and its optional @img/* helpers) should be resolved at runtime by Node. if (isServer) { config.externals.push('sharp'); } else if (webpack) { // For client builds, make sure any accidental sharp import is replaced with an empty shim. config.resolve.alias = { ...config.resolve.alias, sharp: emptyShim, }; } // sharp conditionally requires these optional packages; webpack can't statically resolve them // and we don't want missing-module failures during compilation. if (webpack) { config.plugins.push( new webpack.IgnorePlugin({ resourceRegExp: /^@img\/sharp-libvips-dev\/(include|cplusplus)$/ }) ); config.plugins.push( new webpack.IgnorePlugin({ resourceRegExp: /^@img\/sharp-wasm32\/versions$/ }) ); } // Replace Node.js-only modules with empty shims for client builds // These modules use Node.js built-ins like 'tls', 'net', etc. that don't exist in the browser if (!isServer && webpack) { config.resolve.alias = { ...config.resolve.alias, 'node-vault': emptyShim, 'postman-request': emptyShim, }; } // Rule to handle .wasm files as assets config.module.rules.push({ test: /\.wasm$/, type: 'asset/resource', generator: { filename: 'static/wasm/[name].[hash][ext]', }, }); // Ensure .mjs files in node_modules are treated as JS auto (handles import.meta) config.module.rules.push({ test: /\.mjs$/, include: /node_modules/, type: 'javascript/auto', resolve: { fullySpecified: false, // Needed for some packages that omit extensions }, }); // Exclude flow components CSS files to prevent autoprefixer issues during build config.module.rules.push({ test: /\.module\.css$/, include: path.resolve(__dirname, '../ee/server/src/components/flow'), use: 'null-loader', }); // Enable WebAssembly experiments (temporarily disabled for debugging) // config.experiments = { // ...config.experiments, // asyncWebAssembly: true, // // layers: true, // Might be needed depending on the setup // }; // If running on serverless target, ensure wasm files are copied if (!isServer) { config.output.webassemblyModuleFilename = 'static/wasm/[modulehash].wasm'; } else { config.output.webassemblyModuleFilename = '../static/wasm/[modulehash].wasm'; // Copy the AssemblyScript source files needed at runtime for standard template sync // config.plugins.push( // new CopyPlugin({ // patterns: [ // { // from: path.resolve(__dirname, 'src/invoice-templates/assemblyscript'), // // Copy to a location relative to the server build output (.next/server/) // // so that path.resolve(process.cwd(), 'src/...') works at runtime // to: path.resolve(config.output.path, 'src/invoice-templates/assemblyscript'), // // Filter to only include necessary files if needed, but copying the whole dir is simpler // // filter: async (resourcePath) => resourcePath.endsWith('.ts') || resourcePath.includes('/standard/'), // globOptions: { // ignore: [ // // Ignore temporary or build artifact directories if they exist within // '**/temp_compile/**', // '**/node_modules/**', // '**/*.wasm', // Don't copy wasm files this way // '**/*.js', // Don't copy compiled JS // '**/package.json', // '**/tsconfig.json', // ], // }, // }, // ], // }) // ); } // In CE builds, replace any deep import of the EE S3 provider with the CE stub. // This also catches relative paths like ../../../ee/server/src/lib/storage/providers/S3StorageProvider // and @ee alias imports like @ee/lib/storage/providers/S3StorageProvider if (!isEE) { if (!webpack) { console.warn('[next.config] Skipping CE S3 storage provider replacement because webpack is unavailable in the current runtime.'); } else { config.plugins = config.plugins || []; config.plugins.push( new webpack.NormalModuleReplacementPlugin( // Removed (.*) prefix - was causing catastrophic backtracking on large strings /(ee[\\\/]server[\\\/]src[\\\/]|@ee[\\\/])lib[\\\/]storage[\\\/]providers[\\\/]S3StorageProvider(\.[jt]s)?$/, path.join(__dirname, 'src/empty/lib/storage/providers/S3StorageProvider') ) ); // The MCP seam has no tsconfig `paths` entry (unlike chat/extensions), // so in CE the bare `@product/mcp/entry` specifier falls through to the // package `exports` and lands on ./ee/entry (the EE impl, which imports // @ee/lib/mcp/* with no CE stub) — beating resolve.alias. Force it to the // CE stub before resolution so no EE governance code enters the CE build. config.plugins.push( new webpack.NormalModuleReplacementPlugin( /^@product[\\\/]mcp[\\\/]entry$/, path.join(__dirname, '../packages/product-mcp/oss/entry.ts') ) ); } } // In enterprise builds, remap any CE-stub absolute paths to their EE equivalents. // This ensures tsconfig path mapping that points to src/empty is overridden at webpack stage. if (isEE) { if (!webpack) { console.warn('[next.config] Skipping EE empty-stub replacement plugin because webpack is unavailable in the current runtime.'); } else { const ceEmptyPrefix = path.join(__dirname, 'src', 'empty') + path.sep; const ceEmptyRegex = new RegExp(ceEmptyPrefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')); // Also handle packages/ee/src CE stubs (used by workspace package dynamic imports) const cePackagesEePrefix = path.join(__dirname, '../packages/ee/src') + path.sep; const cePackagesEeRegex = new RegExp(cePackagesEePrefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')); const eeSrcRoot = path.join(__dirname, '../ee/server/src') + path.sep; const workflowsEeEntry = path.join(__dirname, '../ee/server/src/workflows/entry.tsx'); const userActivitiesWorkflowTasksServerEe = path.join(__dirname, '../ee/server/src/user-activities/workflowTasks.server.ts'); const userActivitiesWorkflowTasksClientEe = path.join(__dirname, '../ee/server/src/user-activities/workflowTasks.client.tsx'); const authSsoButtonsEeEntry = path.join( __dirname, '../ee/server/src/components/auth/SsoProviderButtons.tsx', ); config.plugins = config.plugins || []; // Force the MCP seam to the EE implementation before resolution, so the // tsconfig `paths` CE default (@product/mcp/entry -> oss) can't win via // JsConfigPathsPlugin and produce a hybrid EE build. Mirrors the CE force. config.plugins.push( new webpack.NormalModuleReplacementPlugin( /^@product[\\\/]mcp[\\\/]entry$/, path.join(__dirname, '../packages/product-mcp/ee/entry.ts') ) ); config.plugins.push(new webpack.NormalModuleReplacementPlugin(/.*/, (resource) => { try { const req = resource.request || ''; // Next.js adds a JsConfigPathsPlugin based on tsconfig "paths". // If the workflows entry specifier gets resolved via tsconfig `paths` before webpack aliasing, an enterprise build // can accidentally bundle the CE/OSS workflow stub UI ("hybrid" build). // // Force consistency by rewriting the workflows entry specifier to the canonical EE source file *before* resolution. if (req === '@alga-psa/workflows/entry') { resource.request = workflowsEeEntry; return; } // user-activities workflow-task seam: it's a real package export, so without // this an EE build could resolve it to the in-package CE default via the // exports map before the webpack alias wins. Force the EE implementations. if (req === '@alga-psa/user-activities/server/workflow-tasks') { resource.request = userActivitiesWorkflowTasksServerEe; return; } if (req === '@alga-psa/user-activities/client/workflow-tasks') { resource.request = userActivitiesWorkflowTasksClientEe; return; } // Same issue for auth SSO provider buttons: tsconfig may point `@alga-psa/auth/sso/entry` // at the CE stub. Force the EE implementation for enterprise builds. if (req === '@alga-psa/auth/sso/entry') { resource.request = authSsoButtonsEeEntry; return; } // IMPORTANT: // Next.js adds a JsConfigPathsPlugin based on tsconfig "paths". // Our tsconfig maps `@ee/* -> packages/ee/src/*` (CE stubs) and relies on webpack to override // to `ee/server/src` in EE builds. // // In practice, JsConfigPathsPlugin can resolve the stub path first when the stub file exists, // producing "hybrid" EE builds where some `@ee/*` imports fall back to real EE code (when no // stub exists), but many resolve to CE stubs (when the stub does exist). // // To force consistency, rewrite `@ee/*` specifiers to the EE source root *before* resolution. if (req === '@ee') { resource.request = eeSrcRoot.slice(0, -path.sep.length); return; } if (req.startsWith('@ee/')) { const rel = req.substring('@ee/'.length); const mapped = path.join(eeSrcRoot, rel); if (process.env.LOG_MODULE_RESOLUTION === '1') { console.log('[replace:EE:@ee]', { from: req, to: mapped }); } resource.request = mapped; return; } // Prefer @enterprise imports for CE/EE separation; rewrite to EE sources in enterprise builds. if (req === '@enterprise') { resource.request = eeSrcRoot.slice(0, -path.sep.length); return; } if (req.startsWith('@enterprise/')) { const rel = req.substring('@enterprise/'.length); const mapped = path.join(eeSrcRoot, rel); if (process.env.LOG_MODULE_RESOLUTION === '1') { console.log('[replace:EE:@enterprise]', { from: req, to: mapped }); } resource.request = mapped; return; } // Replace src/empty paths if (ceEmptyRegex.test(req)) { const rel = req.substring(ceEmptyPrefix.length); const mapped = path.join(eeSrcRoot, rel); if (process.env.LOG_MODULE_RESOLUTION === '1') { console.log('[replace:EE:empty]', { from: req, to: mapped }); } resource.request = mapped; } // Replace packages/ee/src paths (CE stubs from workspace packages) else if (cePackagesEeRegex.test(req)) { const rel = req.substring(cePackagesEePrefix.length); const mapped = path.join(eeSrcRoot, rel); if (process.env.LOG_MODULE_RESOLUTION === '1') { console.log('[replace:EE:packages]', { from: req, to: mapped }); } resource.request = mapped; } } catch { } })); } } // Conditionally enable verbose resolution logging for EE/CE module paths if (process.env.LOG_MODULE_RESOLUTION === '1') { config.plugins = config.plugins || []; config.plugins.push(new LogModuleResolutionPlugin()); // Also tap the resolver directly to capture final resolved paths class LogResolverPlugin { apply(compiler) { try { compiler.resolverFactory.hooks.resolver.for('normal').tap('LogResolverPlugin', (resolver) => { resolver.hooks.resolve.tapAsync('LogResolverPlugin', (request, ctx, done) => { try { const req = request.request || ''; if (req.startsWith('@ee') || req.includes('ee/server/src')) { console.log('[resolver:resolve]', { request: req, path: request.path, context: request.context?.issuer || ctx.issuer, }); } } catch { } done(); }); resolver.hooks.result.tap('LogResolverPlugin', (result) => { try { if (!result) return; const resPath = result.path || ''; const req = result.request || ''; const hit = req?.startsWith?.('@ee') || req?.includes?.('ee/server/src') || resPath.includes('/ee/server/src/') || resPath.includes('/server/src/empty/'); if (!hit) return; console.log('[resolver:result]', { request: req, resolvedPath: resPath, mappedTo: resPath.includes('/ee/server/src/') ? 'EE' : (resPath.includes('/server/src/empty/') ? 'CE-stub' : 'unknown'), }); } catch { } }); }); console.log('[next.config] LogModuleResolutionPlugin enabled'); } catch (e) { console.log('[next.config] Failed to enable LogResolverPlugin', e?.message); } } } config.plugins.push(new LogResolverPlugin()); } return config; }, // Explicitly disable production browser source maps (default but be explicit). // Eliminates source-map emit work for every client chunk. productionBrowserSourceMaps: false, // SWC compiler: strip console.* in production output (excluding error/warn). // Cuts bytes; minify pass also has less to walk. compiler: { removeConsole: { exclude: ['error', 'warn'] }, }, experimental: { // We alias certain EE-only modules directly into `../ee/server/src/**` (outside this Next.js app root). // Ensure Next is allowed to import/compile source files from outside `server/`. externalDir: true, serverActions: { bodySizeLimit: serverActionsBodyLimit, }, // Increase middleware body size limit for extension installs proxyClientMaxBodySize: '100mb', // Next build "Collecting page data" uses a worker pool sized from this value. // Default-capped (see buildCpus above) to bound peak memory; override with // NEXT_BUILD_CPUS. In large repos the uncapped default (== host CPU count) // OOMs in CI. cpus: buildCpus, ...(memoryBasedWorkersCount ? { memoryBasedWorkersCount: true } : {}), // Disable server source maps (RSC + server actions). Build-only — does // not affect production error traces from Sentry or similar tools. serverSourceMaps: false, // Tried turbopackPersistentCaching — Next 16.2 only supports // turbopackFileSystemCacheForDev (dev mode), not production builds. // Revisit when Next exposes persistent build cache. // Tried optimizePackageImports (broad list, then just lucide-react) — // both regressed cold builds by 20 s and 480 s respectively in this // monorepo's webpack+SWC setup. Leaving it off. }, // Externalize Node.js-only packages with native dependencies from server bundles. // This prevents Turbopack from bundling them with mangled names. serverExternalPackages: [ 'puppeteer', 'sharp', '@opentelemetry/sdk-node', '@opentelemetry/auto-instrumentations-node', '@opentelemetry/exporter-trace-otlp-grpc', '@opentelemetry/exporter-metrics-otlp-grpc', '@opentelemetry/sdk-metrics', '@opentelemetry/sdk-trace-base', '@opentelemetry/resources', '@opentelemetry/semantic-conventions', '@opentelemetry/api', 'expo-server-sdk', // Heavy Node-only deps — runtime requires `require()`, no need to bundle. 'knex', 'pg', 'pg-boss', 'pg-pool', 'pg-cursor', 'pg-protocol', 'redis', 'ioredis', 'stripe', 'openai', '@google-cloud/aiplatform', '@google-cloud/vertexai', 'handlebars', 'jsdom', 'monaco-editor', '@js-temporal/polyfill', 'pdfkit', 'sharp', // Round 2: more Node-only deps 'axios', 'dotenv', 'jsonwebtoken', 'rate-limiter-flexible', '@temporalio/client', '@temporalio/common', '@temporalio/worker', 'winston', 'pino', 'parsimmon', 'ajv', '@aws-sdk/client-s3', '@aws-sdk/client-sts', '@aws-sdk/credential-providers', 'xlsx', 'archiver', 'unzipper', '@grpc/grpc-js', '@huggingface/inference', 'google-auth-library', 'googleapis', '@azure/identity', '@azure/msal-node', '@microsoft/microsoft-graph-client', // Round 3: more server-only/heavy libs 'posthog-node', 'bcryptjs', 'speakeasy', 'qrcode', 'pdf-lib', 'pdf2pic', 'marked', '@faker-js/faker', 'moment', 'tinycolor2', // Round 4 (memory campaign 2026-06-04): more server-only deps confirmed // imported in the app graph but not used in client components. Keeps them // out of the Turbopack server bundle that every static-gen worker loads. 'nodemailer', 'imapflow', 'express', 'fluent-ffmpeg', 'tar', 'node-vault', '@nestjs/common', '@asteasolutions/zod-to-openapi', 'formdata-node', '@aws-sdk/s3-request-presigner', 'winston-daily-rotate-file', ], // Note: output: 'standalone' was removed due to static page generation issues generateBuildId: async () => { return 'build-' + Date.now(); } }; export default nextConfig;