@real-router/angular 0.8.0 → 0.9.0

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.
Files changed (43) hide show
  1. package/README.md +183 -4
  2. package/dist/README.md +183 -4
  3. package/dist/fesm2022/real-router-angular-ssr.mjs +323 -0
  4. package/dist/fesm2022/real-router-angular-ssr.mjs.map +1 -0
  5. package/dist/fesm2022/real-router-angular.mjs +759 -173
  6. package/dist/fesm2022/real-router-angular.mjs.map +1 -1
  7. package/dist/types/real-router-angular-ssr.d.ts +227 -0
  8. package/dist/types/real-router-angular-ssr.d.ts.map +1 -0
  9. package/dist/types/real-router-angular.d.ts +119 -20
  10. package/dist/types/real-router-angular.d.ts.map +1 -1
  11. package/package.json +18 -11
  12. package/src/components/RouteView.ts +81 -56
  13. package/src/components/RouterErrorBoundary.ts +7 -5
  14. package/src/directives/RealLink.ts +57 -37
  15. package/src/directives/RealLinkActive.ts +34 -25
  16. package/src/dom-utils/link-utils.ts +119 -7
  17. package/src/dom-utils/route-announcer.ts +58 -2
  18. package/src/dom-utils/scroll-restore.ts +160 -12
  19. package/src/functions/injectIsActiveRoute.ts +9 -8
  20. package/src/functions/injectNavigator.ts +4 -0
  21. package/src/functions/injectOrThrow.ts +5 -1
  22. package/src/functions/injectRoute.ts +17 -8
  23. package/src/functions/injectRouteEnter.ts +5 -10
  24. package/src/functions/injectRouteNode.ts +3 -0
  25. package/src/functions/injectRouteUtils.ts +3 -0
  26. package/src/functions/injectRouter.ts +4 -0
  27. package/src/functions/injectRouterTransition.ts +3 -0
  28. package/src/index.ts +14 -3
  29. package/src/internal/buildActiveRouteOptions.ts +20 -0
  30. package/src/internal/install.ts +77 -0
  31. package/src/internal/subscribeSourceToSignal.ts +48 -0
  32. package/src/providers.ts +11 -38
  33. package/src/providersFactory.ts +298 -0
  34. package/src/sourceToSignal.ts +10 -2
  35. package/src/types.ts +6 -1
  36. package/ssr/components/ClientOnly.ts +27 -0
  37. package/ssr/components/HttpStatusCode.ts +106 -0
  38. package/ssr/components/ServerOnly.ts +27 -0
  39. package/ssr/functions/injectDeferred.ts +92 -0
  40. package/ssr/functions/provideHttpStatusSink.ts +43 -0
  41. package/ssr/ng-package.json +6 -0
  42. package/ssr/public_api.ts +35 -0
  43. package/ssr/utils/createHttpStatusSink.ts +61 -0
