Skip to content

Commit 023b616

Browse files
ankempCopilot
andcommitted
feat(headless-feature): demo headless-feature with a new 'feature-telemetry' library
Co-authored-by: Copilot <copilot@github.com>
1 parent 68ab5de commit 023b616

22 files changed

Lines changed: 1059 additions & 111 deletions

README.md

Lines changed: 49 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -30,20 +30,12 @@ This repo is paired with a LinkedIn article series. The five-part series will be
3030

3131
## How It Works
3232

33-
NACS transforms your monorepo from a collection of libraries into a governed, scalable platform. The process "pushes the decision left" through the following steps:
33+
NACS transforms your monorepo from a collection of libraries into a governed, scalable platform. The process "pushes decisions left" through the following steps:
3434

3535
1. A **client config JSON** (`configs/*.json`) declares which feature libraries to include and optionally pins each to a specific published version.
3636
2. A **pre-build script** reads the config, installs any versioned packages as npm aliases, then **discovers** each library's routing metadata and slot contributions from the library's own `package.json` (`nacs-contributions` field).
3737
3. The script generates `apps/shell/src/app/app.composition.generated.ts` — a single file containing both the top-level feature routes (`generatedRoutes`) and any extension point arrays (e.g. `extAdmin`).
38-
4. **Angular/esbuild** compiles the shell. Any feature not referenced in the generated routes file is fully tree-shaken from the output bundle.
39-
40-
## High-Level Workflow
41-
42-
1. A client config JSON is selected and parsed.
43-
2. The pre-build step resolves feature modules from local workspace source or a registry, then reads each library's published contribution metadata.
44-
3. Composition code is generated into `apps/shell/src/app/app.composition.generated.ts`.
45-
4. The production build compiles the shell and performs tree-shaking so only referenced features are included.
46-
5. The result is a custom-tailored, client-specific production bundle.
38+
4. **Angular/esbuild** compiles the shell. Any feature not referenced in the generated routes file is fully tree-shaken from the output bundle. The result is a custom-tailored, client-specific production bundle.
4739

4840
---
4941

@@ -118,10 +110,12 @@ Configs live in `configs/` and follow the schema defined in `configs/client-conf
118110
{
119111
"$schema": "./client-config.schema.json",
120112
"clientId": "dev",
121-
"features": [{ "module": "@nacs/feature-dashboard" }, { "module": "@nacs/feature-a" }, { "module": "@nacs/feature-b" }]
113+
"features": [{ "module": "@nacs/feature-dashboard" }, { "module": "@nacs/feature-a" }, { "module": "@nacs/feature-b" }, { "module": "@nacs/feature-telemetry" }]
122114
}
123115
```
124116

