PSA/server/next.config.mjs
Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

1272 lines
61 KiB
JavaScript

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/<pkg>/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;