Estructura completa de la librería compartida de Layout:
configuración de pruebas unitarias con Vitest, integración con Nx, componentes Angular standalone (Sidebar, Topbar, Footer, Tabs, Breadcrumb) y utilidades de validación de rutas.
⚡ Vitest + V8 coverage
📦 Angular 21+ standalone
🎨 Nebula-ui-kit
🧪 Unit testing ready
🔄 Sidebar + Tabs + Breadcrumb
vitest.config.ts
/// <reference types="vitest" />
import { defineConfig } from 'vitest/config';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
export default defineConfig({
root: \_\_dirname,
cacheDir: '../../node\_modules/.vite/libs/layout',
plugins: \[nxViteTsPaths()\],
test: {
name: 'layout',
watch: false,
globals: true,
environment: 'node',
include: \['{src,tests}/\*\*/\*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'\],
reporters: \['default'\],
coverage: {
reportsDirectory: '../../coverage/libs/layout',
provider: 'v8',
},
},
});
Configuración de pruebas unitarias (Vitest)
tsconfig.spec.json
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"types": \["vitest/globals", "vitest/importMeta", "vite/client", "node", "vitest"\],
"module": "preserve",
"moduleResolution": "bundler",
"allowJs": true,
"esModuleInterop": true
},
"include": \["src/\*\*/\*.test.ts", "src/\*\*/\*.spec.ts", "src/\*\*/\*.d.ts", "vite.config.ts"\]
}
** Tipado y entorno Vitest**
✔️ Tipos globales de Vitest (vitest/globals) para usar vi y assertions.
✔️ module: preserve + bundler compatibilidad con Vite.
✔️ Incluye archivos de test y definiciones de tipos (.d.ts).
✔️ Excluye la configuración de producción, asegurando que solo los tests tengan acceso a utilidades de mock.
tsconfig.lib.json
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"declaration": true,
"types": \["node", "vitest/globals", "vite/client"\],
"module": "ES2022",
"moduleResolution": "bundler"
},
"include": \["\*\*/\*.ts", "\*\*/\*.mts"\],
"exclude": \["\*\*/\*.spec.ts", "\*\*/\*.test.ts", "vitest.config.ts"\]
}
Compilación para distribución
🔹 Genera los .d.ts para consumo de otras apps.
🔹 Excluye archivos de prueba y configuración de Vitest.
🔹 Módulos ESNext + resolución bundler, óptimo para empaquetadores modernos.
🔹 Tipos mínimos (node, vite/client) para evitar contaminación con globals de test.
project.json
{
"name": "layout",
"sourceRoot": "libs/layout/src",
"projectType": "library",
"targets": {
"build": {
"executor": "@nx/js:tsc",
"outputs": \["{options.outputPath}"\],
"options": {
"outputPath": "dist/libs/layout",
"main": "libs/layout/src/index.ts",
"tsConfig": "libs/layout/tsconfig.lib.json",
"assets": \["libs/layout/\*.md"\]
}
}
}
}
Build con Nx y TypeScript compiler
✅ Ejecutor @nx/js:tsc → compila la librería a CommonJS/ESModules según configuración.
✅ Salida en dist/libs/layout lista para publicación o consumo interno.
✅ Assets: archivos markdown (README).
✅ Los tests se lanzan mediante nx test layout utilizando la configuración de Vitest.
package.json
{
"name": "@nebula/layout",
"version": "0.0.1",
"type": "commonjs",
"main": "./src/index.js",
"types": "./src/index.d.ts",
"peerDependencies": {
"@angular/core": "~21.1.0",
"@angular/common": "~21.1.0",
"@angular/router": "~21.1.0",
"@angular/material": "^21.2.2",
"nebula-ui-kit": "^0.2.1",
"@nebula/shared/data-access": "\*",
"rxjs": "^7.8.0"
},
"dependencies": { "tslib": "^2.3.0" }
}
🔗 Estrategia de dependencias
✔️ Peer dependencies evitan duplicación de Angular, nebula-ui-kit y shared libs.
✔️ @nebula/shared/data-access → servicios compartidos (Auth, etc).
✔️ Compatible con Angular 21 y Material Design.
✔️ Consumida por nebula-shell y otros micro-frontends.
eslint.config.mjs
import baseConfig from '../../eslint.config.mjs';
export default \[
...baseConfig,
{
files: \['\*\*/\*.json'\],
rules: {
'@nx/dependency-checks': \['error', {
ignoredFiles: \['{projectRoot}/vite.config.{js,ts,mjs,mts}'\],
ignoredDependencies: \[
'@nebula/accounting', '@nebula/products', '@nebula/company',
'@nebula-erp/tesoreria', '@nebula/budget'
\],
}\],
},
languageOptions: { parser: await import('jsonc-eslint-parser') },
},
\];
Control de dependencias fantasma
La regla @nx/dependency-checks verifica que todas las importaciones usadas en la librería estén declaradas en package.json.
ignoredDependencies: se ignoran módulos de negocio (accounting, products, etc) porque layout solo los usa de forma opcional o mediante lazy loading. Previene errores de instalación y mejora la mantenibilidad del monorepo.
index.ts
export { LayoutComponent } from './lib/feature-layout/src/lib/layout/layout.component';
export { TabContainerComponent } from './lib/feature-layout/src/lib/tab-container/tab-container.component';
export { LAYOUT\_ROUTES } from './lib/layout.routes';
export { SidebarStateService } from './lib/data-access/src/lib/sidebar-state.service';
export { BreadcrumbSyncService } from './lib/data-access/src/lib/breadcrumb-sync.service';
export \* from './lib/domain/src/lib/breadcrumb.model';
export \* from './lib/domain/src/lib/sidebar.model';
export type { TabContainerConfig } from './lib/feature-layout/src/lib/tab-container/tab-container.component';
API pública consumible
📌 Componentes: LayoutComponent (estructura principal), TabContainerComponent (gestión de pestañas).
📌 Servicios: SidebarStateService, BreadcrumbSyncService (comunicación entre componentes).
📌 Modelos: BreadcrumbItem, SidebarMenuItem, tipos de configuración.
📌 LAYOUT_ROUTES → rutas hijas para módulos de tesorería, contabilidad, presupuesto.
route-validator.ts
export const VALID\_ROUTES = \[
'bank-account/list', 'embargo/list', 'income-voucher/list',
'budget-parameters/list', 'resource-type/list', ...
\] as const;
export function isRouteValid(route: string): boolean {
const cleanRoute = route.replace(/^\\/layout\\/(accounting|tesorería|budget)\\//i, '')
.split('?')\[0\].toLowerCase().trim();
return VALID\_ROUTES.some(valid => cleanRoute === valid.toLowerCase());
}
export function getModuleFromRoute(route: string): 'accounting' | 'tesorería' | 'budget' | null {
if (route.includes('/budget/')) return 'budget';
if (route.includes('/tesorería/')) return 'tesorería';
if (route.includes('/accounting/')) return 'accounting';
return null;
}
Validación semántica de rutas E2E
✔️ Lista blanca de rutas funcionales (más de 30 endpoints validados).
✔️ isRouteValid limpia prefijos /layout/ y parámetros, usado en SidebarWrapper para mostrar advertencias si la ruta no está disponible.
✔️ getModuleFromRoute identifica el módulo activo (tesorería, presupuesto, accounting).
sidebar-wrapper.component.ts
loadMenu(): void {
this.sidebarService.getMenu().subscribe({
next: (data) => {
this.menuItems = this.processMenuRoutesStatic(data);
this.cdr.markForCheck();
}
});
}
onItemClick(item: any): void {
if (!isRouteValid(item.route)) {
this.showSimpleToast(\`"${item.label}" aún no está disponible\`);
return;
}
this.navService.addTabFromMenu({ label: item.label, route: item.route });
}
Comportamiento inteligente
🔸 Carga de menú: obtiene elementos desde SidebarService (API o estático).
🔸 Protección de rutas: valida cada ruta con isRouteValid antes de abrir pestaña.
🔸 Toast amigable: si la ruta aún no está implementada, muestra notificación no intrusiva.
🔸 Integración con NebulaNavigationService: añade pestañas al TabContainer.
tab-container.component.ts
<lib-tabs [tabs]="getFilteredTabs()" [activeId]="activeId()" (tabChange)="onTabChange($event)">
🎛️ Gestión de pestañas por módulo
✔️ Filtra tabs según el módulo actual (accounting / tesorería / budget).
✔️ Sincroniza la URL activa con la pestaña seleccionada.
✔️ Scroll horizontal con botones de navegación y detección de desbordamiento.
✔️ Actualiza breadcrumbs automáticamente al cambiar de pestaña.
✔️ Previene cierre de la pestaña base (dashboard del módulo).
🍞
data-access services
@Injectable({ providedIn: 'root' })
export class BreadcrumbSyncService {
private breadcrumbSubject = new BehaviorSubject<BreadcrumbItem\[\]>(\[\]);
breadcrumb$ = this.breadcrumbSubject.asObservable();
update(items: BreadcrumbItem\[\]) { this.breadcrumbSubject.next(items); }
}
@Injectable({ providedIn: 'root' })
export class SidebarStateService {
private expandedSubject = new BehaviorSubject<boolean>(true);
isExpanded$ = this.expandedSubject.asObservable();
setState(expanded: boolean, visible: boolean) { ... }
}
🔄 Comunicación entre componentes
🔹 BreadcrumbSyncService: permite que cualquier componente (Layout, TabContainer, rutas hijas) actualice la ruta de navegación superior.
🔹 SidebarStateService: estado compartido del colapso del menú lateral, persistente entre navegaciones.
🔹 Ambos son providedIn: 'root', disponibles globalmente en la app.
📁 domain/
Modelos: BreadcrumbItem, SidebarMenuItem, interfaces.
⚙️ data-access/
Servicios: SidebarState, BreadcrumbSync, SidebarService (API).
🎨 feature-layout/
Componentes: Layout, TabContainer, SidebarWrapper, Topbar, Footer.
🛠️ utils/
route-validator, normalización de rutas, helpers.
🧪 Pruebas unitarias: Cada componente y servicio puede ser testeado con Vitest. Ejemplo de comando: nx test layout --coverage.
🔌 Integración con nebula-shell: La librería layout es consumida por la aplicación principal y se encarga de la estructura común (header, sidebar, footer, tabs) y la lógica de navegación multi-módulo.
🧩 @nebula/layout · Documentación de configuración completa: Vitest, TypeScript, Nx, dependencias, servicios compartidos y componentes reutilizables para una experiencia de usuario unificada.
route-validator.ts
export const VALID\_ROUTES = \[
'bank-account/list', 'embargo/list', 'income-voucher/list',
'budget-parameters/list', 'resource-type/list', ...
\] as const;
export function isRouteValid(route: string): boolean {
const cleanRoute = route.replace(/^\\/layout\\/(accounting|tesorería|budget)\\//i, '')
.split('?')\[0\].toLowerCase().trim();
return VALID\_ROUTES.some(valid => cleanRoute === valid.toLowerCase());
}
export function getModuleFromRoute(route: string): 'accounting' | 'tesorería' | 'budget' | null {
if (route.includes('/budget/')) return 'budget';
if (route.includes('/tesorería/')) return 'tesorería';
if (route.includes('/accounting/')) return 'accounting';
return null;
}
Validación semántica de rutas E2E
✔️ Lista blanca de rutas funcionales (más de 30 endpoints validados).
✔️ isRouteValid limpia prefijos /layout/ y parámetros, usado en SidebarWrapper para mostrar advertencias si la ruta no está disponible.
✔️ getModuleFromRoute identifica el módulo activo (tesorería, presupuesto, accounting).
📂
sidebar-wrapper.component.ts
loadMenu(): void {
this.sidebarService.getMenu().subscribe({
next: (data) => {
this.menuItems = this.processMenuRoutesStatic(data);
this.cdr.markForCheck();
}
});
}
onItemClick(item: any): void {
if (!isRouteValid(item.route)) {
this.showSimpleToast(`"${item.label}" aún no está disponible`);
return;
}
this.navService.addTabFromMenu({ label: item.label, route: item.route });
}
🧠 Comportamiento inteligente
🔸 Carga de menú: obtiene elementos desde SidebarService (API o estático).
🔸 Protección de rutas: valida cada ruta con isRouteValid antes de abrir pestaña.
🔸 Toast amigable: si la ruta aún no está implementada, muestra notificación no intrusiva.
🔸 Integración con NebulaNavigationService: añade pestañas al TabContainer.
📑
tab-container.component.ts
<lib-tabs [tabs]="getFilteredTabs()" [activeId]="activeId()" (tabChange)="onTabChange($event)">
🎛️ Gestión de pestañas por módulo
✔️ Filtra tabs según el módulo actual (accounting / tesorería / budget).
✔️ Sincroniza la URL activa con la pestaña seleccionada.
✔️ Scroll horizontal con botones de navegación y detección de desbordamiento.
✔️ Actualiza breadcrumbs automáticamente al cambiar de pestaña.
✔️ Previene cierre de la pestaña base (dashboard del módulo).
📁 domain/
Modelos: BreadcrumbItem, SidebarMenuItem, interfaces.
⚙️ data-access/
Servicios: SidebarState, BreadcrumbSync, SidebarService (API).
🎨 feature-layout/
Componentes: Layout, TabContainer, SidebarWrapper, Topbar, Footer.
🛠️ utils/
route-validator, normalización de rutas, helpers.