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/getmethods)- Added guidance on lazy-loaded libraries and dynamic imports
- Updated theme-system import paths and library names
- Clarified nested library naming conventions
- Added
incremental: falserecommendation for TypeScript config- Updated Flash Persistence examples to use
autoFlashState()
Prerequisites
Before creating a new library, ensure you have:
- Understanding of our library dependency hierarchy
- Knowledge of our typing system (
@libs-foundational/core-models) - Familiarity with our build and publishing process
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:
- Style:
scss(SCSS for styling) - Linter:
eslint(ESLint for code quality) - Unit Test Runner:
jest(Jest for testing) - Publishable:
yes(if library will be published) - Import Path:
@my-library-name(consistent naming)
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 yourtsconfig.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 explicitpathsmapping 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:
- ⚠️
incremental: falseis recommended to avoid TypeScript build cache issues with our custom builder - For nested libraries (e.g.,
libs/theme-system/theme-runtime/), adjustoutDiraccordingly:"outDir": "../../../dist/libs/theme-system/theme-runtime"
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/):
- The
namefield should match the full path:"name": "theme-system-theme-runtime" - This ensures proper dependency resolution in the build system
Why We Use a Custom Builder
Our custom builder (build-angular-library.js) provides several advantages over the standard @nx/angular:ng-packagr-lite:
- Enhanced TypeScript Support: Ensures
experimentalDecoratorsandemitDecoratorMetadataare properly set for NGXS and Angular decorators - Better Error Messages: Clearer error reporting during compilation
- Consistent Builds: Standardized build process across all libraries
- Path Resolution: Better handling of complex path mappings in our monorepo
The custom builder handles:
- TypeScript compilation with proper decorator support
- SCSS compilation with theme-system integration
- HTML template copying
- Asset management
- Type definition bundling
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:
- ✅ DO: Use simple names like
@ux-top-nav,@ux-menu,@theme-runtime - ❌ DON'T: Use prefixed names like
@widgets-editor/ux-top-nav
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:
- ✅
StorageServiceconstructor takes NO arguments (auto-configured) - ✅ Use
storage.set(key, value)andstorage.get(key)for operations - ✅ Use
storage.list(prefix?)to list keys with optional prefix - ✅ Access metadata via
storage.adapterKindandstorage.config
🎨 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:
@theme-system-theme-runtime: Production theme application (ThemeManagerService, ThemeApplicationService)@theme-system-theme-dev-tools: Development tools (ThemeDebuggerComponent, ThemeEditorComponent)@theme-system-theme-styles: SCSS variables and mixins@theme-system: Unified export of all theme functionality (recommended for most use cases)
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?
- ✅ ZERO configuration - no providers, no boilerplate
- ✅ Automatic persistence - saves to storage automatically
- ✅ Type-safe - full TypeScript support
- ✅ Signals-based - modern Angular reactivity
- ✅ Simpler - just use it like a signal!
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
- Don't import from libraries at the same level
- Use core for shared interfaces
- 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:
- ✅ Use utility classes in template (
ux-bg-surface,ux-text-primary, etc.) - ✅ CSS Variables for values (
var(--ux-spacing-md),var(--ux-primary)) - ✅ Minimal custom SCSS - only what's not available in utilities
- ✅ Full path imports - use complete path from workspace root
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
core (Foundation)
- No dependencies
- Contains all shared type definitions
- Used by all other libraries
Core Libraries
- Can depend on core
- Should not depend on other core libraries
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:
@storage-system- Use dynamic imports in examples/demos- Large feature libraries - Check
project.jsonfor lazy-load tags
When to Use Static vs Dynamic Imports:
- ✅ Static imports: Core services, components used in templates, providers
- ✅ Dynamic imports: Example code, optional features, large libraries in demos
Common Patterns and Best Practices
1. Consistent Naming
- Use kebab-case for library names
- Use PascalCase for components and services
- Use camelCase for properties and methods
2. Type Safety
- Always use
SafeAnyinstead ofany - Define clear interfaces for all public APIs
- Use type imports for type-only imports
3. Component Design
- Use standalone components (Angular 20+)
- Implement OnPush change detection
- Provide clear input/output interfaces
- Support content projection when appropriate
4. Service Design
- Use reactive patterns with RxJS
- Provide clear initialization methods
- Handle errors gracefully
- Use dependency injection properly
5. Testing
- Write unit tests for all public APIs
- Use TestBed for component testing
- Mock external dependencies
- Achieve good test coverage
Examples in Our Monorepo
core Library
Prompt-System Library (new)
Location:
prompt-system/Purpose: Centralize prompt-related UI and services (global modal host + open directives)
Dependencies:
@core,@gograb-aiExports:
PromptSystemComponent,PromptModalService,OpenPromptManagerDirective,OpenPromptLabDirectiveIntegration:
- Add
<lib-prompt-system></lib-prompt-system>once to an app root template - Trigger with
[openPromptManager]or[openPromptLab]on any element
- Add
Location:
libs/Purpose: Shared type definitions
Dependencies: None (foundation library)
Exports: Types, interfaces, and utilities
Troubleshooting
Build Issues
❌ 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"] }
- Cause: Missing or incorrect path mappings in
Circular Dependency Errors
- Move shared types to
@core - Avoid peer library imports
- Move shared types to
Type Resolution Issues
- Verify path mappings in
tsconfig.base.json - Ensure proper export declarations
- Verify path mappings in
Build Order Issues
- Build dependencies first
- Use
--with-depsflag when needed
"Invalid TypeScript program emit for .tsbuildinfo"
- Set
"incremental": falseintsconfig.lib.json - Delete the
.tsbuildinfofile:rm -rf dist/libs/my-library/.tsbuildinfo - Rebuild:
yarn nx build my-library --skip-nx-cache
- Set
"Static imports of lazy-loaded libraries are forbidden"
- Use dynamic imports:
const { MyService } = await import('@my-library'); - See Lazy-Loaded Libraries section
- Use dynamic imports:
"Cannot find module '@my-library'"
- Check
tsconfig.base.jsonhas correct path mapping - For nested libraries, ensure
project.jsonnamematches directory structure - Example:
libs/theme-system/theme-runtime/→"name": "theme-system-theme-runtime"
- Check
Decorator Errors (TS1241, TS2449)
- Ensure using custom builder (not
@nx/angular:ng-packagr-lite) - Custom builder automatically sets
experimentalDecorators: true
- Ensure using custom builder (not
SCSS Import Errors
- Use full path:
@use 'libs/theme-system/theme-styles/src/lib/styles/index' as *; - Or configure
stylePreprocessorOptionsinproject.json
- Use full path:
Testing Issues
Import Errors in Tests
- Verify test setup configuration
- Check jest configuration
Mock Issues
- Use proper Angular testing utilities
- Mock external dependencies
Runtime Issues
"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
- For
Storage API Errors
StorageServicetakes NO constructor arguments- Use
storage.set(key, value)notstorage.setItem() - Use
storage.get(key)notstorage.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
- Zero-config state management using
autoFlashState() - Instant persistence with automatic hydration
- Type-safe signals for reactive state
- No providers needed - just use it!
// 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
- Intelligent caching with sub-10ms operations
- OPFS, IndexedDB, or localStorage - automatically selected
- Simple API with
set(),get(),list(),clear() - No configuration needed - inject and use
// 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
- Automatic theme inheritance with CSS variables
- Utility classes for rapid development (
ux-bg-surface,ux-shadow, etc.) - Responsive design with CSS variables
- Accessibility-first with WCAG-compliant colors
// 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
- ✅ Standalone components everywhere
- ✅ Signals for reactivity (
signal,computed,effect) - ✅ Modern control flow (
@if,@for,@switch) - ✅ Font Awesome icons as standard
- ✅ Accessibility attributes built-in
- ✅ No timeouts - everything is reactive
- ✅ SOLID principles throughout
💡 Benefits You Get Immediately
- 🔥 Performance: Sub-10ms operations with intelligent caching
- 🧠 Intelligence: AI-powered storage decisions
- 🎨 Consistency: Automatic theme integration
- ⚡ Reactivity: Real-time updates across components
- 💾 Persistence: Zero-config state preservation
- 🌐 Sync: Cross-tab synchronization
- 📱 Responsive: Mobile-first design system
- ♿ 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:
- "Can't bind to directive" errors → See Angular Import Issues
- Module resolution failures → Use Direct Library Paths
- 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
Theme System Dependency: Add implicit dependency in
project.json:{ "implicitDependencies": ["theme-system-theme-styles"] }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">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 ...