Add New Library

Documentation for Developers

How to Add a New Library to the Monorepo

This guide documents the complete process of adding a new library to our NX monorepo, following the established patterns and architecture principles.

📅 Last Updated: October 23, 2025
✨ Recent Updates:

  • Updated to use custom builder instead of @nx/angular:ng-packagr-lite
  • Updated StorageService API (no constructor arguments, use set/get methods)
  • Added guidance on lazy-loaded libraries and dynamic imports
  • Updated theme-system import paths and library names
  • Clarified nested library naming conventions
  • Added incremental: false recommendation for TypeScript config
  • Updated Flash Persistence examples to use autoFlashState()

Prerequisites

Before creating a new library, ensure you have:

Step 1: Generate the Library

Use the NX Angular library generator:

npx nx g @nx/angular:library my-library-name

Configuration Options

When prompted, use these settings:

Step 2: Configure Library Structure

Standard Library Structure

libs/my-library-name/
├── src/
│   ├── lib/
│   │   ├── components/      # Angular components
│   │   │   ├── index.ts       # Add barrel file for components
│   │   ├── services/        # Injectable services
│   │   │   ├── index.ts       # Add barrel file for services
│   │   ├── models/          # Type definitions
│   │   │   ├── index.ts       # Add barrel file for models
│   │   ├── directives/      # Custom directives
│   │   │   ├── index.ts       # Add barrel file for directives
│   │   ├── pipes/           # Custom pipes
│   │   │   ├── index.ts       # Add barrel file for pipes
│   │   ├── utils/           # Utility functions
│   │   └── index.ts         # Internal exports
│   └── index.ts             # Public API entry point
├── project.json             # NX project configuration
├── tsconfig.json            # TypeScript configuration
├── tsconfig.lib.json        # Library-specific TypeScript config
├── tsconfig.spec.json       # Test TypeScript config
├── jest.config.ts           # Jest configuration
├── README.md                # Library documentation
└── package.json             # Package configuration

Step 3: Configure TypeScript and Build Settings

⚠️ CRITICAL PATH MAPPING SETUP
This step is THE MOST COMMON SOURCE OF BUILD ERRORS!
If you're building a library that depends on other libraries, you MUST configure explicit path mappings in your tsconfig.lib.json.
See Setup NX Library Guide for the complete checklist.

Update tsconfig.base.json

Add your library to the path mapping:

{
  "compilerOptions": {
    "paths": {
      // ... existing paths ...
      "@my-library-name": ["libs/my-library-name/src/index.ts"],
      "@my-library-name/*": ["libs/my-library-name/src/*"]
    }
  }
}

Configure Library TypeScript (tsconfig.lib.json)

⚠️ STOP! READ THIS FIRST!
If your library imports from @libs-foundational/core-models, @storage-system, or ANY other library in the monorepo,
you MUST add explicit paths mapping to this config. See the example below.

{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    // Sometimes if the library is not under the libs directory, you need to specify the outDir - for example with our theme-system/ and with our core/ libraries:
    "outDir": "../../dist/libs/my-library-name",
    "declaration": true,
    "declarationMap": true,
    "inlineSources": true,
    "incremental": false,  // ⚠️ Set to false to avoid build cache issues
    "types": ["node"],
    
    // ⚠️⚠️⚠️ CRITICAL: Add path mappings for dependencies ⚠️⚠️⚠️
    "paths": {
      // For libraries using @nx/js:tsc (outputs to dist/libs/X/src/)
      "@libs-foundational/core-models": ["../../dist/libs/core-models/src/index"],
      "@libs-foundational/core-models/*": ["../../dist/libs/core-models/src/*"],
      
      // For libraries using custom builder (outputs to dist/libs/X/)
      "@storage-system": ["../../dist/libs/storage-system/index"],
      "@storage-system/*": ["../../dist/libs/storage-system/*"]
      
      // Add more as needed - check actual dist output with: ls -la dist/libs/
    }
  },
  "files": [],
  "include": ["src/**/*.ts"],
  "exclude": [
    "src/**/*.spec.ts",
    "src/**/*.test.ts",
    "jest.config.ts",
    "vite.config.ts",
    "vite.config.mts",
    "vitest.config.ts",
    "vitest.config.mts"
  ]
}

Important Notes:

Configure Library TypeScript (tsconfig.spec.json)

