@swimmesberger/elarion-contributions 0.2.3-preview.71.1 → 0.2.3-preview.72.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,6 +10,11 @@ adding a sidebar item or a cross-module action edits one file in the owning modu
10
10
  - **React bindings** (`@swimmesberger/elarion-contributions/react`) — `ContributionProvider`,
11
11
  `useContributions`, `<ExtensionSlot>`. React is an optional peer dependency; other view frameworks
12
12
  can port the bindings in a page of code.
13
+ - **Angular bindings** (`@swimmesberger/elarion-contributions/angular`) — `provideContributions`,
14
+ `injectContributions` (returns a `Signal`). Idiomatic for Angular 20–22: signal-first, standalone,
15
+ no NgModule, no shipped directive — you render with `@for`. `@angular/core` is an optional peer
16
+ dependency, and the bindings are decorator/template-free so they ship in this one package (no
17
+ ng-packagr).
13
18
  - **TanStack Router guard** (`@swimmesberger/elarion-contributions/tanstack-router`) —
14
19
  `redirectUnless(when, to)` + the vocabulary-bound `createRouteGuards<V>()`: a `beforeLoad` guard
15
20
  evaluating the same `when` clause as a contribution, so a route and its nav entry can never gate
@@ -30,7 +35,8 @@ This package ships the *machinery* with fixed semantics; your application owns t
30
35
  | `defineExtensionPoint`, `defineModule`, `contribute` | Your extension points (`sidebarItems`, …) and their payload types |
31
36
  | `evaluateWhen` + the `when` AND semantics | The kit instantiation binding your generated vocabulary |
32
37
  | `createContributionRegistry` (filter + deterministic order) | The app shell that renders each slot |
33
- | `ContributionProvider` / `useContributions` / `<ExtensionSlot>` | Route composition and module discovery (e.g. `import.meta.glob`) |
38
+ | `ContributionProvider` / `useContributions` / `<ExtensionSlot>` (React) | Route composition and module discovery (e.g. `import.meta.glob`) |
39
+ | `provideContributions` / `injectContributions` (Angular) | Slot rendering — an `@for` block, or a small `*extensionSlot` directive you own (see below) |
34
40
  | `redirectUnless` route guard (`/tanstack-router`) | Everything else routing — the router's own API |
35
41
 
36
42
  ## Quick start