117+
`@nacs/feature-telemetry` is a **headless feature** — it has no primary route, so it never appears in the sidebar. It contributes silently to the dashboard widget slot and lifecycle hook handlers. See [Headless features](#headless-features) below.
118+
125119
**Example — production with pinned versions, a nav override, and a default route:**
126120

127121
```json
@@ -191,15 +185,41 @@ Each feature library is **self-describing**. Instead of declaring routing metada
191185
}
192186
```
193187

194-
| Field | Description |
195-
| -------------------- | ------------------------------------------------------------------------ |
196-
| `primary` | The top-level route this feature registers in the shell navigation |
197-
| `primary.path` | Angular router path segment (lowercase, hyphens only) |
198-
| `primary.exportName` | Named export from the library's public API containing the `Routes` array |
199-
| `primary.title` | Label shown in the navigation sidebar |
200-
| `primary.icon` | Emoji or icon identifier for the nav item |
201-
| `extensions` | Optional map of extension points this feature contributes to |
202-
| `extensions.admin` | Contributes a child route to the built-in Administration panel |
188+
| Field | Required | Description |
189+
| -------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------- |
190+
| `primary` | No | The top-level route this feature registers in the shell navigation. Omit to create a [headless feature](#headless-features). |
191+
| `primary.path` || Angular router path segment (lowercase, hyphens only) |
192+
| `primary.exportName` || Named export from the library's public API containing the `Routes` array |
193+
| `primary.title` || Label shown in the navigation sidebar |
194+
| `primary.icon` || Emoji or icon identifier for the nav item |
195+
| `extensions` | No | Map of extension points this feature contributes to |
196+
| `extensions.admin` || Contributes a child route to the built-in Administration panel |
197+
198+
> Sub-fields marked `` are required when their parent field is present.
199+
200+
### Headless features
201+
202+
A **headless feature** (also called a _ghost feature_) is a feature library that omits the `primary` field from its `nacs-contributions`. It has no route, no navigation entry, and no URL of its own. It exists purely to contribute to extension points declared by other libraries.
203+
204+
```json
205+
{
206+
"nacs-contributions": {
207+
"extensions": {
208+
"dashboard-widget": [{ "exportName": "TelemetryWidget", "title": "Platform Telemetry", "icon": "📡" }],
209+
"lifecycle:user.logout": [{ "exportName": "telemetryLogoutHandler" }],
210+
"lifecycle:session.expired": [{ "exportName": "telemetrySessionExpiredHandler" }]
211+
}
212+
}
213+
}
214+
```
215+
216+
The key properties of headless features:
217+
218+
- **No nav entry** — the sidebar is unchanged whether the feature is present or absent.
219+
- **Silent removal** — dropping the feature from a client config removes all its contributions (widgets, handlers, etc.) with zero code changes.
220+
- **Full governance** — peer dependency checks still apply; headless features are not exempt.
221+
222+
> **Note:** A client config where _all_ features are headless is a build error. At least one feature must declare a primary route to generate valid navigation.
203223
204224
### Extension point types
205225

@@ -217,21 +237,22 @@ Extension points are declared by **consumer** libs (e.g. `core-admin`, `feature-
217237

218238
**Key distinction between `component` and `multi-provider`:** `component` contributions are plain objects in an array — the shell renders them but they cannot inject other services themselves. `multi-provider` contributions are DI-resolved class instances, enabling each contributor to declare its own `deps` and participate fully in Angular's dependency injection graph.
219239

220-
**Key distinction between `lifecycle-hook` and `multi-provider`:** `lifecycle-hook` handlers are stateless functions — they cannot inject services themselves. They are called imperatively by a dispatcher at a named moment in application time (e.g. logout, session expiry). `multi-provider` contributions are DI-resolved class instances that participate fully in Angular's dependency injection graph. Use `lifecycle-hook` for fire-and-forget cleanup; use `multi-provider` when the handler needs its own dependencies.
240+
**Key distinction between `lifecycle-hook` and `multi-provider`:** `lifecycle-hook` handlers are stateless functions — they cannot inject services themselves. They are called imperatively by a dispatcher at a named moment in application time (e.g. logout, session expiry). Use `lifecycle-hook` for fire-and-forget cleanup; use `multi-provider` (see above) when the handler needs its own dependencies.
221241

222-
### How extension point discovery works
242+
### How Extension Point Discovery Works
223243

224244
When the pre-build script runs, it:
225245

226246
1. Resolves each feature's `package.json` (from `node_modules` for versioned installs, from the workspace source for local)
227-
2. Reads `nacs-contributions.primary` to generate the top-level `generatedRoutes` array
228-
3. Reads `nacs-contributions.extensions.*` to generate extension point arrays (e.g. `extAdmin`) and `multi: true` providers (e.g. lifecycle-hook handlers)
247+
2. Enforces peer dependency governance on every feature, including headless ones
248+
3. Reads `nacs-contributions.primary` (if present) to generate the top-level `generatedRoutes` array — features without `primary` are skipped for route generation
249+
4. Reads `nacs-contributions.extensions.*` from **all** features (including headless ones) to generate extension point arrays (e.g. `extDashboardWidget`) and `multi: true` providers (e.g. lifecycle-hook handlers)
229250

230251
All generated code is written into a single file: `apps/shell/src/app/app.composition.generated.ts`.
231252

232253
The shell's `app.routes.ts` imports route arrays and passes them to factory functions at composition time. The shell's `app.config.ts` spreads `generatedProviders` — which includes both component/lazy-component token bindings and lifecycle-hook `multi: true` handler registrations — into the application providers.
233254

234-
### Adding an admin extension to a feature
255+
### Adding an Admin Extension to a Feature
235256

236257
**Step 1 — Create an admin component and route export in the feature library:**
237258

@@ -283,7 +304,7 @@ npx nx run shell:prepare-build
283304

284305
The admin tab will appear automatically in the Administration panel for any client config that includes this feature. Features that declare no `extensions.admin` entry contribute nothing to the admin panel — omission is the opt-out.
285306

286-
### Adding a lifecycle hook to a feature
307+
### Adding a Lifecycle Hook to a Feature
287308

288309
Lifecycle hooks let features respond to application-level events (e.g. `user.logout`, `session.expired`) without importing anything from `@nacs/shell-lifecycle`. The feature exports a plain stateless function; `prepare-build` wires it into the dispatcher via `multi: true` DI providers at build time. If the feature is absent from a client config, its handler is fully tree-shaken.
289310

@@ -295,7 +316,7 @@ See [`libs/shell-lifecycle/README.md`](libs/shell-lifecycle/README.md) for the f
295316

296317
To ensure each client build is a hermetically sealed artifact, the pre-build engine enforces strict governance. When a feature is loaded from the registry by version, the script enforces that the feature's `peerDependencies` are satisfied by the shell's installed dependencies.
297318

298-
This prevents "Module Federation version mismatch" errors by catching framework or library version mismatches (e.g. an Angular 18 feature in an Angular 21 shell) before a bad deploy ever reaches your CI pipeline.
319+
This prevents "Module Federation version mismatch" errors by catching framework or library version mismatches (e.g. an Angular 18 feature in an Angular 21 shell) before a bad build ever reaches your deployment pipeline.
299320

300321
The enforced peers are: `@angular/core`, `@angular/common`, `@angular/router`, `rxjs`.
301322

@@ -318,7 +339,7 @@ This project treats each feature library as an independently versioned contract.
318339

319340
### Versioning & Contracts
320341

321-
Feature libraries utilize `nx release` for independent semver versioning. This process updates the library's `package.json` and creates matching git tags, ensuring that every published version is a stable, referencable artifact.
342+
Feature libraries utilize `nx release` for independent semver versioning. This process updates the library's `package.json` and creates matching git tags, ensuring that every published version is a stable, referenceable artifact.
322343

323344
### Distribution
324345

configs/client-dev.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"features": [
55
{ "module": "@nacs/feature-dashboard" },
66
{ "module": "@nacs/feature-a" },
7-
{ "module": "@nacs/feature-b" }
7+
{ "module": "@nacs/feature-b" },
8+
{ "module": "@nacs/feature-telemetry" }
89
]
910
}

configs/client-no-telemetry.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"$schema": "./client-config.schema.json",
3+
"clientId": "client-no-telemetry",
4+
"features": [
5+
{ "module": "@nacs/feature-dashboard" },
6+
{ "module": "@nacs/feature-a" },
7+
{ "module": "@nacs/feature-b" }
8+
]
9+
}

0 commit comments

Comments
 (0)