{
   // Sometimes if the library is not under the libs directory, you need to specify the outDir - for example with our theme-system/ and with our core/ libraries:
  "extends": "../../tsconfig.templates/lib-coordinator.json",
  "compilerOptions": {
    "strict": true,
    "noImplicitOverride": true,
    "noPropertyAccessFromIndexSignature": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true
  },
  "include": [
    "src/**/*.ts"
  ],
  "exclude": [
    "src/**/*.spec.ts",
    "src/**/*.test.ts"
  ]
}

Configure Build Target (project.json)

IMPORTANT: We use a custom builder for Angular libraries with enhanced TypeScript support!

{
  "name": "my-library-name",
  "sourceRoot": "libs/my-library-name/src",
  "prefix": "lib",
  "targets": {
    "build": {
      "executor": "nx:run-commands",
      "outputs": ["{workspaceRoot}/dist/libs/my-library-name"],
      "options": {
        "command": "node tools/scripts/build/build-angular-library.js my-library-name"
      },
      "configurations": {
        "production": {
          "command": "node tools/scripts/build/build-angular-library.js my-library-name"
        }
      },
      "defaultConfiguration": "production"
    },
    "test": {
      "executor": "@nx/jest:jest",
      "outputs": ["coverage/libs/my-library-name"],
      "options": {
        "jestConfig": "libs/my-library-name/jest.config.ts"
      }
    },
    "lint": {
      "executor": "@nx/eslint:lint",
      "outputs": ["eslint-report.json"],
      "options": {
        "lintFilePatterns": [
          "libs/my-library-name/**/*.ts",
          "libs/my-library-name/**/*.html"
        ]
      }
    }
  },
  "tags": ["npm:public"]
}

For Nested Libraries (e.g., libs/theme-system/theme-runtime/):

Why We Use a Custom Builder

Our custom builder (build-angular-library.js) provides several advantages over the standard @nx/angular:ng-packagr-lite:

  1. Enhanced TypeScript Support: Ensures experimentalDecorators and emitDecoratorMetadata are properly set for NGXS and Angular decorators
  2. Better Error Messages: Clearer error reporting during compilation
  3. Consistent Builds: Standardized build process across all libraries
  4. Path Resolution: Better handling of complex path mappings in our monorepo

The custom builder handles:

Step 4: Configure Package for Publishing

Create ng-package.json

{
  "$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
  "dest": "../../dist/libs/my-library-name",
  "lib": {
    "entryFile": "src/index.ts"
  }
}

Note: We use src/index.ts as the standard entry point for all libraries (not public-api.ts).

Create package.json

IMPORTANT: Library names should be simple: @my-library-name, NOT @widgets-editor/my-library-name

{
  "name": "@my-library-name",
  "version": "0.0.1",
  "description": "Description of my library",
  "keywords": ["angular", "library", "typescript"],
  "author": "GoGrab Space",
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/gograb-space/editor.git",
    "directory": "libs/my-library-name"
  },
  "peerDependencies": {
    "@angular/common": "^20.0.0",
    "@angular/core": "^20.0.0"
  },
  "dependencies": {
    "tslib": "^2.3.0"
  },
  "sideEffects": false
}

Naming Convention:

Step 5: Integrate Core Systems (ESSENTIAL) ⭐

Before implementing any logic, configure integration with our three core systems:

🧠 Flash-Persistence Integration

IMPORTANT: We use autoFlashState() for state management, NOT NGXS!

// libs/my-library-name/src/lib/services/my-library.service.ts
import { Injectable, computed } from '@angular/core';
import { autoFlashState } from '@flash-persistence';
import type { MyLibraryConfig } from '../models';

@Injectable({ providedIn: 'root' })
export class MyLibraryService {
  // ✨ ZERO configuration - just use autoFlashState!
  private readonly _config = autoFlashState<MyLibraryConfig>('my-library/config', {
    theme: 'default',
    enabled: true
  });

  // 📊 Public Readonly Computed Signals
  readonly config = computed(() => this._config());
  readonly isReady = computed(() => this._config().enabled);

  // Simple methods to update state
  async updateConfig(config: Partial<MyLibraryConfig>): Promise<void> {
    this._config.update(current => ({ ...current, ...config }));
  }
}

No providers needed! Flash Persistence is already configured in your app's main.ts with provideAutoFlashPersistence().

🗄️ Storage-System Integration

Configure intelligent storage:

// libs/my-library-name/src/lib/services/my-library.service.ts
import { Injectable, inject, signal, computed } from '@angular/core';
import { StorageService } from '@storage-system';

@Injectable({ providedIn: 'root' })
export class MyLibraryService {
  private readonly storage = inject(StorageService);
  
