Manifest Mvp Plan
Ecosistema completo para organización y gestión de contenido con interfaces espaciales
ModuleManifest MVP Update Plan
🎯 Directivas Core
- ❌ Sin retrocompatibilidad - MVP en construcción, cambios libres
- ⚡ Signals primero - Usar Signals donde sea posible
- 🎯 Tipos estrictos - Evitar
any, usar estructuras bien definidas - 🚀 Simplicidad MVP - Solo lo necesario para funcionar
1. Análisis Simplificado
Estado Actual (module-manifest.interface.ts)
export interface ModuleManifest {
readonly id: string;
readonly version: string;
readonly displayName?: string;
readonly permissions?: readonly string[];
}
Estado Objetivo MVP (module-base.md)
export interface ModuleManifest {
// Core (SIMPLIFICADO)
id: string;
version: string;
name: string; // displayName → name (requerido)
description: string;
// Navigation (ESENCIAL)
navigation?: ModuleNavigationConfig;
routes?: ModuleRoute[];
// Components & Actions (CORE MVP)
embeddableComponents?: ModuleEmbeddableComponent[];
publicActions?: ModulePublicAction[];
// Dependencies (MÍNIMO)
dependencies?: ModuleDependency[];
permissions: ModulePermission[]; // Siempre array estructurado
// Metadata (OPCIONAL MVP)
author?: string;
tags?: string[];
}
Diferencias clave:
- ✅
displayName→name(obligatorio, sin opcionalidad) - ✅
permissions: string[]→permissions: ModulePermission[](siempre estructurado) - ✅ Solo propiedades esenciales para MVP funcional
- ❌ Sin
config,assets,exports,imports,provides,consumes(complejidad innecesaria para MVP)
2. Interfaces MVP (Simplificadas)
ModuleManifest Principal
/**
* Module manifest for MVP - simplified and focused
*/
export interface ModuleManifest {
/** Unique module identifier */
readonly id: string;
/** Semantic version (e.g., '1.0.0') */
readonly version: string;
/** Display name for UI */
readonly name: string;
/** Brief description */
readonly description: string;
/** Navigation configuration */
readonly navigation?: ModuleNavigationConfig;
/** Module routes for Angular Router */
readonly routes?: readonly ModuleRoute[];
/** Components that can be embedded by other modules */
readonly embeddableComponents?: readonly ModuleEmbeddableComponent[];
/** Public actions exposed to other modules */
readonly publicActions?: readonly ModulePublicAction[];
/** Module dependencies */
readonly dependencies?: readonly ModuleDependency[];
/** Required permissions (structured) */
readonly permissions: readonly ModulePermission[];
/** Module author (optional) */
readonly author?: string;
/** Tags for categorization (optional) */
readonly tags?: readonly string[];
}
NavigationConfig (con Signals)
export interface ModuleNavigationConfig {
/** Main menu items */
readonly menuItems: readonly NavigationMenuItem[];
/** Breadcrumb configuration (optional) */
readonly breadcrumbs?: readonly BreadcrumbConfig[];
/** Toolbar items (optional) */
readonly toolbar?: readonly ToolbarItem[];
}
export interface NavigationMenuItem {
readonly id: string;
readonly label: string;
readonly icon?: string;
readonly route?: string;
readonly order: number;
/** Badge count using Signal (Angular 20) */
readonly badge?: Signal<number>; // ✅ Signal, no Observable
/** Required permissions to view */
readonly permissions?: readonly string[];
/** Child menu items */
readonly children?: readonly NavigationMenuItem[];
}
export interface BreadcrumbConfig {
readonly label: string;
readonly route?: string;
}
export interface ToolbarItem {
readonly id: string;
readonly label: string;
readonly icon?: string;
readonly action: string; // Action ID to execute
readonly permissions?: readonly string[];
}
ModuleRoute (Angular Router)
export interface ModuleRoute {
readonly path: string;
readonly component: Type<unknown>; // ✅ No `any`, usar `unknown`
readonly permissions?: readonly string[];
readonly data?: Record<string, unknown>; // ✅ Typed object
}
ModuleEmbeddableComponent
export interface ModuleEmbeddableComponent {
readonly id: string;
readonly componentType: Type<unknown>; // ✅ No `any`
readonly displayName: string;
readonly description?: string;
readonly category: 'widget' | 'list' | 'form' | 'dashboard';
readonly permissions?: readonly string[];
/** Expected inputs (typed) */
readonly inputs?: readonly ComponentInput[];
/** Preview thumbnail (optional) */
readonly thumbnail?: string;
}
export interface ComponentInput {
readonly name: string;
readonly type: 'string' | 'number' | 'boolean' | 'object' | 'array';
readonly required: boolean;
readonly defaultValue?: unknown;
}
ModulePublicAction
export interface ModulePublicAction {
readonly id: string;
readonly displayName: string;
readonly description?: string;
readonly category: 'navigation' | 'crud' | 'workflow' | 'integration';
readonly permissions?: readonly string[];
/** Input schema (typed) */
readonly inputSchema?: Record<string, unknown>;
/** Output schema (typed) */
readonly outputSchema?: Record<string, unknown>;
}
ModuleDependency
export interface ModuleDependency {
readonly moduleId: string;
readonly version: string;
readonly required: boolean;
}
ModulePermission (desde @libs-core/shared-models o redefinir)
export interface ModulePermission {
readonly id: string;
readonly name: string;
readonly description?: string;
readonly category?: string;
}
3. Estrategia de Implementación MVP
Fase Única: Implementación Directa (Simplificado)
Sin fases complejas - implementación directa en 1-2 días
Día 1: Interfaces + Services
Mañana (4 horas): Interfaces
- ✅ Actualizar
module-manifest.interface.ts(reemplazar, no extender) - ✅ Crear
navigation.interface.ts(NavigationConfig, MenuItem, Breadcrumb, Toolbar) - ✅ Crear
embeddable-component.interface.ts(ModuleEmbeddableComponent, ComponentInput) - ✅ Crear
public-action.interface.ts(ModulePublicAction) - ✅ Crear
module-dependency.interface.ts(ModuleDependency) - ✅ Actualizar barrel
src/index.ts
Tarde (4 horas): Services
- ✅ Actualizar
ModuleRegistryService- procesar nuevas propiedades - ✅ Crear
NavigationService(básico) - agregar menús de módulos - ✅ Crear
ComponentRegistryService(básico) - registrar componentes embebibles - ✅ Integrar con
ActionRegistryServiceexistente para public actions
Día 2: Testing + Docs
Mañana (4 horas): Testing
- ✅ Tests unitarios para interfaces (validación TypeScript)
- ✅ Tests para
ModuleRegistryServicecon manifests completos - ✅ Tests para
NavigationService(agregación básica) - ✅ Tests para
ComponentRegistryService
Tarde (3 horas): Documentación
- ✅ JSDoc completo en todas las interfaces
- ✅ Ejemplo de manifest completo (Products module)
- ✅ Actualizar
module-base.mdcon estado real
Total: ~15 horas (~2 días)
4. Decisiones de Diseño MVP
✅ Signals sobre Observables
// NavigationMenuItem
badge?: Signal<number>; // ✅ MVP usa Signals
// Si necesitamos convertir Observable a Signal:
const badgeCount = toSignal(observable$, { initialValue: 0 });
✅ Tipos Estrictos (No any)
// ❌ Evitar
componentType: Type<any>
// ✅ Usar
componentType: Type<unknown> // Más seguro, requiere validación
✅ Estructuras Simples
// Permissions siempre estructurado
permissions: readonly ModulePermission[]
// No strings directos
// permissions?: string[] // ❌
❌ Sin Features Innecesarias para MVP
- ❌
config: ModuleConfigSchema- No necesario ahora - ❌
assets: ModuleAssets- No necesario ahora - ❌
exports/imports: ModuleExports/ModuleImports- Complejidad innecesaria - ❌
provides/consumes: ModuleProvider/ModuleConsumer- DI complejo, no MVP - ❌ Dynamic component loading system - Registrar solo, cargar después
- ❌ Provider/Consumer DI system - No necesario para MVP
Foco MVP: Navigation + Routes + Registro de componentes/acciones
5. Estructura de Archivos
Nuevos Archivos a Crear
/libs/core/module-base/src/lib/contracts/
├── module-manifest.interface.ts (REEMPLAZAR)
├── navigation.interface.ts (NUEVO)
├── embeddable-component.interface.ts (NUEVO)
├── public-action.interface.ts (NUEVO)
└── module-dependency.interface.ts (NUEVO)
/libs/core/module-base/src/lib/services/
├── module-registry.service.ts (ACTUALIZAR)
├── navigation.service.ts (NUEVO - básico)
└── component-registry.service.ts (NUEVO - básico)
/libs/core/module-base/tests/
├── manifest-interfaces.test.ts (NUEVO)
├── module-registry.test.ts (ACTUALIZAR)
├── navigation.test.ts (NUEVO)
└── component-registry.test.ts (NUEVO)
/docs/libs/module-base/
├── examples/
│ └── products-manifest.example.ts (NUEVO)
└── module-base.md (ACTUALIZAR)
Archivos a Actualizar
/libs/core/module-base/src/index.ts (barrel exports)
/libs/core/module-integrator/ (si usa ModuleManifest directamente)
6. Testing MVP (Pragmático)
Tests Esenciales
Unit Tests (interfaces):
describe('ModuleManifest MVP', () => {
it('should compile with minimal required properties', () => {
const manifest: ModuleManifest = {
id: 'test-module',
version: '1.0.0',
name: 'Test Module',
description: 'A test module',
permissions: []
};
expect(manifest.id).toBe('test-module');
});
it('should accept full navigation config with Signals', () => {
const badgeSignal = signal(5);
const manifest: ModuleManifest = {
id: 'test-module',
version: '1.0.0',
name: 'Test Module',
description: 'A test module',
permissions: [],
navigation: {
menuItems: [
{
id: 'item1',
label: 'Item 1',
route: '/item1',
order: 1,
badge: badgeSignal // ✅ Signal
}
]
}
};
expect(manifest.navigation?.menuItems[0].badge?.()).toBe(5);
});
});
Integration Tests (services):
describe('NavigationService MVP', () => {
it('should aggregate menu items from multiple modules', () => {
const manifest1: ModuleManifest = {
id: 'module1',
version: '1.0.0',
name: 'Module 1',
description: 'First module',
permissions: [],
navigation: {
menuItems: [{ id: 'item1', label: 'Item 1', route: '/item1', order: 1 }]
}
};
const manifest2: ModuleManifest = {
id: 'module2',
version: '1.0.0',
name: 'Module 2',
description: 'Second module',
permissions: [],
navigation: {
menuItems: [{ id: 'item2', label: 'Item 2', route: '/item2', order: 2 }]
}
};
moduleRegistry.registerModule(manifest1);
moduleRegistry.registerModule(manifest2);
const menuItems = navigationService.getAllMenuItems();
expect(menuItems).toHaveLength(2);
expect(menuItems[0].id).toBe('item1');
expect(menuItems[1].id).toBe('item2');
});
});
Cobertura Objetivo: ≥80% (pragmático para MVP, no 90%)
7. Ejemplo Completo MVP
Products Module Manifest
import { signal } from '@angular/core';
import { ProductListComponent } from './components/product-list.component';
import { ProductListWidgetComponent } from './widgets/product-list-widget.component';
export const PRODUCTS_MODULE_MANIFEST: ModuleManifest = {
id: 'products',
version: '1.0.0',
name: 'Product Management',
description: 'Comprehensive product catalog and inventory management',
// Navigation
navigation: {
menuItems: [
{
id: 'products-main',
label: 'Products',
icon: 'inventory',
route: '/products',
order: 10,
badge: signal(0), // ✅ Signal para badge count
permissions: ['products:read'],
children: [
{
id: 'products-list',
label: 'Product List',
route: '/products/list',
order: 1,
permissions: ['products:read']
},
{
id: 'products-create',
label: 'New Product',
route: '/products/create',
order: 2,
permissions: ['products:create']
}
]
}
],
breadcrumbs: [
{ label: 'Home', route: '/' },
{ label: 'Products', route: '/products' }
],
toolbar: [
{
id: 'products-export',
label: 'Export',
icon: 'download',
action: 'products:export',
permissions: ['products:export']
}
]
},
// Routes
routes: [
{
path: 'products',
component: ProductListComponent,
permissions: ['products:read']
}
],
// Embeddable Components
embeddableComponents: [
{
id: 'product-list-widget',
componentType: ProductListWidgetComponent,
displayName: 'Product List Widget',
description: 'Displays a list of products in widget format',
category: 'widget',
permissions: ['products:read'],
inputs: [
{ name: 'maxItems', type: 'number', required: false, defaultValue: 10 },
{ name: 'category', type: 'string', required: false }
]
}
],
// Public Actions
publicActions: [
{
id: 'products:get-by-id',
displayName: 'Get Product by ID',
description: 'Retrieve a single product by its ID',
category: 'crud',
permissions: ['products:read'],
inputSchema: { productId: 'string' },
outputSchema: { product: 'object' }
},
{
id: 'products:export',
displayName: 'Export Products',
description: 'Export products to CSV/Excel',
category: 'integration',
permissions: ['products:export']
}
],
// Dependencies
dependencies: [
{ moduleId: 'inventory', version: '^1.0.0', required: true },
{ moduleId: 'pricing', version: '^2.0.0', required: false }
],
// Permissions
permissions: [
{ id: 'products:read', name: 'Read Products', category: 'products' },
{ id: 'products:create', name: 'Create Products', category: 'products' },
{ id: 'products:update', name: 'Update Products', category: 'products' },
{ id: 'products:delete', name: 'Delete Products', category: 'products' },
{ id: 'products:export', name: 'Export Products', category: 'products' }
],
// Metadata
author: 'GoGrab Team',
tags: ['ecommerce', 'inventory', 'catalog']
};
8. Servicios MVP (Implementación Básica)
NavigationService (Básico)
import { Injectable, Signal } from '@angular/core';
import { ModuleManifest, NavigationMenuItem } from '../contracts';
@Injectable({ providedIn: 'root' })
export class NavigationService {
private modules = new Map<string, ModuleManifest>();
registerModule(manifest: ModuleManifest): void {
this.modules.set(manifest.id, manifest);
}
getAllMenuItems(): NavigationMenuItem[] {
const allItems: NavigationMenuItem[] = [];
for (const manifest of this.modules.values()) {
if (manifest.navigation?.menuItems) {
allItems.push(...manifest.navigation.menuItems);
}
}
// Sort by order
return allItems.sort((a, b) => a.order - b.order);
}
getMenuItemsWithPermissions(userPermissions: string[]): NavigationMenuItem[] {
return this.getAllMenuItems().filter(item =>
this.hasPermissions(item, userPermissions)
);
}
private hasPermissions(item: NavigationMenuItem, userPermissions: string[]): boolean {
if (!item.permissions || item.permissions.length === 0) {
return true;
}
return item.permissions.every(perm => userPermissions.includes(perm));
}
}
ComponentRegistryService (Básico)
import { Injectable, Type } from '@angular/core';
import { ModuleManifest, ModuleEmbeddableComponent } from '../contracts';
@Injectable({ providedIn: 'root' })
export class ComponentRegistryService {
private components = new Map<string, ModuleEmbeddableComponent>();
registerModule(manifest: ModuleManifest): void {
if (manifest.embeddableComponents) {
for (const component of manifest.embeddableComponents) {
this.components.set(component.id, component);
}
}
}
getComponent(id: string): ModuleEmbeddableComponent | undefined {
return this.components.get(id);
}
getComponentsByCategory(category: string): ModuleEmbeddableComponent[] {
return Array.from(this.components.values()).filter(
comp => comp.category === category
);
}
getAllComponents(): ModuleEmbeddableComponent[] {
return Array.from(this.components.values());
}
}
ModuleRegistryService (Actualización)
import { Injectable } from '@angular/core';
import { ModuleManifest } from '../contracts';
import { NavigationService } from './navigation.service';
import { ComponentRegistryService } from './component-registry.service';
import { ActionRegistryService } from '@libs-core/action-registry';
@Injectable({ providedIn: 'root' })
export class ModuleRegistryService {
private modules = new Map<string, ModuleManifest>();
constructor(
private navigationService: NavigationService,
private componentRegistry: ComponentRegistryService,
private actionRegistry: ActionRegistryService
) {}
registerModule(manifest: ModuleManifest): void {
// Validar manifest básico
this.validateManifest(manifest);
// Registrar módulo
this.modules.set(manifest.id, manifest);
// Registrar navigation
if (manifest.navigation) {
this.navigationService.registerModule(manifest);
}
// Registrar embeddable components
if (manifest.embeddableComponents) {
this.componentRegistry.registerModule(manifest);
}
// Registrar public actions
if (manifest.publicActions) {
for (const action of manifest.publicActions) {
// Integration con ActionRegistryService existente
this.actionRegistry.registerAction(manifest.id, action);
}
}
}
private validateManifest(manifest: ModuleManifest): void {
if (!manifest.id || !manifest.version || !manifest.name) {
throw new Error('Invalid manifest: id, version, and name are required');
}
// Validar dependencies si existen
if (manifest.dependencies) {
for (const dep of manifest.dependencies) {
if (dep.required && !this.modules.has(dep.moduleId)) {
throw new Error(`Required dependency not found: ${dep.moduleId}`);
}
}
}
}
getModule(id: string): ModuleManifest | undefined {
return this.modules.get(id);
}
getAllModules(): ModuleManifest[] {
return Array.from(this.modules.values());
}
}
9. Cronograma MVP
Día 1 (8 horas)
09:00-13:00: Interfaces
- ✅ Actualizar
module-manifest.interface.ts - ✅ Crear interfaces de navegación
- ✅ Crear interfaces de componentes/acciones
- ✅ Actualizar barrel exports
- ✅ Actualizar
14:00-18:00: Services
- ✅
NavigationServicebásico - ✅
ComponentRegistryServicebásico - ✅ Actualizar
ModuleRegistryService - ✅ Integración con
ActionRegistryService
- ✅
Día 2 (7 horas)
09:00-13:00: Testing
- ✅ Tests unitarios para interfaces
- ✅ Tests para services (Registry, Navigation, Components)
- ✅ Coverage ≥80%
14:00-17:00: Documentación
- ✅ JSDoc completo
- ✅ Ejemplo Products module
- ✅ Actualizar
module-base.md
Total: 15 horas (~2 días)
10. Checklist de Implementación
Interfaces ✅
-
module-manifest.interface.tsactualizado (sin retrocompatibilidad) -
navigation.interface.tscreado (NavigationConfig, MenuItem, Breadcrumb, Toolbar) -
embeddable-component.interface.tscreado -
public-action.interface.tscreado -
module-dependency.interface.tscreado - Barrel
src/index.tsactualizado - Todos los tipos usan
unknownen lugar deany - Signals usados en
NavigationMenuItem.badge
Services ✅
-
NavigationServiceimplementado (agregación básica) -
ComponentRegistryServiceimplementado (registro básico) -
ModuleRegistryServiceactualizado (procesa nuevas propiedades) - Integración con
ActionRegistryServicefuncional - Validación básica de manifests implementada
Testing ✅
- Tests unitarios para interfaces (compilation + runtime)
- Tests para
ModuleRegistryService - Tests para
NavigationService - Tests para
ComponentRegistryService - Coverage ≥80% alcanzado
- Todos los tests pasan
Documentación ✅
- JSDoc completo en todas las interfaces
- Ejemplo Products module creado
-
module-base.mdactualizado con estado real - Comentarios inline en código complejo
11. Criterios de Éxito MVP
Funcional
- ✅ Módulo puede declarar navegación completa (menú, breadcrumbs, toolbar)
- ✅ NavigationService agrega navegación de múltiples módulos
- ✅ Componentes embebibles registrados y recuperables
- ✅ Acciones públicas integradas con ActionRegistry
- ✅ Dependencias validadas al registrar módulo
Técnico
- ✅ Sin uso de
any(usarunknowndonde sea necesario) - ✅ Signals usados para badges (no Observables)
- ✅ TypeScript compila sin errores con strict mode
- ✅ Coverage ≥80% en tests
- ✅ Solo propiedades esenciales para MVP funcional
Documentación
- ✅ Todas las interfaces documentadas con JSDoc
- ✅ Al menos 1 ejemplo completo (Products module)
- ✅ Documentación refleja estado actual (no futuro)
12. Próximos Pasos Post-MVP
No implementar ahora, pero considerar para futuro:
- Dynamic component loading (lazy loading de embeddables)
- Provider/Consumer DI system (inter-module services)
- ModuleConfigSchema (configuración dinámica)
- ModuleAssets (recursos del módulo)
- Route guards basados en permissions
- Navigation state management (Signal-based)
Foco MVP: Registro + Navegación básica + Componentes/Acciones catalogados
Conclusión MVP
Este plan simplificado y pragmático se enfoca en:
- ✅ Sin retrocompatibilidad - cambio directo
- ⚡ Signals primero - Angular 20 best practices
- 🎯 Tipos estrictos - no
any, estructuras bien definidas - 🚀 MVP funcional - solo lo esencial para que funcione
Entregable: Sistema de manifests completo y funcional en 2 días (~15 horas)
Próximo paso: ¿Comenzar implementación directa de interfaces?