Appearance
Shared Libraries
The Nx workspace contains four shared libraries under libs/. They are consumed by every remote app as normal TypeScript imports — not through Module Federation's shared config. This page covers what each library contains, how to add code to them, and why the isolation-first bundling decision was made.
Library Summary
| Library | Path | Consumers | Bundle Size Impact |
|---|---|---|---|
shared-lib | libs/shared-lib/ | All remote apps | ~200 KB gzipped per remote |
microtec-auth-lib | libs/microtec-auth-lib/ | All remote apps | ~30 KB gzipped per remote |
apps-shared-lib | libs/apps-shared-lib/ | ERP remotes only | ~50 KB gzipped per remote |
shared-assets | libs/shared-assets/ | All apps (assets, not TypeScript) | Loaded from CDN, not bundled |
shared-lib
The core UI and utility library. Every Angular remote imports from shared-lib for its foundational building blocks.
Contents
| Category | Examples |
|---|---|
| UI Components | lib-table, lib-select, lib-dialog, lib-confirm, lib-loader, lib-page-header, lib-breadcrumbs, lib-microtec-viewer |
| Layout | Sidebar component, header component, page shell wrapper |
| Directives | appPermission (show/hide by role), appLoading, appConfirm |
| Pipes | translateEnum, microtecDate, microtecCurrency, safeHtml |
| Validators | Saudi National ID, Arabic text pattern, password strength, email |
| Services | LoaderService, StorageService, TranslationService, ToastService |
| Guards | AuthGuard, PermissionGuard (re-exported from microtec-auth-lib) |
| Base Classes | BaseListComponent, BaseFormComponent, BaseService |
| Models | Common DTOs, ApiResponse<T>, PaginatedResult<T>, LookupDto |
File structure
libs/shared-lib/src/
├── lib/
│ ├── components/ # Reusable UI components
│ ├── directives/ # Angular directives
│ ├── pipes/ # Angular pipes
│ ├── services/ # Shared services
│ ├── guards/ # Route guards (wraps microtec-auth-lib)
│ ├── models/ # Common DTOs and interfaces
│ ├── validators/ # FluentValidation-style Angular validators
│ └── modules/
│ ├── layout/ # Sidebar, header, layout shell
│ └── shared.module.ts # SharedModule — import this in every feature module
└── index.ts # Public API barrel fileHow to import
typescript
// In any remote app's feature module
import { SharedModule } from 'shared-lib';
@NgModule({
imports: [SharedModule],
})
export class AccountingModule {}typescript
// Direct named imports
import { BaseListComponent, ApiResponse, LookupDto } from 'shared-lib';How to add a new component
- Create the component under
libs/shared-lib/src/lib/components/<component-name>/. - Declare and export it in
SharedModule(libs/shared-lib/src/lib/modules/shared.module.ts). - Export the component class from
libs/shared-lib/src/index.ts. - Test in isolation before using in any remote app.
Keep shared-lib generic
shared-lib must not contain any business-domain logic (accounting, HR, etc.). Domain-specific shared components belong in apps-shared-lib. If a component references a backend endpoint or a domain model, it is in the wrong library.
microtec-auth-lib
Wraps keycloak-angular and provides all authentication primitives. Remote apps never import keycloak-angular directly — they always go through microtec-auth-lib.
Contents
| Export | Description |
|---|---|
AuthService | Login, logout, isAuthenticated(), getUserClaims(), hasRole() |
AuthGuard | CanActivate guard — redirects to Keycloak login if not authenticated |
PermissionGuard | CanActivate guard — checks a specific Keycloak role; returns 403 page if missing |
KeycloakInterceptor | HttpInterceptor — injects Authorization: Bearer <token> on every outbound request |
KeycloakInitFactory | APP_INITIALIZER factory — determines realm from URL path and initialises Keycloak |
AUTH_CONFIG injection token | Runtime configuration for Keycloak URL, realm, and client ID |
Realm detection logic
KeycloakInitFactory reads the current URL path at app startup:
typescript
// libs/microtec-auth-lib/src/lib/keycloak-init.factory.ts
export function keycloakInitFactory(keycloak: KeycloakService, config: AuthConfig) {
return async () => {
const realm = window.location.pathname.startsWith('/bussiness-owners')
? 'businessowner'
: config.realm; // 'microtec' for all ERP apps
await keycloak.init({
config: {
url: config.keycloakUrl,
realm,
clientId: config.clientId,
},
initOptions: {
onLoad: 'check-sso',
silentCheckSsoRedirectUri: `${window.location.origin}/assets/silent-check-sso.html`,
},
});
};
}How to register in a remote app
typescript
// apps/apps-accounting/src/app/app.module.ts
import { MicrotecAuthModule } from 'microtec-auth-lib';
import { environment } from '../environments/environment';
@NgModule({
imports: [
MicrotecAuthModule.forRoot({
keycloakUrl: environment.keycloakUrl,
realm: environment.keycloakRealm,
clientId: 'angular',
}),
],
})
export class AppModule {}How to use in services
typescript
import { AuthService } from 'microtec-auth-lib';
@Injectable({ providedIn: 'root' })
export class InvoiceService {
constructor(private auth: AuthService, private http: HttpClient) {}
canPostInvoice(): boolean {
return this.auth.hasRole('accounting.invoice.post');
}
}apps-shared-lib
ERP-domain-specific shared code that is too specific for shared-lib but used across more than one remote app.
Contents
| Category | Examples |
|---|---|
| Module constants | MODULE_PERMISSIONS — permission key constants per module |
| Lookup components | AccountLookupComponent, CostCenterLookupComponent, CustomerLookupComponent — tenant-aware typeahead fields |
| Report viewer | ReportViewerComponent — embeds the reporting service iframe |
| Import/Export UI | ImportButtonComponent, ExportButtonComponent, ImportDialogComponent |
| Shared ERP models | TenantContext, ModuleActivation, PermissionSet |
| Shared ERP services | LookupService, ModuleService, ReportService |
How to import
typescript
import { AppsSharedModule } from 'apps-shared-lib';
import { MODULE_PERMISSIONS } from 'apps-shared-lib';How to add a new lookup component
Lookup components are the most common addition. They wrap an API call for a specific domain entity and expose a PrimeNG Dropdown or AutoComplete:
- Create the component in
libs/apps-shared-lib/src/lib/lookups/<entity>-lookup/. - Declare and export it in
AppsSharedModule. - Export from
libs/apps-shared-lib/src/index.ts. - The component should accept an
(ngModelChange)or[formControlName]binding.
shared-assets
Static assets shared across all apps. This library contains no TypeScript — only files copied into each app's dist/ folder at build time via the publishAssets script.
Contents
libs/shared-assets/
├── src/
│ └── assets/
│ ├── langs/
│ │ ├── shared/
│ │ │ ├── ar.json # Arabic translations (shared keys)
│ │ │ └── en.json # English translations (shared keys)
│ │ └── <module>/ # Module-specific translations
│ ├── icons/
│ │ └── sprite.svg # Icon sprite
│ ├── images/
│ │ ├── logo.svg
│ │ └── logo-dark.svg
│ └── silent-check-sso.html # Keycloak silent SSO redirect pageHow shared-assets gets distributed
The publishAssets npm script runs after every build and copies the shared-assets contents into each app's output directory:
bash
npm run publishAssets
# Runs: node ./publish_assets.jsThe script copies libs/shared-assets/src/assets/ into dist/<app>/assets/ for every built app. This means each deployed SWA contains the shared translations and images alongside its own app-specific assets.
How to add a new shared translation key
- Add the key to
libs/shared-assets/src/assets/langs/shared/en.json. - Add the Arabic equivalent to
libs/shared-assets/src/assets/langs/shared/ar.json. - Use the key with the
shared_prefix convention:shared_save,shared_cancel,shared_confirm_delete. - Run
publishAssetsto propagate to dist folders.
Why Shared Libraries Are NOT in MF shared config
A common question is why these libraries are bundled into each remote rather than declared as shared singletons in the Module Federation configuration.
The answer is deployment independence:
The trade-off is bundle size: each remote includes its own copy of shared-lib (~200 KB gzipped). This is acceptable because:
- Each SWA is deployed to Azure CDN with long-lived cache headers
- Users only load each remote once per session
- Deployment independence eliminates an entire class of production incidents
Angular core (@angular/core, rxjs) remains in the MF shared config as a true singleton because two Angular instances on one page break change detection fundamentally.