  // Internal signal for config
  private readonly _config = signal<MyLibraryConfig | null>(null);
  
  // Public readonly config signal
  readonly config = computed(() => this._config());
  readonly isReady = computed(() => this._config() !== null);
  
  constructor() {
    // Load initial config from storage
    this.loadConfig();
  }
  
  private async loadConfig(): Promise<void> {
    try {
      const stored = await this.storage.get<MyLibraryConfig>('my-library:config');
      this._config.set(stored || {
        theme: 'default',
        enabled: true
      });
    } catch (error) {
      console.error('Failed to load config:', error);
      this._config.set({ theme: 'default', enabled: true });
    }
  }
  
  async updateConfig(config: Partial<MyLibraryConfig>): Promise<void> {
    const current = this._config();
    const updated = { ...current, ...config };
    await this.storage.set('my-library:config', updated);
    this._config.set(updated);
  }
}

Important Storage API Notes:

🎨 Theme-System Integration

Our theme system consists of three specialized libraries:

// libs/my-library-name/src/lib/services/my-library.service.ts
import { Injectable, inject } from '@angular/core';
import { ThemeManagerService } from '@theme-system';  // ✅ Main theme service

@Injectable({ providedIn: 'root' })
export class MyLibraryService {
  private readonly themeManager = inject(ThemeManagerService);
  
  // Theme is automatically available - no configuration needed!
  // The theme service is already provided in your app's main.ts
}

Available Theme Libraries:

Library SCSS Integration:

// libs/my-library-name/src/lib/components/my-component/my-component.component.scss
// ✅ CORRECT: Import theme styles using the configured path
@use 'libs/theme-system/theme-styles/src/lib/styles/index' as *;

// Or use the shorter path if stylePreprocessorOptions is configured:
// @use 'index' as *;

.my-component {
  // ✅ Use CSS variables from the theme system
  background: var(--ux-surface);
  color: var(--ux-on-surface);
  border: 1px solid var(--ux-outline);
  padding: var(--ux-spacing-md);
  border-radius: var(--ux-radius-md);
  
  // ✅ Prefer utility classes in your template HTML over custom SCSS
  // Example: Use 'ux-shadow' and 'ux-transition' classes in template
}

Configure SCSS Include Paths (Optional):

Add to your library's project.json for shorter imports:

{
  "targets": {
    "build": {
      "options": {
        "stylePreprocessorOptions": {
          "includePaths": ["libs/theme-system/theme-styles/src/lib/styles"]
        }
      }
    }
  }
}

Step 6: Implement Safe Typing with Models Directory

ALWAYS Create Models Directory First

# Create models directory structure
mkdir -p libs/my-library-name/src/lib/models
mkdir -p libs/my-library-name/src/lib/state
mkdir -p libs/my-library-name/src/lib/providers
touch libs/my-library-name/src/lib/models/index.ts
touch libs/my-library-name/src/lib/models/my-library.types.ts
touch libs/my-library-name/src/lib/state/my-library.state.ts

Define All Types in Models Directory

// libs/my-library-name/src/lib/models/my-library.types.ts
import type { SafeAny } from '@libs-foundational/core-models'; 

export interface MyLibraryConfig {
  name: string;
  theme: 'light' | 'dark' | 'auto';
  enabled: boolean;
  options: Record<string, SafeAny>;
  storage?: {
    namespace?: string;
    strategy?: 'speed' | 'balanced' | 'durable';
  };
}

export interface MyLibraryState {
  config: MyLibraryConfig;
  isInitialized: boolean;
  error?: string;
}

export interface MyComponentProps {
  data: MyLibraryConfig;
  disabled?: boolean;
  className?: string;
}

Create Service with Flash-Persistence State

DO NOT use NGXS! Use autoFlashState() instead:

// libs/my-library-name/src/lib/services/my-library-state.service.ts
import { Injectable, computed } from '@angular/core';
import { autoFlashState } from '@flash-persistence';
import type { MyLibraryConfig } from '../models';

/**
 * 🚀 Library State Service with Flash Persistence
 * 
 * ZERO configuration - just use autoFlashState!
 * Automatically persists all state across page refreshes.
 */
@Injectable({ providedIn: 'root' })
export class MyLibraryStateService {
  // ✨ Auto Flash State - ZERO CONFIGURATION!
  private readonly _config = autoFlashState<MyLibraryConfig>('my-library/config', {
    name: 'MyLibrary',
    theme: 'auto',
    enabled: true,
    options: {}
  });

  private readonly _isInitialized = autoFlashState<boolean>('my-library/initialized', false);