@@ -93,6 +99,80 @@ const registry = createContributionRegistry(appModules.map((m) => m.manifest), c
93
99
  const items = useContributions(sidebarItems)
94
100
  ```
95
101
 
102
+ ## Angular
103
+
104
+ Same kernel, idiomatic Angular surface. `provideContributions` is an environment provider (shaped like
105
+ `provideRouter`); `injectContributions` returns a `Signal` your templates track, so refreshing the
106
+ capability snapshot re-resolves every slot by setting one signal:
107
+
108
+ ```ts
109
+ // main.ts — hand the resolved registry to the injector tree
110
+ import { createContributionRegistry } from "@swimmesberger/elarion-contributions"
111
+ import { provideContributions } from "@swimmesberger/elarion-contributions/angular"
112
+
113
+ const registry = createContributionRegistry(appModules.map((m) => m.manifest), capabilities)
114
+ bootstrapApplication(App, { providers: [provideContributions(registry)] })
115
+ // snapshot can change at runtime? provideContributions(signal(registry)) and .set(...) on login.
116
+ ```
117
+
118
+ ```ts
119
+ // any standalone component — read the point as a signal, render with @for
120
+ import { Component } from "@angular/core"
121
+ import { injectContributions } from "@swimmesberger/elarion-contributions/angular"
122
+ import { sidebarItems } from "../platform/points"
123
+
124
+ @Component({
125
+ standalone: true,
126
+ imports: [RouterLink],
127
+ template: `@for (item of items(); track item.id) {
128
+ <a [routerLink]="item.to">{{ item.label }}</a>
129
+ }`,
130
+ })
131
+ export class Sidebar {
132
+ readonly items = injectContributions(sidebarItems)
133
+ }
134
+ ```
135
+
136
+ ### Want a `*extensionSlot` directive? Build it on top
137
+
138
+ The package deliberately ships no directive (that would need the Angular compiler / ng-packagr and a second
139
+ build). If you prefer slot sugar over an inline `@for`, a structural directive is a few lines in **your**
140
+ app over `injectContributions` — you own the rendering, the kernel stays framework-agnostic:
141
+
142
+ ```ts
143
+ // app/extension-slot.directive.ts
144
+ import {
145
+ Directive, effect, inject, Injector, input, runInInjectionContext, TemplateRef, ViewContainerRef,
146
+ } from "@angular/core"
147
+ import { injectContributions } from "@swimmesberger/elarion-contributions/angular"
148
+ import type { Contribution, ExtensionPoint } from "@swimmesberger/elarion-contributions"
149
+
150
+ @Directive({ selector: "[extensionSlot]", standalone: true })
151
+ export class ExtensionSlotDirective<TItem> {
152
+ // *extensionSlot="point" — the point token to render.
153
+ readonly point = input.required<ExtensionPoint<TItem, unknown>>({ alias: "extensionSlot" })
154
+
155
+ private readonly tpl = inject<TemplateRef<{ $implicit: Contribution<TItem> }>>(TemplateRef)
156
+ private readonly vcr = inject(ViewContainerRef)
157
+ private readonly injector = inject(Injector)
158
+
159
+ constructor() {
160
+ // Re-render when the point changes or the snapshot refreshes; each item is the template's $implicit.
161
+ // injectContributions() calls inject(), so it must run in an injection context — hence runInInjectionContext.
162
+ effect(() => {
163
+ const items = runInInjectionContext(this.injector, () => injectContributions(this.point())())
164
+ this.vcr.clear()
165
+ for (const item of items) this.vcr.createEmbeddedView(this.tpl, { $implicit: item })
166
+ })
167
+ }
168
+ }
169
+ ```
170
+
171
+ ```html
172
+ <!-- usage: item is the contribution, typed by the point -->
173
+ <a *extensionSlot="sidebarItems; let item" [routerLink]="item.to">{{ item.label }}</a>
174
+ ```
175
+
96
176
  ## Semantics (the part that must not drift)
97
177
 
98
178
  - **`when` is AND** across its fields (`module`, `permission`, `flag`, `role`), mirroring
@@ -0,0 +1,32 @@
1
+ import { type EnvironmentProviders, type Signal } from "@angular/core";
2
+ import type { Contribution, ContributionRegistry, ExtensionPoint } from "./index.js";
3
+ /**
4
+ * Provides the resolved registry to the injector tree — the idiomatic mirror of React's
5
+ * `<ContributionProvider>`, shaped like `provideRouter`/`provideHttpClient`. Pass a live registry for a
6
+ * static snapshot, or a `Signal<ContributionRegistry>` when the capability snapshot can change at runtime
7
+ * (login, tenant switch): rebuilding the registry and setting the signal re-resolves every slot.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * bootstrapApplication(App, {
12
+ * providers: [provideContributions(createContributionRegistry(manifests, capabilities))],
13
+ * })
14
+ * ```
15
+ */
16
+ export declare function provideContributions(source: ContributionRegistry | Signal<ContributionRegistry>): EnvironmentProviders;
17
+ /**
18
+ * Reads a point's resolved contributions as a `Signal` — call from an injection context (a component field,
19
+ * a `factory`, or inside `runInInjectionContext`). The signal tracks the provided registry, so a snapshot
20
+ * refresh updates every template reading it. Already filtered by `when` and deterministically ordered.
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * @Component({
25
+ * template: `@for (item of items(); track item.id) { <a [routerLink]="item.to">{{ item.label }}</a> }`,
26
+ * })
27
+ * export class Sidebar {
28
+ * readonly items = injectContributions(sidebarItems)
29
+ * }
30
+ * ```
31
+ */
32
+ export declare function injectContributions<TItem, TContext>(point: ExtensionPoint<TItem, TContext>): Signal<ReadonlyArray<Contribution<TItem>>>;
@@ -0,0 +1,49 @@
1
+ // Angular bindings over the contribution kernel — the `/angular` adapter of ADR-0032, the sibling of the
2
+ // `/react` sub-export. Deliberately decorator- and template-free: it ships only the DI + reactivity seam
3
+ // (`provideContributions`, `injectContributions`) as plain `@angular/core` runtime APIs, so it compiles with
4
+ // the same `tsc` build as the rest of the package — no ng-packagr, no second toolchain, one npm package.
5
+ // Rendering is the app's own `@for` block (idiomatic Angular 20+ control flow); a structural directive is a
6
+ // handful of lines the app writes on top of `injectContributions` (see the README) when it wants slot sugar.
7
+ import { computed, inject, InjectionToken, isSignal, makeEnvironmentProviders, signal, } from "@angular/core";
8
+ // A Signal is held (never a bare registry) so a snapshot refresh — login, context change — re-resolves every
9
+ // slot reactively by setting one signal, the Angular mirror of React swapping the provider value.
10
+ const CONTRIBUTION_REGISTRY = new InjectionToken("elarion.contributions.registry");
11
+ /**
12
+ * Provides the resolved registry to the injector tree — the idiomatic mirror of React's
13
+ * `<ContributionProvider>`, shaped like `provideRouter`/`provideHttpClient`. Pass a live registry for a
14
+ * static snapshot, or a `Signal<ContributionRegistry>` when the capability snapshot can change at runtime
15
+ * (login, tenant switch): rebuilding the registry and setting the signal re-resolves every slot.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * bootstrapApplication(App, {
20
+ * providers: [provideContributions(createContributionRegistry(manifests, capabilities))],
21
+ * })
22
+ * ```
23
+ */
24
+ export function provideContributions(source) {
25
+ const registry = isSignal(source) ? source : signal(source);
26
+ return makeEnvironmentProviders([{ provide: CONTRIBUTION_REGISTRY, useValue: registry }]);
27
+ }
28
+ /**
29
+ * Reads a point's resolved contributions as a `Signal` — call from an injection context (a component field,
30
+ * a `factory`, or inside `runInInjectionContext`). The signal tracks the provided registry, so a snapshot
31
+ * refresh updates every template reading it. Already filtered by `when` and deterministically ordered.
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * @Component({
36
+ * template: `@for (item of items(); track item.id) { <a [routerLink]="item.to">{{ item.label }}</a> }`,
37
+ * })
38
+ * export class Sidebar {
39
+ * readonly items = injectContributions(sidebarItems)
40
+ * }
41
+ * ```
42
+ */
43
+ export function injectContributions(point) {
44
+ const registry = inject(CONTRIBUTION_REGISTRY, { optional: true });
45
+ if (registry === null) {
46
+ throw new Error("injectContributions requires provideContributions() in the injector tree.");
47
+ }
48
+ return computed(() => registry().get(point));
49
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@swimmesberger/elarion-contributions",
3
- "version": "0.2.3-preview.71.1",
4
- "description": "The Elarion frontend contribution model: typed extension points, declarative module manifests, and capability-gated resolution — framework-free core with React bindings under /react.",
3
+ "version": "0.2.3-preview.72.1",
4
+ "description": "The Elarion frontend contribution model: typed extension points, declarative module manifests, and capability-gated resolution — framework-free core with React bindings under /react and Angular bindings under /angular.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
7
7
  "exports": {
@@ -13,6 +13,10 @@
13
13
  "types": "./dist/react.d.ts",
14
14
  "import": "./dist/react.js"
15
15
  },
16
+ "./angular": {
17
+ "types": "./dist/angular.d.ts",
18
+ "import": "./dist/angular.js"
19
+ },
16
20
  "./tanstack-router": {
17
21
  "types": "./dist/tanstack-router.d.ts",
18
22
  "import": "./dist/tanstack-router.js"
@@ -46,6 +50,7 @@
46
50
  "node": ">=20.11"
47
51
  },
48
52
  "peerDependencies": {
53
+ "@angular/core": ">=20",
49
54
  "@tanstack/react-router": ">=1",
50
55
  "react": ">=18"
51
56
  },
@@ -53,13 +58,18 @@
53
58
  "react": {
54
59
  "optional": true
55
60
  },
61
+ "@angular/core": {
62
+ "optional": true
63
+ },
56
64
  "@tanstack/react-router": {
57
65
  "optional": true
58
66
  }
59
67
  },
60
68
  "devDependencies": {
69
+ "@angular/core": "^22.0.5",
61
70
  "@tanstack/react-router": "^1.170.17",
62
71
  "@types/react": "^19.2.17",
72
+ "rxjs": "^7.8.2",
63
73
  "typescript": "^5.9.3",
64
74
  "vitest": "^4.0.18"
65
75
  }