@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,92 @@
1
+ import { computed, effect, signal } from "@angular/core";
2
+
3
+ import { injectRoute } from "@real-router/angular";
4
+
5
+ import type { Signal } from "@angular/core";
6
+
7
+ interface DeferredContext {
8
+ ssrDataDeferred?: Record<string, Promise<unknown>>;
9
+ }
10
+
11
+ const NEVER_PROMISE = new Promise<never>(() => {
12
+ // Intentionally never resolves — settles as `undefined` indefinitely when
13
+ // a key is requested that the loader never declared. Surfaces consumer/
14
+ // loader key drift as a visible "loading" state in the UI.
15
+ });
16
+
17
+ /**
18
+ * Read a deferred promise published by `defer({ deferred: { <key>: Promise } })`
19
+ * inside an SSR data loader. Returns an Angular `Signal<T | undefined>` that
20
+ * tracks the active route — re-keying picks up the new state's deferred map.
21
+ *
22
+ * The signal starts `undefined` and updates to the resolved value once the
23
+ * promise settles. Use with native Angular control flow:
24
+ *
25
+ * ```ts
26
+ * @Component({
27
+ * template: `
28
+ * @if (reviews()) {
29
+ * <ul>
30
+ * @for (r of reviews(); track r.id) {
31
+ * <li>{{ r.author }}</li>
32
+ * }
33
+ * </ul>
34
+ * } @else {
35
+ * <p>Loading reviews…</p>
36
+ * }
37
+ * `,
38
+ * })
39
+ * export class Reviews {
40
+ * readonly reviews = injectDeferred<Review[]>("reviews");
41
+ * }
42
+ * ```
43
+ *
44
+ * **Asymmetric Angular** (see `.claude/SSR_FEATURE_GAPS_RU.md` §8): Angular
45
+ * does not ship `<Await>` / `<Streamed>` adapter components — Angular has no
46
+ * direct analogue to React's `use(promise)` or Svelte's `{#await}`. Use
47
+ * `@if (signal()) { … } @else { … }` or the `async` pipe with
48
+ * `from(deferredPromise)` instead.
49
+ */
50
+ export function injectDeferred<T = unknown>(
51
+ key: string,
52
+ ): Signal<T | undefined> {
53
+ const { routeState } = injectRoute();
54
+
55
+ // Re-derive the promise reference whenever the route changes — invalidate()
56
+ // + reload, navigation to a new route, etc. all refresh the underlying
57
+ // deferred map, and we want the signal to track the *latest* promise.
58
+ const promiseSignal = computed<Promise<T>>(() => {
59
+ const context = routeState().route.context as DeferredContext;
60
+ const deferred = context.ssrDataDeferred;
61
+
62
+ return (deferred?.[key] ?? NEVER_PROMISE) as Promise<T>;
63
+ });
64
+
65
+ const value = signal<T | undefined>(undefined);
66
+
67
+ effect((onCleanup) => {
68
+ const promise = promiseSignal();
69
+ let cancelled = false;
70
+
71
+ onCleanup(() => {
72
+ cancelled = true;
73
+ });
74
+
75
+ promise.then(
76
+ (resolved) => {
77
+ if (!cancelled) {
78
+ value.set(resolved);
79
+ }
80
+ },
81
+ /* v8 ignore next 4 -- @preserve: rejection branch — `effect` swallows
82
+ async errors silently, so leaving the signal as `undefined` is the
83
+ only observable behaviour. Real error surfacing is the loader's
84
+ responsibility (throw → navigation rejects → app error boundary). */
85
+ () => {
86
+ // Intentional swallow — see v8 ignore note above.
87
+ },
88
+ );
89
+ });
90
+
91
+ return value.asReadonly();
92
+ }
@@ -0,0 +1,43 @@
1
+ import { makeEnvironmentProviders } from "@angular/core";
2
+
3
+ import { HTTP_STATUS_SINK } from "../utils/createHttpStatusSink";
4
+
5
+ import type { HttpStatusSink } from "../utils/createHttpStatusSink";
6
+ import type { EnvironmentProviders } from "@angular/core";
7
+
8
+ /**
9
+ * Environment providers for a request-scoped `HttpStatusSink`. Pair with
10
+ * `createHttpStatusSink()` and read `sink.code` after the SSR render pass
11
+ * completes.
12
+ *
13
+ * Application bootstrap:
14
+ *
15
+ * ```ts
16
+ * const sink = createHttpStatusSink();
17
+ *
18
+ * await bootstrapApplication(AppRoot, {
19
+ * providers: [
20
+ * provideRealRouterFactory({ ... }),
21
+ * provideHttpStatusSink(sink),
22
+ * ],
23
+ * });
24
+ *
25
+ * response.status(sink.code ?? 200).send(html);
26
+ * ```
27
+ *
28
+ * Equivalent to:
29
+ *
30
+ * ```ts
31
+ * { provide: HTTP_STATUS_SINK, useValue: sink }
32
+ * ```
33
+ *
34
+ * Use the explicit `useValue` form when you need to compose with other
35
+ * application providers in a single `providers: [...]` block.
36
+ */
37
+ export function provideHttpStatusSink(
38
+ sink: HttpStatusSink,
39
+ ): EnvironmentProviders {
40
+ return makeEnvironmentProviders([
41
+ { provide: HTTP_STATUS_SINK, useValue: sink },
42
+ ]);
43
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/ng-packagr/ng-packagr/main/src/ng-package.schema.json",
3
+ "lib": {
4
+ "entryFile": "public_api.ts"
5
+ }
6
+ }
@@ -0,0 +1,35 @@
1
+ // SSR-feature entry — Angular 21+
2
+ //
3
+ // Server-side and SSR-aware components/functions. Mirrors the
4
+ // `/ssr` subpath split shipped by every other adapter (#604 + #610).
5
+ // Trigger reached: `<ClientOnly>`, `<ServerOnly>`, `injectDeferred()`
6
+ // — three SSR-feature exports, ≥3 threshold per
7
+ // `.claude/SSR_FEATURE_GAPS_RU.md` §8.
8
+ //
9
+ // Asymmetric Angular note: Angular has no native `<Suspense>` /
10
+ // `use(promise)` analogue, so this entry exposes the signal-based
11
+ // `injectDeferred()` instead of `<Await>` / `<Streamed>` adapter
12
+ // components. Consumers compose with `@if (signal()) { … } @else { … }`,
13
+ // the `async` pipe (`from(deferredPromise)`), or native `@defer`
14
+ // blocks for chunk-level lazy hydration.
15
+
16
+ // Components
17
+ export { ClientOnly } from "./components/ClientOnly";
18
+
19
+ export { ServerOnly } from "./components/ServerOnly";
20
+
21
+ export { HttpStatusCode } from "./components/HttpStatusCode";
22
+
23
+ // Functions
24
+ export { injectDeferred } from "./functions/injectDeferred";
25
+
26
+ export { provideHttpStatusSink } from "./functions/provideHttpStatusSink";
27
+
28
+ // Utilities
29
+ export {
30
+ HTTP_STATUS_SINK,
31
+ createHttpStatusSink,
32
+ } from "./utils/createHttpStatusSink";
33
+
34
+ // Types
35
+ export type { HttpStatusSink } from "./utils/createHttpStatusSink";
@@ -0,0 +1,61 @@
1
+ import { InjectionToken } from "@angular/core";
2
+
3
+ /**
4
+ * Render-scoped HTTP status sink. Created per request on the server and
5
+ * provided via `provideHttpStatusSink(sink)` (or directly through
6
+ * `{ provide: HTTP_STATUS_SINK, useValue: sink }`). Read after the SSR pass
7
+ * (`renderApplication` / `AngularNodeAppEngine` rendering) to apply the value
8
+ * to the HTTP response.
9
+ *
10
+ * Last write wins: if the rendered tree mounts more than one
11
+ * `<http-status-code [code]="N" />`, the value reflects the last component
12
+ * that ran during the render pass.
13
+ *
14
+ * No-op on the client — `<http-status-code />` injects `HTTP_STATUS_SINK`
15
+ * with `{ optional: true }` and skips the write when no provider is
16
+ * registered, so the same component tree can be hydrated without changing
17
+ * behaviour.
18
+ *
19
+ * Constraints:
20
+ * - **Per-request only.** Don't share a sink across requests; the rendered
21
+ * tree mutates `code` in place. Module-level singletons leak status
22
+ * between concurrent requests.
23
+ * - **Don't `Object.freeze` the sink.** The component writes to `.code`;
24
+ * freezing makes the assignment throw under ESM strict mode.
25
+ * - **Pass through `REQUEST_CONTEXT`, not via `req` properties.** Wire the
26
+ * sink with `angularApp.handle(req, { httpStatusSink })` and read it back
27
+ * in the `HTTP_STATUS_SINK` factory via `inject(REQUEST_CONTEXT)`.
28
+ * `AngularNodeAppEngine` builds a fresh Web `Request` from the
29
+ * `IncomingMessage` and discards every custom property, so attaching to
30
+ * `req` directly silently no-ops.
31
+ */
32
+ export interface HttpStatusSink {
33
+ code: number | undefined;
34
+ }
35
+
36
+ export function createHttpStatusSink(): HttpStatusSink {
37
+ return { code: undefined };
38
+ }
39
+
40
+ /**
41
+ * DI token for the request-scoped HTTP status sink. Application-side wiring:
42
+ *
43
+ * ```ts
44
+ * import { bootstrapApplication } from "@angular/platform-browser";
45
+ * import { provideHttpStatusSink, createHttpStatusSink } from "@real-router/angular/ssr";
46
+ *
47
+ * const sink = createHttpStatusSink();
48
+ *
49
+ * await bootstrapApplication(AppRoot, {
50
+ * providers: [
51
+ * provideRealRouterFactory({ ... }),
52
+ * provideHttpStatusSink(sink),
53
+ * ],
54
+ * });
55
+ *
56
+ * response.status(sink.code ?? 200).send(html);
57
+ * ```
58
+ */
59
+ export const HTTP_STATUS_SINK = new InjectionToken<HttpStatusSink>(
60
+ "HTTP_STATUS_SINK",
61
+ );