fix(c3,arch-019): static chunk map for plugin components - production build loads plugin pages correctly

This commit is contained in:
Agent Zero
2026-08-23 22:14:45 +02:00
parent 4bce89aecb
commit b01b756a4a
@@ -91,6 +91,62 @@ class PluginErrorBoundary extends Component<PluginErrorBoundaryProps, PluginErro
} }
} }
// ── Static Chunk Map (ARCH-019) ─────────────────────────────────────────
//
// Vite cannot statically analyze dynamic import paths built at runtime
// (`import(/* @vite-ignore */ path)`), so production builds would fail to
// load plugin pages. All plugin-contributed component paths are known at
// build time — they are declared in the backend manifests — so we register
// them here as explicit lazy imports. Vite chunks each one correctly.
//
// Unknown paths (e.g. third-party plugins added later) fall back to the
// runtime dynamic import, which works in dev mode.
type ComponentModule = Record<string, unknown> & { default?: React.ComponentType<any> };
type LazyComponentFactory = () => Promise<{ default: React.ComponentType<any> }>;
/**
* Normalize a module namespace to the { default } shape React.lazy expects:
* uses the default export when present, otherwise the first named export
* (same fallback as the runtime dynamic-import path below).
*/
function normalizeModule(m: ComponentModule): { default: React.ComponentType<any> } {
const Comp = (m.default ?? m[Object.keys(m)[0]]) as React.ComponentType<any>;
return { default: Comp };
}
const STATIC_COMPONENT_MAP: Record<string, LazyComponentFactory> = {
// Pages
'@/pages/AgentDashboard': () => import('@/pages/AgentDashboard').then(normalizeModule),
// NOTE: @/pages/AIAssistant is declared in the backend manifest but does
// not exist as a frontend page — ghost component, see PROGRESS.md.
'@/pages/AISettings': () => import('@/pages/AISettings').then(normalizeModule),
'@/pages/AutomationDashboard': () => import('@/pages/AutomationDashboard').then(normalizeModule),
'@/pages/AutomationSettings': () => import('@/pages/AutomationSettings').then(normalizeModule),
'@/pages/Calendar': () => import('@/pages/Calendar').then(normalizeModule),
'@/pages/Communication': () => import('@/pages/Communication').then(normalizeModule),
'@/pages/Dms': () => import('@/pages/Dms').then(normalizeModule),
'@/pages/DmsTrash': () => import('@/pages/DmsTrash').then(normalizeModule),
'@/pages/GlobalSearchResults': () => import('@/pages/GlobalSearchResults').then(normalizeModule),
'@/pages/ImportExport': () => import('@/pages/ImportExport').then(normalizeModule),
'@/pages/Mail': () => import('@/pages/Mail').then(normalizeModule),
'@/pages/MailSettings': () => import('@/pages/MailSettings').then(normalizeModule),
'@/pages/ProactiveAISettings': () => import('@/pages/ProactiveAISettings').then(normalizeModule),
'@/pages/Reports': () => import('@/pages/Reports').then(normalizeModule),
'@/pages/SettingsGroups': () => import('@/pages/SettingsGroups').then(normalizeModule),
'@/pages/SettingsNotifications': () => import('@/pages/SettingsNotifications').then(normalizeModule),
'@/pages/SettingsRoles': () => import('@/pages/SettingsRoles').then(normalizeModule),
'@/pages/SettingsUsers': () => import('@/pages/SettingsUsers').then(normalizeModule),
'@/pages/Tasks': () => import('@/pages/Tasks').then(normalizeModule),
'@/pages/Wiki': () => import('@/pages/Wiki').then(normalizeModule),
'@/pages/Workflows': () => import('@/pages/Workflows').then(normalizeModule),
// NOTE: The backend manifests also declare contact detail tabs
// (@/components/contact/Contact*Tab) whose components do not exist in the
// frontend yet — they are intentionally NOT registered here; they fall
// through to the runtime fallback and surface via the error boundary.
// See PROGRESS.md "Ghost components" finding.
};
// ── Lazy Component Cache ──────────────────────────────────────────────── // ── Lazy Component Cache ────────────────────────────────────────────────
const componentCache = new Map<string, React.LazyExoticComponent<React.ComponentType<any>>>(); const componentCache = new Map<string, React.LazyExoticComponent<React.ComponentType<any>>>();
@@ -100,8 +156,16 @@ function getLazyComponent(componentPath: string): React.LazyExoticComponent<Reac
return componentCache.get(componentPath)!; return componentCache.get(componentPath)!;
} }
// Convert @/pages/Mail to ../../pages/Mail for dynamic import // Known plugin components: statically chunked at build time (ARCH-019)
// PluginLoader.tsx is in src/components/plugins/, so ../../ resolves to src/ const factory = STATIC_COMPONENT_MAP[componentPath];
if (factory) {
const LazyComp = lazy(factory);
componentCache.set(componentPath, LazyComp);
return LazyComp;
}
// Fallback for unknown paths (third-party plugins): runtime dynamic
// import. Works in dev; production needs the path registered above.
const importPath = componentPath.replace(/^@\//, '../../'); const importPath = componentPath.replace(/^@\//, '../../');
const LazyComp = lazy(() => const LazyComp = lazy(() =>