  // 📊 Public Readonly Computed Signals (replaces NGXS selectors)
  readonly config = computed(() => this._config());
  readonly isInitialized = computed(() => this._isInitialized());
  readonly isReady = computed(() => {
    return this._isInitialized() && this._config().enabled;
  });

  // Simple methods to update state (replaces NGXS actions)
  async updateConfig(config: Partial<MyLibraryConfig>): Promise<void> {
    this._config.update(current => ({ ...current, ...config }));
  }

  async initialize(): Promise<void> {
    this._isInitialized.set(true);
  }
}

Why autoFlashState instead of NGXS?

Export from Models Index

// libs/my-library-name/src/lib/models/index.ts
export * from './my-library.types';

Import from Models in Services/Components

// libs/my-library-name/src/lib/services/my-service.ts
import { MyLibraryConfig } from '../models';  // ← Use relative import from models

// libs/my-library-name/src/lib/components/my-component.ts
import { MyComponentProps } from '../models';  // ← Use relative import from models

Avoid Circular Dependencies

  1. Don't import from libraries at the same level
  2. Use core for shared interfaces
  3. Follow dependency hierarchy: Core → UI → Feature → App
// Good ✅
import type { SafeAny } from '@libs-foundational/core-models'; 

// Avoid ❌ - Don't import from peer libraries
// import { SomeType } from '@other-library';

Step 6: Create Public API

Define Public Exports (src/index.ts)

// Export all public models FIRST (other modules depend on these)
export * from './lib/models';

// Export all public components
export * from './lib/components';

// Export all public services
export * from './lib/services';

// Export all public directives
export * from './lib/directives';

// Export all public pipes
export * from './lib/pipes';

// Export utilities if public
export * from './lib/utils/public-utils';

// Re-export types from core for convenience
export type { SafeAny } from '@libs-foundational/core-models'; 

Component Export Example

// libs/my-library-name/src/lib/components/index.ts
export { MyComponent } from './my-component/my-component.component';
export { AnotherComponent } from './another-component/another-component.component';

// Export component types
export type { MyComponentProps } from '../models/my-component-props';

Step 7: Implement Modern Components with Full Integration

Modern Standalone Component with Full Theme Integration [[memory:6454111]]

// libs/my-library-name/src/lib/components/my-component/my-component.component.ts
import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, inject, signal, computed, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { StorageService } from '@storage-system';
import { ThemeManagerService } from '@theme-system';
import type { SafeAny } from '@libs-foundational/core-models'; 
import { MyComponentConfig } from '../../models';
import { MyLibraryService } from '../../services/my-library.service';
import { MyLibraryStateService } from '../../services/my-library-state.service';