@@ -0,0 +1,323 @@
1
+ import { NgTemplateOutlet } from '@angular/common';
2
+ import * as i0 from '@angular/core';
3
+ import { input, signal, afterNextRender, Component, InjectionToken, inject, computed, effect, makeEnvironmentProviders } from '@angular/core';
4
+ import { injectRoute } from '@real-router/angular';
5
+
6
+ class ClientOnly {
7
+ fallback = input(...(ngDevMode ? [undefined, { debugName: "fallback" }] : /* istanbul ignore next */ []));
8
+ mounted = signal(false, ...(ngDevMode ? [{ debugName: "mounted" }] : /* istanbul ignore next */ []));
9
+ constructor() {
10
+ afterNextRender(() => {
11
+ this.mounted.set(true);
12
+ });
13
+ }
14
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ClientOnly, deps: [], target: i0.ɵɵFactoryTarget.Component });
15
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: ClientOnly, isStandalone: true, selector: "client-only", inputs: { fallback: { classPropertyName: "fallback", publicName: "fallback", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
16
+ @if (mounted()) {
17
+ <ng-content />
18
+ } @else if (fallback()) {
19
+ <ng-container [ngTemplateOutlet]="fallback() ?? null" />
20
+ }
21
+ `, isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] });
22
+ }
23
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ClientOnly, decorators: [{
24
+ type: Component,
25
+ args: [{
26
+ selector: "client-only",
27
+ template: `
28
+ @if (mounted()) {
29
+ <ng-content />
30
+ } @else if (fallback()) {
31
+ <ng-container [ngTemplateOutlet]="fallback() ?? null" />
32
+ }
33
+ `,
34
+ imports: [NgTemplateOutlet],
35
+ }]
36
+ }], ctorParameters: () => [], propDecorators: { fallback: [{ type: i0.Input, args: [{ isSignal: true, alias: "fallback", required: false }] }] } });
37
+
38
+ class ServerOnly {
39
+ fallback = input(...(ngDevMode ? [undefined, { debugName: "fallback" }] : /* istanbul ignore next */ []));
40
+ mounted = signal(false, ...(ngDevMode ? [{ debugName: "mounted" }] : /* istanbul ignore next */ []));
41
+ constructor() {
42
+ afterNextRender(() => {
43
+ this.mounted.set(true);
44
+ });
45
+ }
46
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ServerOnly, deps: [], target: i0.ɵɵFactoryTarget.Component });
47
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: ServerOnly, isStandalone: true, selector: "server-only", inputs: { fallback: { classPropertyName: "fallback", publicName: "fallback", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
48
+ @if (!mounted()) {
49
+ <ng-content />
50
+ } @else if (fallback()) {
51
+ <ng-container [ngTemplateOutlet]="fallback() ?? null" />
52
+ }
53
+ `, isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] });
54
+ }
55
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ServerOnly, decorators: [{
56
+ type: Component,
57
+ args: [{
58
+ selector: "server-only",
59
+ template: `
60
+ @if (!mounted()) {
61
+ <ng-content />
62
+ } @else if (fallback()) {
63
+ <ng-container [ngTemplateOutlet]="fallback() ?? null" />
64
+ }
65
+ `,
66
+ imports: [NgTemplateOutlet],
67
+ }]
68
+ }], ctorParameters: () => [], propDecorators: { fallback: [{ type: i0.Input, args: [{ isSignal: true, alias: "fallback", required: false }] }] } });
69
+
70
+ function createHttpStatusSink() {
71
+ return { code: undefined };
72
+ }
73
+ /**
74
+ * DI token for the request-scoped HTTP status sink. Application-side wiring:
75
+ *
76
+ * ```ts
77
+ * import { bootstrapApplication } from "@angular/platform-browser";
78
+ * import { provideHttpStatusSink, createHttpStatusSink } from "@real-router/angular/ssr";
79
+ *
80
+ * const sink = createHttpStatusSink();
81
+ *
82
+ * await bootstrapApplication(AppRoot, {
83
+ * providers: [
84
+ * provideRealRouterFactory({ ... }),
85
+ * provideHttpStatusSink(sink),
86
+ * ],
87
+ * });
88
+ *
89
+ * response.status(sink.code ?? 200).send(html);
90
+ * ```
91
+ */
92
+ const HTTP_STATUS_SINK = new InjectionToken("HTTP_STATUS_SINK");
93
+
94
+ /**
95
+ * Render-time HTTP status declaration. Mount inside a route component
96
+ * (typical use case: a glob `*` route's NotFound page) when the status is
97
+ * decided by the rendered tree rather than a loader.
98
+ *
99
+ * Writes `code` to the optionally injected `HTTP_STATUS_SINK` in `ngOnInit`
100
+ * (after the input binding has fired) and renders nothing. Without a provider
101
+ * registered (the standard client-side case) the component is a silent no-op
102
+ * — same component tree hydrates without touching the DOM or warning about
103
+ * mismatches.
104
+ *
105
+ * Loader-driven errors (`LoaderNotFound` → 404, `LoaderRedirect` → 30x) keep
106
+ * working as before; this component covers render-time decisions only.
107
+ *
108
+ * Last write wins when several `<http-status-code />` instances mount in the
109
+ * same render pass — sink reflects the last component whose `ngOnInit` ran.
110
+ *
111
+ * ```ts
112
+ * // entry-server.ts
113
+ * import { bootstrapApplication } from "@angular/platform-browser";
114
+ * import {
115
+ * createHttpStatusSink,
116
+ * provideHttpStatusSink,
117
+ * } from "@real-router/angular/ssr";
118
+ *
119
+ * const sink = createHttpStatusSink();
120
+ * await bootstrapApplication(AppRoot, {
121
+ * providers: [
122
+ * provideRealRouterFactory({ ... }),
123
+ * provideHttpStatusSink(sink),
124
+ * ],
125
+ * });
126
+ * response.status(sink.code ?? 200).send(html);
127
+ * ```
128
+ *
129
+ * ```html
130
+ * <!-- inside not-found.component.ts template -->
131
+ * <http-status-code [code]="404" />
132
+ * ```
133
+ *
134
+ * **Per-request wiring with `AngularNodeAppEngine`:** the sink must be
135
+ * passed via the second arg of `handle(req, requestContext)` — Angular
136
+ * surfaces it through the `REQUEST_CONTEXT` token. Attaching to `req`
137
+ * directly does NOT work: `AngularNodeAppEngine.handle` constructs a fresh
138
+ * Web `Request` from the Express `IncomingMessage` and discards every
139
+ * custom property. See the `ssr/` example's `app.config.ts` factory for
140
+ * the canonical pattern (`inject(REQUEST_CONTEXT, { optional: true })`
141
+ * → `(ctx as { httpStatusSink? } | null)?.httpStatusSink`).
142
+ *
143
+ * **`@angular/ssr` streaming + `@defer` blocks:** `@defer` blocks hydrate
144
+ * lazily on the client; their server-side rendering is fully synchronous,
145
+ * so `<http-status-code />` inside or outside a `@defer` writes to the
146
+ * sink before `AngularNodeAppEngine.handle` resolves. No streaming
147
+ * ordering concern in Angular's current SSR model.
148
+ *
149
+ * **JIT vs AOT:** the `code` input is declared as `input<number>()` (not
150
+ * `input.required<number>()`) because `input.required` trips `NG0950` in
151
+ * JIT/TestBed even after `componentRef.setInput(...)`. `ngOnInit` skips
152
+ * the write when `code()` is `undefined`. AOT (production build) binds
153
+ * the value normally and the skip never fires.
154
+ *
155
+ * **Valid `code` range:** Node's `res.end()` throws `Invalid status code`
156
+ * on `NaN`, `0`, negative values, or values `> 999` — this surfaces as a
157
+ * 5xx / dropped connection, not silent corruption. Pass a real HTTP status
158
+ * integer (commonly 4xx/5xx; 100-999 is what Node accepts).
159
+ */
160
+ class HttpStatusCode {
161
+ /**
162
+ * HTTP status to apply to the response. Common values: 404, 410, 451, 503.
163
+ *
164
+ * Declared as optional so the signal is safe to read in `ngOnInit` under
165
+ * both AOT (template binding fires before init hooks) and JIT/TestBed
166
+ * (`componentRef.setInput("code", N)` writes the value before the first
167
+ * change detection). `input.required` would trip `NG0950` in the JIT path
168
+ * because the required-flag is asserted independently of the runtime
169
+ * value. Consumers should always pass a value — `undefined` makes
170
+ * `ngOnInit` skip the sink write rather than throw.
171
+ */
172
+ code = input(...(ngDevMode ? [undefined, { debugName: "code" }] : /* istanbul ignore next */ []));
173
+ // Optional injection — when no `provideHttpStatusSink(...)` is registered
174
+ // (client side) the field is null and `ngOnInit` skips the write.
175
+ sink = inject(HTTP_STATUS_SINK, { optional: true });
176
+ ngOnInit() {
177
+ if (!this.sink) {
178
+ return;
179
+ }
180
+ const value = this.code();
181
+ if (value !== undefined) {
182
+ this.sink.code = value;
183
+ }
184
+ }
185
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: HttpStatusCode, deps: [], target: i0.ɵɵFactoryTarget.Component });
186
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.9", type: HttpStatusCode, isStandalone: true, selector: "http-status-code", inputs: { code: { classPropertyName: "code", publicName: "code", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "", isInline: true });
187
+ }
188
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: HttpStatusCode, decorators: [{
189
+ type: Component,
190
+ args: [{
191
+ selector: "http-status-code",
192
+ template: "",
193
+ }]
194
+ }], propDecorators: { code: [{ type: i0.Input, args: [{ isSignal: true, alias: "code", required: false }] }] } });
195
+
196
+ const NEVER_PROMISE = new Promise(() => {
197
+ // Intentionally never resolves — settles as `undefined` indefinitely when
198
+ // a key is requested that the loader never declared. Surfaces consumer/
199
+ // loader key drift as a visible "loading" state in the UI.
200
+ });
201
+ /**
202
+ * Read a deferred promise published by `defer({ deferred: { <key>: Promise } })`
203
+ * inside an SSR data loader. Returns an Angular `Signal<T | undefined>` that
204
+ * tracks the active route — re-keying picks up the new state's deferred map.
205
+ *
206
+ * The signal starts `undefined` and updates to the resolved value once the
207
+ * promise settles. Use with native Angular control flow:
208
+ *
209
+ * ```ts
210
+ * @Component({
211
+ * template: `
212
+ * @if (reviews()) {
213
+ * <ul>
214
+ * @for (r of reviews(); track r.id) {
215
+ * <li>{{ r.author }}</li>
216
+ * }
217
+ * </ul>
218
+ * } @else {
219
+ * <p>Loading reviews…</p>
220
+ * }
221
+ * `,
222
+ * })
223
+ * export class Reviews {
224
+ * readonly reviews = injectDeferred<Review[]>("reviews");
225
+ * }
226
+ * ```
227
+ *
228
+ * **Asymmetric Angular** (see `.claude/SSR_FEATURE_GAPS_RU.md` §8): Angular
229
+ * does not ship `<Await>` / `<Streamed>` adapter components — Angular has no
230
+ * direct analogue to React's `use(promise)` or Svelte's `{#await}`. Use
231
+ * `@if (signal()) { … } @else { … }` or the `async` pipe with
232
+ * `from(deferredPromise)` instead.
233
+ */
234
+ function injectDeferred(key) {
235
+ const { routeState } = injectRoute();
236
+ // Re-derive the promise reference whenever the route changes — invalidate()
237
+ // + reload, navigation to a new route, etc. all refresh the underlying
238
+ // deferred map, and we want the signal to track the *latest* promise.
239
+ const promiseSignal = computed(() => {
240
+ const context = routeState().route.context;
241
+ const deferred = context.ssrDataDeferred;
242
+ return (deferred?.[key] ?? NEVER_PROMISE);
243
+ }, ...(ngDevMode ? [{ debugName: "promiseSignal" }] : /* istanbul ignore next */ []));
244
+ const value = signal(undefined, ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
245
+ effect((onCleanup) => {
246
+ const promise = promiseSignal();
247
+ let cancelled = false;
248
+ onCleanup(() => {
249
+ cancelled = true;
250
+ });
251
+ promise.then((resolved) => {
252
+ if (!cancelled) {
253
+ value.set(resolved);
254
+ }
255
+ },
256
+ /* v8 ignore next 4 -- @preserve: rejection branch — `effect` swallows
257
+ async errors silently, so leaving the signal as `undefined` is the
258
+ only observable behaviour. Real error surfacing is the loader's
259
+ responsibility (throw → navigation rejects → app error boundary). */
260
+ () => {
261
+ // Intentional swallow — see v8 ignore note above.
262
+ });
263
+ });
264
+ return value.asReadonly();
265
+ }
266
+
267
+ /**
268
+ * Environment providers for a request-scoped `HttpStatusSink`. Pair with
269
+ * `createHttpStatusSink()` and read `sink.code` after the SSR render pass
270
+ * completes.
271
+ *
272
+ * Application bootstrap:
273
+ *
274
+ * ```ts
275
+ * const sink = createHttpStatusSink();
276
+ *
277
+ * await bootstrapApplication(AppRoot, {
278
+ * providers: [
279
+ * provideRealRouterFactory({ ... }),
280
+ * provideHttpStatusSink(sink),
281
+ * ],
282
+ * });
283
+ *
284
+ * response.status(sink.code ?? 200).send(html);
285
+ * ```
286
+ *
287
+ * Equivalent to:
288
+ *
289
+ * ```ts
290
+ * { provide: HTTP_STATUS_SINK, useValue: sink }
291
+ * ```
292
+ *
293
+ * Use the explicit `useValue` form when you need to compose with other
294
+ * application providers in a single `providers: [...]` block.
295
+ */
296
+ function provideHttpStatusSink(sink) {
297
+ return makeEnvironmentProviders([
298
+ { provide: HTTP_STATUS_SINK, useValue: sink },
299
+ ]);
300
+ }
301
+
302
+ // SSR-feature entry — Angular 21+
303
+ //
304
+ // Server-side and SSR-aware components/functions. Mirrors the
305
+ // `/ssr` subpath split shipped by every other adapter (#604 + #610).
306
+ // Trigger reached: `<ClientOnly>`, `<ServerOnly>`, `injectDeferred()`
307
+ // — three SSR-feature exports, ≥3 threshold per
308
+ // `.claude/SSR_FEATURE_GAPS_RU.md` §8.
309
+ //
310
+ // Asymmetric Angular note: Angular has no native `<Suspense>` /
311
+ // `use(promise)` analogue, so this entry exposes the signal-based
312
+ // `injectDeferred()` instead of `<Await>` / `<Streamed>` adapter
313
+ // components. Consumers compose with `@if (signal()) { … } @else { … }`,
314
+ // the `async` pipe (`from(deferredPromise)`), or native `@defer`
315
+ // blocks for chunk-level lazy hydration.
316
+ // Components
317
+
318
+ /**
319
+ * Generated bundle index. Do not edit.
320
+ */
321
+
322
+ export { ClientOnly, HTTP_STATUS_SINK, HttpStatusCode, ServerOnly, createHttpStatusSink, injectDeferred, provideHttpStatusSink };
323
+ //# sourceMappingURL=real-router-angular-ssr.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"real-router-angular-ssr.mjs","sources":["../../ssr/components/ClientOnly.ts","../../ssr/components/ServerOnly.ts","../../ssr/utils/createHttpStatusSink.ts","../../ssr/components/HttpStatusCode.ts","../../ssr/functions/injectDeferred.ts","../../ssr/functions/provideHttpStatusSink.ts","../../ssr/public_api.ts","../../ssr/real-router-angular-ssr.ts"],"sourcesContent":["import { NgTemplateOutlet } from \"@angular/common\";\nimport { afterNextRender, Component, input, signal } from \"@angular/core\";\n\nimport type { TemplateRef } from \"@angular/core\";\n\n@Component({\n selector: \"client-only\",\n template: `\n @if (mounted()) {\n <ng-content />\n } @else if (fallback()) {\n <ng-container [ngTemplateOutlet]=\"fallback() ?? null\" />\n }\n `,\n imports: [NgTemplateOutlet],\n})\nexport class ClientOnly {\n readonly fallback = input<TemplateRef<unknown>>();\n\n readonly mounted = signal(false);\n\n constructor() {\n afterNextRender(() => {\n this.mounted.set(true);\n });\n }\n}\n","import { NgTemplateOutlet } from \"@angular/common\";\nimport { afterNextRender, Component, input, signal } from \"@angular/core\";\n\nimport type { TemplateRef } from \"@angular/core\";\n\n@Component({\n selector: \"server-only\",\n template: `\n @if (!mounted()) {\n <ng-content />\n } @else if (fallback()) {\n <ng-container [ngTemplateOutlet]=\"fallback() ?? null\" />\n }\n `,\n imports: [NgTemplateOutlet],\n})\nexport class ServerOnly {\n readonly fallback = input<TemplateRef<unknown>>();\n\n readonly mounted = signal(false);\n\n constructor() {\n afterNextRender(() => {\n this.mounted.set(true);\n });\n }\n}\n","import { InjectionToken } from \"@angular/core\";\n\n/**\n * Render-scoped HTTP status sink. Created per request on the server and\n * provided via `provideHttpStatusSink(sink)` (or directly through\n * `{ provide: HTTP_STATUS_SINK, useValue: sink }`). Read after the SSR pass\n * (`renderApplication` / `AngularNodeAppEngine` rendering) to apply the value\n * to the HTTP response.\n *\n * Last write wins: if the rendered tree mounts more than one\n * `<http-status-code [code]=\"N\" />`, the value reflects the last component\n * that ran during the render pass.\n *\n * No-op on the client — `<http-status-code />` injects `HTTP_STATUS_SINK`\n * with `{ optional: true }` and skips the write when no provider is\n * registered, so the same component tree can be hydrated without changing\n * behaviour.\n *\n * Constraints:\n * - **Per-request only.** Don't share a sink across requests; the rendered\n * tree mutates `code` in place. Module-level singletons leak status\n * between concurrent requests.\n * - **Don't `Object.freeze` the sink.** The component writes to `.code`;\n * freezing makes the assignment throw under ESM strict mode.\n * - **Pass through `REQUEST_CONTEXT`, not via `req` properties.** Wire the\n * sink with `angularApp.handle(req, { httpStatusSink })` and read it back\n * in the `HTTP_STATUS_SINK` factory via `inject(REQUEST_CONTEXT)`.\n * `AngularNodeAppEngine` builds a fresh Web `Request` from the\n * `IncomingMessage` and discards every custom property, so attaching to\n * `req` directly silently no-ops.\n */\nexport interface HttpStatusSink {\n code: number | undefined;\n}\n\nexport function createHttpStatusSink(): HttpStatusSink {\n return { code: undefined };\n}\n\n/**\n * DI token for the request-scoped HTTP status sink. Application-side wiring:\n *\n * ```ts\n * import { bootstrapApplication } from \"@angular/platform-browser\";\n * import { provideHttpStatusSink, createHttpStatusSink } from \"@real-router/angular/ssr\";\n *\n * const sink = createHttpStatusSink();\n *\n * await bootstrapApplication(AppRoot, {\n * providers: [\n * provideRealRouterFactory({ ... }),\n * provideHttpStatusSink(sink),\n * ],\n * });\n *\n * response.status(sink.code ?? 200).send(html);\n * ```\n */\nexport const HTTP_STATUS_SINK = new InjectionToken<HttpStatusSink>(\n \"HTTP_STATUS_SINK\",\n);\n","import { Component, inject, input } from \"@angular/core\";\n\nimport { HTTP_STATUS_SINK } from \"../utils/createHttpStatusSink\";\n\nimport type { OnInit } from \"@angular/core\";\n\n/**\n * Render-time HTTP status declaration. Mount inside a route component\n * (typical use case: a glob `*` route's NotFound page) when the status is\n * decided by the rendered tree rather than a loader.\n *\n * Writes `code` to the optionally injected `HTTP_STATUS_SINK` in `ngOnInit`\n * (after the input binding has fired) and renders nothing. Without a provider\n * registered (the standard client-side case) the component is a silent no-op\n * — same component tree hydrates without touching the DOM or warning about\n * mismatches.\n *\n * Loader-driven errors (`LoaderNotFound` → 404, `LoaderRedirect` → 30x) keep\n * working as before; this component covers render-time decisions only.\n *\n * Last write wins when several `<http-status-code />` instances mount in the\n * same render pass — sink reflects the last component whose `ngOnInit` ran.\n *\n * ```ts\n * // entry-server.ts\n * import { bootstrapApplication } from \"@angular/platform-browser\";\n * import {\n * createHttpStatusSink,\n * provideHttpStatusSink,\n * } from \"@real-router/angular/ssr\";\n *\n * const sink = createHttpStatusSink();\n * await bootstrapApplication(AppRoot, {\n * providers: [\n * provideRealRouterFactory({ ... }),\n * provideHttpStatusSink(sink),\n * ],\n * });\n * response.status(sink.code ?? 200).send(html);\n * ```\n *\n * ```html\n * <!-- inside not-found.component.ts template -->\n * <http-status-code [code]=\"404\" />\n * ```\n *\n * **Per-request wiring with `AngularNodeAppEngine`:** the sink must be\n * passed via the second arg of `handle(req, requestContext)` — Angular\n * surfaces it through the `REQUEST_CONTEXT` token. Attaching to `req`\n * directly does NOT work: `AngularNodeAppEngine.handle` constructs a fresh\n * Web `Request` from the Express `IncomingMessage` and discards every\n * custom property. See the `ssr/` example's `app.config.ts` factory for\n * the canonical pattern (`inject(REQUEST_CONTEXT, { optional: true })`\n * → `(ctx as { httpStatusSink? } | null)?.httpStatusSink`).\n *\n * **`@angular/ssr` streaming + `@defer` blocks:** `@defer` blocks hydrate\n * lazily on the client; their server-side rendering is fully synchronous,\n * so `<http-status-code />` inside or outside a `@defer` writes to the\n * sink before `AngularNodeAppEngine.handle` resolves. No streaming\n * ordering concern in Angular's current SSR model.\n *\n * **JIT vs AOT:** the `code` input is declared as `input<number>()` (not\n * `input.required<number>()`) because `input.required` trips `NG0950` in\n * JIT/TestBed even after `componentRef.setInput(...)`. `ngOnInit` skips\n * the write when `code()` is `undefined`. AOT (production build) binds\n * the value normally and the skip never fires.\n *\n * **Valid `code` range:** Node's `res.end()` throws `Invalid status code`\n * on `NaN`, `0`, negative values, or values `> 999` — this surfaces as a\n * 5xx / dropped connection, not silent corruption. Pass a real HTTP status\n * integer (commonly 4xx/5xx; 100-999 is what Node accepts).\n */\n@Component({\n selector: \"http-status-code\",\n template: \"\",\n})\nexport class HttpStatusCode implements OnInit {\n /**\n * HTTP status to apply to the response. Common values: 404, 410, 451, 503.\n *\n * Declared as optional so the signal is safe to read in `ngOnInit` under\n * both AOT (template binding fires before init hooks) and JIT/TestBed\n * (`componentRef.setInput(\"code\", N)` writes the value before the first\n * change detection). `input.required` would trip `NG0950` in the JIT path\n * because the required-flag is asserted independently of the runtime\n * value. Consumers should always pass a value — `undefined` makes\n * `ngOnInit` skip the sink write rather than throw.\n */\n readonly code = input<number>();\n\n // Optional injection — when no `provideHttpStatusSink(...)` is registered\n // (client side) the field is null and `ngOnInit` skips the write.\n private readonly sink = inject(HTTP_STATUS_SINK, { optional: true });\n\n ngOnInit(): void {\n if (!this.sink) {\n return;\n }\n\n const value = this.code();\n\n if (value !== undefined) {\n this.sink.code = value;\n }\n }\n}\n","import { computed, effect, signal } from \"@angular/core\";\n\nimport { injectRoute } from \"@real-router/angular\";\n\nimport type { Signal } from \"@angular/core\";\n\ninterface DeferredContext {\n ssrDataDeferred?: Record<string, Promise<unknown>>;\n}\n\nconst NEVER_PROMISE = new Promise<never>(() => {\n // Intentionally never resolves — settles as `undefined` indefinitely when\n // a key is requested that the loader never declared. Surfaces consumer/\n // loader key drift as a visible \"loading\" state in the UI.\n});\n\n/**\n * Read a deferred promise published by `defer({ deferred: { <key>: Promise } })`\n * inside an SSR data loader. Returns an Angular `Signal<T | undefined>` that\n * tracks the active route — re-keying picks up the new state's deferred map.\n *\n * The signal starts `undefined` and updates to the resolved value once the\n * promise settles. Use with native Angular control flow:\n *\n * ```ts\n * @Component({\n * template: `\n * @if (reviews()) {\n * <ul>\n * @for (r of reviews(); track r.id) {\n * <li>{{ r.author }}</li>\n * }\n * </ul>\n * } @else {\n * <p>Loading reviews…</p>\n * }\n * `,\n * })\n * export class Reviews {\n * readonly reviews = injectDeferred<Review[]>(\"reviews\");\n * }\n * ```\n *\n * **Asymmetric Angular** (see `.claude/SSR_FEATURE_GAPS_RU.md` §8): Angular\n * does not ship `<Await>` / `<Streamed>` adapter components — Angular has no\n * direct analogue to React's `use(promise)` or Svelte's `{#await}`. Use\n * `@if (signal()) { … } @else { … }` or the `async` pipe with\n * `from(deferredPromise)` instead.\n */\nexport function injectDeferred<T = unknown>(\n key: string,\n): Signal<T | undefined> {\n const { routeState } = injectRoute();\n\n // Re-derive the promise reference whenever the route changes — invalidate()\n // + reload, navigation to a new route, etc. all refresh the underlying\n // deferred map, and we want the signal to track the *latest* promise.\n const promiseSignal = computed<Promise<T>>(() => {\n const context = routeState().route.context as DeferredContext;\n const deferred = context.ssrDataDeferred;\n\n return (deferred?.[key] ?? NEVER_PROMISE) as Promise<T>;\n });\n\n const value = signal<T | undefined>(undefined);\n\n effect((onCleanup) => {\n const promise = promiseSignal();\n let cancelled = false;\n\n onCleanup(() => {\n cancelled = true;\n });\n\n promise.then(\n (resolved) => {\n if (!cancelled) {\n value.set(resolved);\n }\n },\n /* v8 ignore next 4 -- @preserve: rejection branch — `effect` swallows\n async errors silently, so leaving the signal as `undefined` is the\n only observable behaviour. Real error surfacing is the loader's\n responsibility (throw → navigation rejects → app error boundary). */\n () => {\n // Intentional swallow — see v8 ignore note above.\n },\n );\n });\n\n return value.asReadonly();\n}\n","import { makeEnvironmentProviders } from \"@angular/core\";\n\nimport { HTTP_STATUS_SINK } from \"../utils/createHttpStatusSink\";\n\nimport type { HttpStatusSink } from \"../utils/createHttpStatusSink\";\nimport type { EnvironmentProviders } from \"@angular/core\";\n\n/**\n * Environment providers for a request-scoped `HttpStatusSink`. Pair with\n * `createHttpStatusSink()` and read `sink.code` after the SSR render pass\n * completes.\n *\n * Application bootstrap:\n *\n * ```ts\n * const sink = createHttpStatusSink();\n *\n * await bootstrapApplication(AppRoot, {\n * providers: [\n * provideRealRouterFactory({ ... }),\n * provideHttpStatusSink(sink),\n * ],\n * });\n *\n * response.status(sink.code ?? 200).send(html);\n * ```\n *\n * Equivalent to:\n *\n * ```ts\n * { provide: HTTP_STATUS_SINK, useValue: sink }\n * ```\n *\n * Use the explicit `useValue` form when you need to compose with other\n * application providers in a single `providers: [...]` block.\n */\nexport function provideHttpStatusSink(\n sink: HttpStatusSink,\n): EnvironmentProviders {\n return makeEnvironmentProviders([\n { provide: HTTP_STATUS_SINK, useValue: sink },\n ]);\n}\n","// SSR-feature entry — Angular 21+\n//\n// Server-side and SSR-aware components/functions. Mirrors the\n// `/ssr` subpath split shipped by every other adapter (#604 + #610).\n// Trigger reached: `<ClientOnly>`, `<ServerOnly>`, `injectDeferred()`\n// — three SSR-feature exports, ≥3 threshold per\n// `.claude/SSR_FEATURE_GAPS_RU.md` §8.\n//\n// Asymmetric Angular note: Angular has no native `<Suspense>` /\n// `use(promise)` analogue, so this entry exposes the signal-based\n// `injectDeferred()` instead of `<Await>` / `<Streamed>` adapter\n// components. Consumers compose with `@if (signal()) { … } @else { … }`,\n// the `async` pipe (`from(deferredPromise)`), or native `@defer`\n// blocks for chunk-level lazy hydration.\n\n// Components\nexport { ClientOnly } from \"./components/ClientOnly\";\n\nexport { ServerOnly } from \"./components/ServerOnly\";\n\nexport { HttpStatusCode } from \"./components/HttpStatusCode\";\n\n// Functions\nexport { injectDeferred } from \"./functions/injectDeferred\";\n\nexport { provideHttpStatusSink } from \"./functions/provideHttpStatusSink\";\n\n// Utilities\nexport {\n HTTP_STATUS_SINK,\n createHttpStatusSink,\n} from \"./utils/createHttpStatusSink\";\n\n// Types\nexport type { HttpStatusSink } from \"./utils/createHttpStatusSink\";\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public_api';\n"],"names":[],"mappings":"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"}