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
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
1272 lines
61 KiB
JavaScript
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;
|