@Component({
  selector: 'lib-my-component',
  standalone: true,
  imports: [CommonModule],
  template: `
    <div 
      class="my-component ux-bg-surface ux-text-on-surface ux-p-4 ux-rounded-lg ux-shadow" 
      [class.ux-opacity-50]="disabled()"
      [class.ux-bg-primary]="isReady()"
    >
      @if (isReady()) {
        <div class="component-header ux-flex ux-items-center ux-space-x-3 ux-mb-4">
          @if (config().icon) {
            <i [class]="'fa fa-' + config().icon" class="ux-text-primary" aria-hidden="true"></i>
          }
          <h3 class="ux-text-lg ux-font-semibold ux-text-on-surface">{{ config().title }}</h3>
        </div>
        
        <div class="component-content ux-mb-4">
          <ng-content></ng-content>
        </div>
        
        <div class="component-actions ux-flex ux-space-x-2">
          @for (action of actions(); track action.id) {
            <button 
              type="button"
              class="ux-btn ux-btn-primary ux-px-4 ux-py-2 ux-rounded ux-transition"
              [class.ux-opacity-50]="disabled() || action.disabled"
              [disabled]="disabled() || action.disabled"
              (click)="handleAction(action)"
              [attr.aria-label]="action.label"
            >
              {{ action.label }}
            </button>
          }
        </div>
      } @else {
        <div class="component-loading ux-flex ux-items-center ux-justify-center ux-p-8">
          <i class="fa fa-spinner fa-spin ux-text-primary ux-mr-2" aria-hidden="true"></i>
          <span class="ux-text-on-surface">Loading...</span>
        </div>
      }
    </div>
  `,
  styleUrls: ['./my-component.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class MyComponent implements OnInit {
  private readonly storage = inject(StorageService);
  private readonly libraryService = inject(MyLibraryService);
  private readonly stateService = inject(MyLibraryStateService);

  // Inputs with signals
  private _config = signal<MyComponentConfig>({
    title: 'Default Title',
    icon: 'cog',
    actions: []
  });
  
  @Input() set config(value: MyComponentConfig) {
    this._config.set(value);
  }
  
  private _disabled = signal(false);
  @Input() set disabled(value: boolean) {
    this._disabled.set(value);
  }

  // Outputs
  @Output() actionTriggered = new EventEmitter<SafeAny>();
  @Output() configChanged = new EventEmitter<MyComponentConfig>();

  // Computed properties
  config = computed(() => this._config());
  disabled = computed(() => this._disabled());
  
  // State from Flash Persistence service
  readonly libraryConfig = this.stateService.config;
  readonly isReady = computed(() => this.stateService.isReady());
  
  // Storage-backed preferences (loaded asynchronously)
  private readonly _userPreferences = signal<SafeAny>({});
  readonly userPreferences = computed(() => this._userPreferences());
  
  // Theme integration
  readonly themeStyles = computed(() => {
    const theme = this.libraryConfig().theme || 'auto';
    const preferences = this.userPreferences();
    
    return {
      '--component-theme': theme,
      '--component-accent': preferences.accentColor || 'var(--ux-primary)'
    };
  });

  // Actions computed from config and state
  readonly actions = computed(() => {
    const baseActions = this.config().actions || [];
    const isEnabled = this.libraryConfig().enabled;
    
    return baseActions.map(action => ({
      ...action,
      disabled: !isEnabled || action.disabled
    }));
  });

  async ngOnInit(): Promise<void> {
    // Initialize component with state
    await this.libraryService.initialize();
    
    // Load user preferences from storage
    try {
      const prefs = await this.storage.get<SafeAny>('my-component:preferences');
      if (prefs) {
        this._userPreferences.set(prefs);
      }
    } catch (error) {
      console.error('Failed to load preferences:', error);
    }
  }

  async handleAction(action: SafeAny): Promise<void> {
    if (this.disabled() || action.disabled) return;

    // Store action in intelligent storage
    await this.storage.set(`my-component:last-action:${action.id}`, {
      action: action.id,
      timestamp: new Date().toISOString(),
      config: this.config()
    });

    // Emit action event
    this.actionTriggered.emit({
      action: action.id,
      data: action.data,
      timestamp: new Date()
    });

    // Update state if needed
    if (action.updateState) {
      await this.stateService.updateConfig(action.stateUpdate);
    }
  }
}

Models Example (Required)

// libs/my-library-name/src/lib/models/my-component.types.ts
import type { SafeAny } from '@libs-foundational/core-models'; 

export interface MyComponentConfig {
  title: string;
  data: SafeAny;
  options?: Record<string, SafeAny>;
}

Corresponding SCSS File:

// libs/my-library-name/src/lib/components/my-component/my-component.component.scss
// ✅ CORRECT: Use full path or configured include path
@use 'libs/theme-system/theme-styles/src/lib/styles/index' as *;
// OR if stylePreprocessorOptions is configured:
// @use 'index' as *;

:host {
  display: block;
  width: 100%;
  
  .my-component {
    // ✅ Minimal custom styles - rely on theme utility classes in template
    position: relative;
    
    &.ux-bg-primary {
      // Enhanced styling when ready
      transform: translateY(-1px);
      box-shadow: var(--ux-shadow-lg);
    }
    
    // Only add styles not available in theme utilities
    .component-loading {
      min-height: 120px; // Custom min-height for loading state
    }
  }
}

Key Theme Integration Principles:

Service Example

// libs/my-library-name/src/lib/services/my-service.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import type { SafeAny } from '@libs-foundational/core-models'; 
import { MyServiceConfig } from '../models';  // ← Import from models, NO local types!

@Injectable({
  providedIn: 'root'
})
export class MyService {
  private configSubject = new BehaviorSubject<MyServiceConfig | null>(null);
  private dataSubject = new BehaviorSubject<SafeAny[]>([]);
  
  config$ = this.configSubject.asObservable();
  data$ = this.dataSubject.asObservable();
  
  initialize(config: MyServiceConfig): void {
    this.configSubject.next(config);
  }
  
  async loadData(): Promise<SafeAny[]> {
    const config = this.configSubject.value;
    if (!config) {
      throw new Error('Service not initialized');
    }
    
    try {
      // Implementation logic here
      const data: SafeAny[] = [];
      this.dataSubject.next(data);
      return data;
    } catch (error) {
      console.error('Failed to load data:', error);
      throw error;
    }
  }
  
  updateData(newData: SafeAny[]): void {
    this.dataSubject.next(newData);
  }
}

Service Types (In Models Directory)

// libs/my-library-name/src/lib/models/my-service.types.ts
export interface MyServiceConfig {
  apiUrl: string;
  timeout?: number;
  retries?: number;
}

Step 8: Testing Setup

Configure Jest (jest.config.ts)

export default {
  displayName: 'my-library-name',
  preset: '../../jest.preset.js',
  setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
  coverageDirectory: '../../coverage/libs/my-library-name',
  transform: {
    '^.+\\.(ts|mjs|js|html)$': [
      'jest-preset-angular',
      {
        tsconfig: '<rootDir>/tsconfig.spec.json',
        stringifyContentPathRegex: '\\.(html|svg)$',
      },
    ],
  },
  transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
  snapshotSerializers: [
    'jest-preset-angular/build/serializers/no-ng-attributes',
    'jest-preset-angular/build/serializers/ng-snapshot',
    'jest-preset-angular/build/serializers/html-comment',
  ],
};

Component Test Example

// libs/my-library-name/src/lib/components/my-component/my-component.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MyComponent, MyComponentConfig } from './my-component.component';

describe('MyComponent', () => {
  let component: MyComponent;
  let fixture: ComponentFixture<MyComponent>;
  
  const mockConfig: MyComponentConfig = {
    title: 'Test Component',
    data: { test: true }
  };
  
  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [MyComponent]
    }).compileComponents();
    
    fixture = TestBed.createComponent(MyComponent);
    component = fixture.componentInstance;
    component.config = mockConfig;
    fixture.detectChanges();
  });
  
  it('should create', () => {
    expect(component).toBeTruthy();
  });
  
  it('should display the title', () => {
    const compiled = fixture.nativeElement;
    expect(compiled.querySelector('h3').textContent).toContain('Test Component');
  });
  
  it('should emit actionTriggered when clicked', () => {
    spyOn(component.actionTriggered, 'emit');
    
    const button = fixture.nativeElement.querySelector('button');
    button.click();
    
    expect(component.actionTriggered.emit).toHaveBeenCalled();
  });
});

