@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.
- package/README.md +183 -4
- package/dist/README.md +183 -4
- 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 +759 -173
- 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 +18 -11
- 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 +160 -12
- 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,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,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
|
+
);
|