@real-router/angular 0.8.1 → 0.10.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.
- package/README.md +184 -5
- package/dist/README.md +184 -5
- package/dist/fesm2022/real-router-angular-ssr.mjs +323 -0
- package/dist/fesm2022/real-router-angular-ssr.mjs.map +1 -0
- package/dist/fesm2022/real-router-angular.mjs +773 -180
- package/dist/fesm2022/real-router-angular.mjs.map +1 -1
- package/dist/types/real-router-angular-ssr.d.ts +227 -0
- package/dist/types/real-router-angular-ssr.d.ts.map +1 -0
- package/dist/types/real-router-angular.d.ts +119 -20
- package/dist/types/real-router-angular.d.ts.map +1 -1
- package/package.json +17 -10
- package/src/components/RouteView.ts +81 -56
- package/src/components/RouterErrorBoundary.ts +7 -5
- package/src/directives/RealLink.ts +57 -37
- package/src/directives/RealLinkActive.ts +34 -25
- package/src/dom-utils/link-utils.ts +119 -7
- package/src/dom-utils/route-announcer.ts +58 -2
- package/src/dom-utils/scroll-restore.ts +179 -23
- package/src/functions/injectIsActiveRoute.ts +9 -8
- package/src/functions/injectNavigator.ts +4 -0
- package/src/functions/injectOrThrow.ts +5 -1
- package/src/functions/injectRoute.ts +17 -8
- package/src/functions/injectRouteEnter.ts +5 -10
- package/src/functions/injectRouteNode.ts +3 -0
- package/src/functions/injectRouteUtils.ts +3 -0
- package/src/functions/injectRouter.ts +4 -0
- package/src/functions/injectRouterTransition.ts +3 -0
- package/src/index.ts +14 -3
- package/src/internal/buildActiveRouteOptions.ts +20 -0
- package/src/internal/install.ts +77 -0
- package/src/internal/subscribeSourceToSignal.ts +48 -0
- package/src/providers.ts +11 -38
- package/src/providersFactory.ts +298 -0
- package/src/sourceToSignal.ts +10 -2
- package/src/types.ts +6 -1
- package/ssr/components/ClientOnly.ts +27 -0
- package/ssr/components/HttpStatusCode.ts +106 -0
- package/ssr/components/ServerOnly.ts +27 -0
- package/ssr/functions/injectDeferred.ts +92 -0
- package/ssr/functions/provideHttpStatusSink.ts +43 -0
- package/ssr/ng-package.json +6 -0
- package/ssr/public_api.ts +35 -0
- 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.13", ngImport: i0, type: ClientOnly, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
15
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.13", 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.13", 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.13", ngImport: i0, type: ServerOnly, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
47
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.13", 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.13", 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.13", ngImport: i0, type: HttpStatusCode, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
186
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.13", 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.13", 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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"}
|