Step 9: Documentation

🚨 CRITICAL: No Local Types Policy

NEVER define types/interfaces locally in service or component files!

✅ Correct Pattern: Use Models Directory

// ✅ GOOD: In libs/my-library/src/lib/models/my-types.ts
export interface MyComponentConfig {
  title: string;
  data: SafeAny;
}

// ✅ GOOD: In libs/my-library/src/lib/services/my-service.ts  
import { MyComponentConfig } from '../models/my-types';

❌ Wrong Pattern: Local Type Definitions

// ❌ BAD: Local interface in service file
interface MyComponentConfig {  // <- Never do this!
  title: string;
  data: SafeAny;
}

Required Structure: Models Directory

Every library MUST have a models/ directory:

libs/my-library-name/
├── src/
│   ├── lib/
│   │   ├── components/
│   │   ├── services/  
│   │   ├── models/              # ← REQUIRED
│   │   │   ├── my-types.ts      # ← All types go here
│   │   │   └── index.ts         # ← Export all types
│   │   └── index.ts
│   └── index.ts                 # ← Export models (library entry point)

Create README.md

# @my-library-name

Brief description of what this library does.

## Installation

```bash
yarn add @my-library-name

Usage

Basic Usage

import { MyComponent, MyService } from '@my-library-name';

// Component usage
@Component({
  imports: [MyComponent],
  template: `
    <lib-my-component 
      [config]="myConfig"
      (actionTriggered)="onAction($event)">
    </lib-my-component>
  `
})
export class MyAppComponent {
  myConfig = {
    title: 'Hello World',
    data: { example: true }
  };
  
  onAction(event: any): void {
    console.log('Action triggered:', event);
  }
}

API Reference

Components

MyComponent

Property Type Description
config MyComponentConfig Configuration object
disabled boolean Whether component is disabled

Services

MyService

Methods for managing data and configuration.

Development

See the monorepo documentation for development guidelines.


## Step 10: Build and Testing

### Build the Library

```bash
# Build the library
npx nx build my-library-name

# Build with dependencies
npx nx build my-library-name --with-deps

Test the Library

# Run tests
npx nx test my-library-name

# Run tests with coverage
npx nx test my-library-name --coverage

# Run linting
npx nx lint my-library-name

Verify Library Integration

Create a test component in an app to verify the library works:

// In any app component
import { MyComponent } from '@my-library-name';

@Component({
  imports: [MyComponent],
  template: `<lib-my-component [config]="testConfig"></lib-my-component>`
})
export class TestComponent {
  testConfig = {
    title: 'Library Test',
    data: { working: true }
  };
}

Step 11: Dependency Management

Understanding Library Dependencies

  1. core (Foundation)

    • No dependencies
    • Contains all shared type definitions
    • Used by all other libraries
  2. Core Libraries

    • Can depend on core
    • Should not depend on other core libraries
  3. UI Libraries

    • Can depend on core and core libraries
    • Should not depend on feature libraries

Dependency Declaration

In your library's project.json, declare dependencies:

{
  "implicitDependencies": ["core-models"],
  "tags": ["npm:public", "scope:ui", "type:library"]
}

Lazy-Loaded Libraries and Dynamic Imports

Some libraries are marked as lazy-loaded to optimize bundle size. If you need to use a lazy-loaded library (like @storage-system), you must use dynamic imports:

// ❌ WRONG: Static import of lazy-loaded library
import { provideIntelligentStorage } from '@storage-system';

// ✅ CORRECT: Dynamic import
export async function setupStorage() {
  const { provideIntelligentStorage } = await import('@storage-system');
  return provideIntelligentStorage();
}

Common Lazy-Loaded Libraries:

When to Use Static vs Dynamic Imports:

Common Patterns and Best Practices

1. Consistent Naming

2. Type Safety

3. Component Design

4. Service Design

5. Testing

Examples in Our Monorepo

core Library

Prompt-System Library (new)

Troubleshooting

Build Issues

  1. ❌ ERROR: "Cannot find module '@libs-foundational/core-models' or '@storage-system'" ⚠️ MOST COMMON ERROR

    • Cause: Missing or incorrect path mappings in tsconfig.lib.json
    • Solution: Add explicit path mappings pointing to dist/ folders (see Step 3 above)
    • Check: Run ls -la dist/libs/core-models/ to see actual output structure
    • Example:
      "paths": {
        "@libs-foundational/core-models": ["../../dist/libs/core-models/src/index"],
        "@storage-system": ["../../dist/libs/storage-system/index"]
      }
      
  2. Circular Dependency Errors

    • Move shared types to @core
    • Avoid peer library imports
  3. Type Resolution Issues

    • Verify path mappings in tsconfig.base.json
    • Ensure proper export declarations
  4. Build Order Issues

    • Build dependencies first
    • Use --with-deps flag when needed
  5. "Invalid TypeScript program emit for .tsbuildinfo"

    • Set "incremental": false in tsconfig.lib.json
    • Delete the .tsbuildinfo file: rm -rf dist/libs/my-library/.tsbuildinfo
    • Rebuild: yarn nx build my-library --skip-nx-cache
  6. "Static imports of lazy-loaded libraries are forbidden"

    • Use dynamic imports: const { MyService } = await import('@my-library');
    • See Lazy-Loaded Libraries section
  7. "Cannot find module '@my-library'"

    • Check tsconfig.base.json has correct path mapping
    • For nested libraries, ensure project.json name matches directory structure
    • Example: libs/theme-system/theme-runtime/ → "name": "theme-system-theme-runtime"
  8. Decorator Errors (TS1241, TS2449)

    • Ensure using custom builder (not @nx/angular:ng-packagr-lite)
    • Custom builder automatically sets experimentalDecorators: true
  9. SCSS Import Errors

    • Use full path: @use 'libs/theme-system/theme-styles/src/lib/styles/index' as *;
    • Or configure stylePreprocessorOptions in project.json

Testing Issues

  1. Import Errors in Tests

    • Verify test setup configuration
    • Check jest configuration
  2. Mock Issues

    • Use proper Angular testing utilities
    • Mock external dependencies

Runtime Issues

  1. "inject() must be called from an injection context"

    • For autoFlashState(): Use inside services/components, not in module-level constants
    • Convert constants to factory functions that run in injection context
  2. Storage API Errors

    • StorageService takes NO constructor arguments
    • Use storage.set(key, value) not storage.setItem()
    • Use storage.get(key) not storage.getItem()
    • Use storage.list(prefix?) with optional string prefix, not an object

🌟 INTEGRATION SUMMARY ⭐

Every new library you create now automatically benefits from our three core systems:

🧠 Flash-Persistence Integration

// Automatic state persistence and reactivity with autoFlashState
private readonly _config = autoFlashState<MyConfig>('my-lib/config', defaultConfig);
readonly config = computed(() => this._config());

// Update state - automatically persists!
this._config.update(current => ({ ...current, newValue }));

🗄️ Storage-System Integration

// Simple storage operations
private readonly storage = inject(StorageService);

await this.storage.set('my-lib:config', newConfig);
const config = await this.storage.get<Config>('my-lib:config');
const keys = await this.storage.list('my-lib:');

🎨 Theme-System Integration

// Import theme styles in component SCSS
@use 'libs/theme-system/theme-styles/src/lib/styles/index' as *;

.my-component {
  // Use CSS variables from theme system
  background: var(--ux-surface);
  color: var(--ux-on-surface);
  padding: var(--ux-spacing-md);
  border-radius: var(--ux-radius-md);
}
<!-- Use utility classes in templates -->
<div class="ux-bg-surface ux-text-on-surface ux-p-4 ux-rounded-lg ux-shadow">
  <h3 class="ux-text-lg ux-font-semibold ux-mb-2">Title</h3>
  <button class="ux-btn ux-btn-primary">Action</button>
</div>

🚀 Modern Angular 20+ Patterns

💡 Benefits You Get Immediately

  1. 🔥 Performance: Sub-10ms operations with intelligent caching
  2. 🧠 Intelligence: AI-powered storage decisions
  3. 🎨 Consistency: Automatic theme integration
  4. ⚡ Reactivity: Real-time updates across components
  5. 💾 Persistence: Zero-config state preservation
  6. 🌐 Sync: Cross-tab synchronization
  7. 📱 Responsive: Mobile-first design system
  8. ♿ Accessible: WCAG-compliant components

This comprehensive guide ensures that new libraries follow our established patterns and integrate seamlessly with our monorepo architecture.

🚨 Quick Troubleshooting Reference

If you encounter import/binding issues after creating a new library:

  1. "Can't bind to directive" errors → See Angular Import Issues
  2. Module resolution failures → Use Direct Library Paths
  3. Event handler type errors → Use $any($event) in templates

For detailed troubleshooting, see: Imports & Exports Guide

Integration in Applications

// In your application's global styles
@import '@theme-system/styles/themes';
@import '@theme-system/styles/utilities';

// Use theme mixins
.my-component {
  @include ux-card(true);
  
  .my-button {
    @include ux-button('primary');
  }
  
  @include ux-responsive(md) {
    padding: ux-spacing('lg');
  }
}

Step 8: Configure SASS Imports for Theme System (Optional)

If your library needs to use theme-system styles, you have two options:

Option 1: Use Full Paths (Recommended)

Use the complete path from workspace root in your SCSS files:

// libs/my-library-name/src/lib/components/my-component/my-component.component.scss
@use 'libs/theme-system/theme-styles/src/lib/styles/index' as *;

.my-library-component {
  // Use theme CSS variables
  background: var(--ux-surface);
  color: var(--ux-on-surface);
  padding: var(--ux-spacing-md);
  border-radius: var(--ux-radius-md);
  
  // Prefer utility classes in templates over mixins
  // Example: Use "ux-shadow" class instead of custom box-shadow
}

Option 2: Configure Include Paths

Add stylePreprocessorOptions to your library's build configuration in libs/my-library-name/project.json:

{
  "targets": {
    "build": {
      "executor": "nx:run-commands",
      "options": {
        "command": "node tools/scripts/build/build-angular-library.js my-library-name",
        "stylePreprocessorOptions": {
          "includePaths": ["libs/theme-system/theme-styles/src/lib/styles"]
        }
      }
    }
  }
}

Then use shorter imports:

// With stylePreprocessorOptions configured
@use 'index' as *;

.my-component {
  background: var(--ux-surface);
  color: var(--ux-on-surface);
}

Important Notes for Library Development

  1. Theme System Dependency: Add implicit dependency in project.json:

    {
      "implicitDependencies": ["theme-system-theme-styles"]
    }
    
  2. Prefer Utility Classes: Use theme utility classes in templates instead of custom SCSS:

    <!-- ✅ Preferred: Use utility classes -->
    <div class="ux-bg-surface ux-p-4 ux-rounded-lg ux-shadow">
    
    <!-- ❌ Avoid: Custom SCSS for theme values -->
    <div class="my-custom-card">
    
  3. CSS Variables: When custom SCSS is needed, use CSS variables:

    .custom-element {
      background: var(--ux-surface);  // ✅ Good
      // background: #ffffff;         // ❌ Bad - hardcoded
    }
    

Step 9: Testing

// ... existing code ...