@real-router/angular 0.11.1 → 0.11.2
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/package.json +5 -6
- package/src/components/NavigationAnnouncer.ts +0 -18
- package/src/components/RouteView.ts +0 -141
- package/src/components/RouterErrorBoundary.ts +0 -72
- package/src/directives/RealLink.ts +0 -144
- package/src/directives/RealLinkActive.ts +0 -77
- package/src/directives/RouteMatch.ts +0 -7
- package/src/directives/RouteNotFound.ts +0 -6
- package/src/directives/RouteSelf.ts +0 -6
- package/src/dom-utils/direction-tracker.ts +0 -70
- package/src/dom-utils/index.ts +0 -31
- package/src/dom-utils/link-utils.ts +0 -339
- package/src/dom-utils/route-announcer.ts +0 -215
- package/src/dom-utils/scroll-restore.ts +0 -511
- package/src/dom-utils/scroll-spy.ts +0 -688
- package/src/dom-utils/view-transitions.ts +0 -142
- package/src/functions/index.ts +0 -29
- package/src/functions/injectIsActiveRoute.ts +0 -31
- package/src/functions/injectNavigator.ts +0 -12
- package/src/functions/injectOrThrow.ts +0 -19
- package/src/functions/injectRoute.ts +0 -39
- package/src/functions/injectRouteEnter.ts +0 -117
- package/src/functions/injectRouteExit.ts +0 -118
- package/src/functions/injectRouteNode.ts +0 -19
- package/src/functions/injectRouteUtils.ts +0 -15
- package/src/functions/injectRouter.ts +0 -12
- package/src/functions/injectRouterTransition.ts +0 -17
- package/src/index.ts +0 -63
- package/src/internal/buildActiveRouteOptions.ts +0 -20
- package/src/internal/install.ts +0 -90
- package/src/internal/subscribeSourceToSignal.ts +0 -48
- package/src/providers.ts +0 -80
- package/src/providersFactory.ts +0 -316
- package/src/sourceToSignal.ts +0 -28
- package/src/types.ts +0 -13
package/src/internal/install.ts
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import { ApplicationRef, DestroyRef, inject } from "@angular/core";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
createScrollRestoration,
|
|
5
|
-
createScrollSpy,
|
|
6
|
-
createViewTransitions,
|
|
7
|
-
} from "../dom-utils";
|
|
8
|
-
import { ROUTER } from "../providers";
|
|
9
|
-
|
|
10
|
-
import type { ScrollRestorationOptions, ScrollSpyOptions } from "../dom-utils";
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Shared installation helpers for `provideRealRouter` and
|
|
14
|
-
* `provideRealRouterFactory`. Must be called inside the body of a
|
|
15
|
-
* `provideEnvironmentInitializer(() => { ... })` callback so the active
|
|
16
|
-
* injection context resolves `ROUTER`, `ApplicationRef`, and `DestroyRef`.
|
|
17
|
-
*
|
|
18
|
-
* Closes review-2026-05-10 §8.1 MED — eliminates duplicate wiring between
|
|
19
|
-
* `providers.ts` and `providersFactory.ts` (high drift risk noted in the
|
|
20
|
-
* audit: the comment blocks were identical down to the punctuation).
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
export function installScrollRestoration(
|
|
24
|
-
options: ScrollRestorationOptions,
|
|
25
|
-
): void {
|
|
26
|
-
const router = inject(ROUTER);
|
|
27
|
-
const sr = createScrollRestoration(router, options);
|
|
28
|
-
|
|
29
|
-
inject(DestroyRef).onDestroy(() => {
|
|
30
|
-
sr.destroy();
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function installScrollSpy(options: ScrollSpyOptions): void {
|
|
35
|
-
const router = inject(ROUTER);
|
|
36
|
-
const spy = createScrollSpy(router, options);
|
|
37
|
-
|
|
38
|
-
inject(DestroyRef).onDestroy(() => {
|
|
39
|
-
spy.destroy();
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function installViewTransitions(): void {
|
|
44
|
-
const router = inject(ROUTER);
|
|
45
|
-
|
|
46
|
-
// Feature-detect `document.startViewTransition` once at install time. The
|
|
47
|
-
// `appRef.tick()` listener exists ONLY to feed Angular's zoneless CD into
|
|
48
|
-
// the VT utility's `setTimeout(0)`-driven snapshot capture (see comment
|
|
49
|
-
// below). When `startViewTransition` is unavailable (Firefox as of 2026-04,
|
|
50
|
-
// SSR, older browsers), `createViewTransitions` short-circuits to its
|
|
51
|
-
// frozen NOOP_INSTANCE — no leave subscriber registered, no
|
|
52
|
-
// `setTimeout(0)` invariant to satisfy. Installing the per-navigation
|
|
53
|
-
// tick listener anyway would force a synchronous CD pass on every
|
|
54
|
-
// navigation with zero benefit, doubling CD work in zoneless apps.
|
|
55
|
-
// Closes review-2026-05-10 §8.2 MED (view-transitions hot path).
|
|
56
|
-
const vtAvailable =
|
|
57
|
-
typeof document !== "undefined" &&
|
|
58
|
-
typeof document.startViewTransition === "function";
|
|
59
|
-
|
|
60
|
-
let offTick: (() => void) | undefined;
|
|
61
|
-
|
|
62
|
-
if (vtAvailable) {
|
|
63
|
-
// Force synchronous change detection on every transition success BEFORE
|
|
64
|
-
// the VT utility resolves its deferred. The utility uses `setTimeout(0)`
|
|
65
|
-
// to release the new-snapshot capture, which is load-bearing because
|
|
66
|
-
// Chromium blocks rAF callbacks while VT sits in the
|
|
67
|
-
// `update-callback-called` phase. Angular's zoneless CD is rAF-driven by
|
|
68
|
-
// default — without this synchronous tick the new DOM is not committed
|
|
69
|
-
// when the browser captures the new snapshot, so old and new snapshots
|
|
70
|
-
// end up identical and animations finish in ~0 ms with no visible work
|
|
71
|
-
// (the inner-route `products.list ↔ products.detail` morph in the
|
|
72
|
-
// example app was the canary).
|
|
73
|
-
//
|
|
74
|
-
// Subscribers fire in registration order; this one runs BEFORE
|
|
75
|
-
// `createViewTransitions` registers its own subscriber, guaranteeing CD
|
|
76
|
-
// completes first.
|
|
77
|
-
const appRef = inject(ApplicationRef);
|
|
78
|
-
|
|
79
|
-
offTick = router.subscribe(() => {
|
|
80
|
-
appRef.tick();
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const vt = createViewTransitions(router);
|
|
85
|
-
|
|
86
|
-
inject(DestroyRef).onDestroy(() => {
|
|
87
|
-
offTick?.();
|
|
88
|
-
vt.destroy();
|
|
89
|
-
});
|
|
90
|
-
}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import type { RouterSource } from "@real-router/sources";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Subscribe a `RouterSource<T>` to a write-callback and return a cleanup
|
|
5
|
-
* function. The shape is the per-effect-run pattern that `RealLink`,
|
|
6
|
-
* `RealLinkActive`, and `RouteView` all share inside their constructor
|
|
7
|
-
* `effect(...)` (review-2026-05-16 §8a MEDIUM — identical 8-line block
|
|
8
|
-
* repeated in 3 directives):
|
|
9
|
-
*
|
|
10
|
-
* 1. Read initial snapshot and apply it via `onSnapshot(snap)`.
|
|
11
|
-
* 2. Subscribe — every subsequent emission calls `onSnapshot(snap)` again.
|
|
12
|
-
* 3. Return a cleanup that unsubscribes and destroys the source. For
|
|
13
|
-
* cached factories from `@real-router/sources` (`createActiveRouteSource`,
|
|
14
|
-
* `createRouteNodeSource`, `getTransitionSource`, `getErrorSource`,
|
|
15
|
-
* `createDismissableError`) `destroy()` is a no-op on the shared
|
|
16
|
-
* wrapper, so this helper is safe to invoke from rapid effect re-runs
|
|
17
|
-
* under signal-input changes.
|
|
18
|
-
*
|
|
19
|
-
* Callers pass the result to `onCleanup(...)` from Angular's `effect()`.
|
|
20
|
-
*
|
|
21
|
-
* @example
|
|
22
|
-
* ```ts
|
|
23
|
-
* effect((onCleanup) => {
|
|
24
|
-
* const source = createActiveRouteSource(router, routeName(), params());
|
|
25
|
-
* onCleanup(
|
|
26
|
-
* subscribeSourceToSignal(source, (snap) => {
|
|
27
|
-
* this.isActive.set(snap);
|
|
28
|
-
* this.updateDom();
|
|
29
|
-
* }),
|
|
30
|
-
* );
|
|
31
|
-
* });
|
|
32
|
-
* ```
|
|
33
|
-
*/
|
|
34
|
-
export function subscribeSourceToSignal<T>(
|
|
35
|
-
source: RouterSource<T>,
|
|
36
|
-
onSnapshot: (snapshot: T) => void,
|
|
37
|
-
): () => void {
|
|
38
|
-
onSnapshot(source.getSnapshot());
|
|
39
|
-
|
|
40
|
-
const unsub = source.subscribe(() => {
|
|
41
|
-
onSnapshot(source.getSnapshot());
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
return () => {
|
|
45
|
-
unsub();
|
|
46
|
-
source.destroy();
|
|
47
|
-
};
|
|
48
|
-
}
|
package/src/providers.ts
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
InjectionToken,
|
|
3
|
-
makeEnvironmentProviders,
|
|
4
|
-
provideEnvironmentInitializer,
|
|
5
|
-
type EnvironmentProviders,
|
|
6
|
-
} from "@angular/core";
|
|
7
|
-
import { getNavigator, type Router, type Navigator } from "@real-router/core";
|
|
8
|
-
import { createRouteSource } from "@real-router/sources";
|
|
9
|
-
|
|
10
|
-
import {
|
|
11
|
-
installScrollRestoration,
|
|
12
|
-
installScrollSpy,
|
|
13
|
-
installViewTransitions,
|
|
14
|
-
} from "./internal/install";
|
|
15
|
-
import { sourceToSignal } from "./sourceToSignal";
|
|
16
|
-
|
|
17
|
-
import type { ScrollRestorationOptions, ScrollSpyOptions } from "./dom-utils";
|
|
18
|
-
import type { RouteSignals } from "./types";
|
|
19
|
-
|
|
20
|
-
export const ROUTER = new InjectionToken<Router>("ROUTER");
|
|
21
|
-
|
|
22
|
-
export const NAVIGATOR = new InjectionToken<Navigator>("NAVIGATOR");
|
|
23
|
-
|
|
24
|
-
export const ROUTE = new InjectionToken<RouteSignals>("ROUTE");
|
|
25
|
-
|
|
26
|
-
export interface RealRouterOptions {
|
|
27
|
-
scrollRestoration?: ScrollRestorationOptions;
|
|
28
|
-
scrollSpy?: ScrollSpyOptions;
|
|
29
|
-
viewTransitions?: boolean;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function provideRealRouter(
|
|
33
|
-
router: Router,
|
|
34
|
-
options?: RealRouterOptions,
|
|
35
|
-
): EnvironmentProviders {
|
|
36
|
-
const navigator = getNavigator(router);
|
|
37
|
-
|
|
38
|
-
// `Parameters<typeof makeEnvironmentProviders>[0]` is the actual union
|
|
39
|
-
// `(Provider | EnvironmentProviders | EnvironmentProviders[])[]` —
|
|
40
|
-
// `provideEnvironmentInitializer()` returns `EnvironmentProviders`, so the
|
|
41
|
-
// narrower `Provider[]` would force a cast at every push (review §8a — the
|
|
42
|
-
// proposed Provider[] swap was retracted after discovering this).
|
|
43
|
-
const providers: Parameters<typeof makeEnvironmentProviders>[0] = [
|
|
44
|
-
{ provide: ROUTER, useValue: router },
|
|
45
|
-
{ provide: NAVIGATOR, useValue: navigator },
|
|
46
|
-
{
|
|
47
|
-
provide: ROUTE,
|
|
48
|
-
useFactory: (): RouteSignals => ({
|
|
49
|
-
routeState: sourceToSignal(createRouteSource(router)),
|
|
50
|
-
navigator,
|
|
51
|
-
}),
|
|
52
|
-
},
|
|
53
|
-
];
|
|
54
|
-
|
|
55
|
-
if (options?.scrollRestoration) {
|
|
56
|
-
const scrollOpts = options.scrollRestoration;
|
|
57
|
-
|
|
58
|
-
providers.push(
|
|
59
|
-
provideEnvironmentInitializer(() => {
|
|
60
|
-
installScrollRestoration(scrollOpts);
|
|
61
|
-
}),
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (options?.scrollSpy && options.scrollSpy.selector !== "") {
|
|
66
|
-
const spyOpts = options.scrollSpy;
|
|
67
|
-
|
|
68
|
-
providers.push(
|
|
69
|
-
provideEnvironmentInitializer(() => {
|
|
70
|
-
installScrollSpy(spyOpts);
|
|
71
|
-
}),
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (options?.viewTransitions === true) {
|
|
76
|
-
providers.push(provideEnvironmentInitializer(installViewTransitions));
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return makeEnvironmentProviders(providers);
|
|
80
|
-
}
|
package/src/providersFactory.ts
DELETED
|
@@ -1,316 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
DestroyRef,
|
|
3
|
-
REQUEST,
|
|
4
|
-
TransferState,
|
|
5
|
-
inject,
|
|
6
|
-
makeEnvironmentProviders,
|
|
7
|
-
makeStateKey,
|
|
8
|
-
provideAppInitializer,
|
|
9
|
-
provideEnvironmentInitializer,
|
|
10
|
-
type EnvironmentProviders,
|
|
11
|
-
} from "@angular/core";
|
|
12
|
-
import {
|
|
13
|
-
getNavigator,
|
|
14
|
-
type DefaultDependencies,
|
|
15
|
-
type PluginFactory,
|
|
16
|
-
type Router,
|
|
17
|
-
} from "@real-router/core";
|
|
18
|
-
import { cloneRouter } from "@real-router/core/api";
|
|
19
|
-
import { hydrateRouter, serializeRouterState } from "@real-router/core/utils";
|
|
20
|
-
import { createRouteSource } from "@real-router/sources";
|
|
21
|
-
|
|
22
|
-
import {
|
|
23
|
-
installScrollRestoration,
|
|
24
|
-
installScrollSpy,
|
|
25
|
-
installViewTransitions,
|
|
26
|
-
} from "./internal/install";
|
|
27
|
-
import { NAVIGATOR, ROUTE, ROUTER } from "./providers";
|
|
28
|
-
import { sourceToSignal } from "./sourceToSignal";
|
|
29
|
-
|
|
30
|
-
import type { ScrollRestorationOptions, ScrollSpyOptions } from "./dom-utils";
|
|
31
|
-
import type { RouteSignals } from "./types";
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* `TransferState` key carrying the SSR-resolved router state from server to
|
|
35
|
-
* client as an XSS-safe JSON string (produced by `serializeRouterState`).
|
|
36
|
-
* Populated server-side by the `provideAppInitializer` callback after
|
|
37
|
-
* `router.start()` resolves; consumed client-side after hydration. Mirrors the
|
|
38
|
-
* `<script>window.__SSR_STATE__ = …</script>` pattern used by every other
|
|
39
|
-
* adapter — Angular's idiomatic transport is `TransferState` (#599).
|
|
40
|
-
*
|
|
41
|
-
* Stored as `string`: `serializeRouterState(state)` already produces JSON;
|
|
42
|
-
* `hydrateRouter(router, json)` accepts a JSON string and parses it once
|
|
43
|
-
* internally. Storing the parsed object would force a double round-trip
|
|
44
|
-
* (TransferState wraps every value in JSON for transport).
|
|
45
|
-
*
|
|
46
|
-
* Internal implementation detail. Not re-exported.
|
|
47
|
-
*/
|
|
48
|
-
const ROUTER_STATE_KEY = makeStateKey<string>("@real-router/angular:ssrState");
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Factory function for deriving per-request dependencies from an SSR `Request`.
|
|
52
|
-
*
|
|
53
|
-
* - **Server:** receives the real `Request` exposed via Angular's `REQUEST` token.
|
|
54
|
-
* - **SSG:** receives a mocked `Request` injected via `platformProviders`.
|
|
55
|
-
* - **Client:** receives `null` — derive deps from `document.cookie` etc.
|
|
56
|
-
*
|
|
57
|
-
* The returned object becomes the second argument to
|
|
58
|
-
* `cloneRouter(baseRouter, deps)`. Returning `undefined` clones the router with
|
|
59
|
-
* no extra deps (cloneRouter accepts the optional 2nd argument).
|
|
60
|
-
*/
|
|
61
|
-
export type RequestDepsFactory<
|
|
62
|
-
TDeps extends DefaultDependencies = DefaultDependencies,
|
|
63
|
-
> = (request: Request | null) => TDeps | undefined;
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Function form for conditional plugins (different sets server vs client).
|
|
67
|
-
*
|
|
68
|
-
* Use this when the plugin set must differ — typically because some plugins
|
|
69
|
-
* (e.g. `browser-plugin`, `navigation-plugin`, `hash-plugin`) touch
|
|
70
|
-
* `window.history` / `window.location` and cannot run on the server.
|
|
71
|
-
*/
|
|
72
|
-
export type RequestPluginsFactory<
|
|
73
|
-
TDeps extends DefaultDependencies = DefaultDependencies,
|
|
74
|
-
> = (request: Request | null) => readonly PluginFactory<TDeps>[];
|
|
75
|
-
|
|
76
|
-
export interface RealRouterFactoryOptions<
|
|
77
|
-
TDeps extends DefaultDependencies = DefaultDependencies,
|
|
78
|
-
> {
|
|
79
|
-
/**
|
|
80
|
-
* Base router instance — created once at app bootstrap (typically inside
|
|
81
|
-
* `app.config.ts` module scope). Each request clones this router via
|
|
82
|
-
* `cloneRouter(baseRouter, deps?.(request))`, producing an isolated
|
|
83
|
-
* router with its own state, plugins, and subscriptions.
|
|
84
|
-
*
|
|
85
|
-
* **Important:** the `baseRouter` MUST NOT be started ahead of time —
|
|
86
|
-
* `provideAppInitializer` is responsible for calling `router.start(url)`
|
|
87
|
-
* inside the per-request DI scope.
|
|
88
|
-
*/
|
|
89
|
-
baseRouter: Router<TDeps>;
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Plugins applied to every per-request router clone.
|
|
93
|
-
*
|
|
94
|
-
* **Static form** — same plugins on both sides:
|
|
95
|
-
* ```ts
|
|
96
|
-
* plugins: [ssrDataPluginFactory(loaders)]
|
|
97
|
-
* ```
|
|
98
|
-
*
|
|
99
|
-
* **Function form** — conditional client vs server (recommended when any
|
|
100
|
-
* browser-only plugin is involved):
|
|
101
|
-
* ```ts
|
|
102
|
-
* plugins: (request) => request
|
|
103
|
-
* ? [ssrDataPluginFactory(loaders)]
|
|
104
|
-
* : [browserPluginFactory(), ssrDataPluginFactory(loaders)],
|
|
105
|
-
* ```
|
|
106
|
-
*
|
|
107
|
-
* Function form is required if the plugin list contains
|
|
108
|
-
* `browser-plugin`, `navigation-plugin`, or `hash-plugin` — those plugins
|
|
109
|
-
* read `window.history` / `window.location` and crash on the server.
|
|
110
|
-
*/
|
|
111
|
-
plugins?: readonly PluginFactory<TDeps>[] | RequestPluginsFactory<TDeps>;
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Derive request-scoped deps (e.g. `currentUser` from cookies). The result
|
|
115
|
-
* is passed to `cloneRouter(baseRouter, deps)` and merged with any deps
|
|
116
|
-
* already registered on the base router.
|
|
117
|
-
*
|
|
118
|
-
* Receives `request: Request | null`:
|
|
119
|
-
* - non-null on server (real `Request` from `@angular/ssr` runtime)
|
|
120
|
-
* - non-null on SSG (mocked `Request` via `platformProviders`)
|
|
121
|
-
* - null on client (derive deps externally — e.g. parse `document.cookie`)
|
|
122
|
-
*/
|
|
123
|
-
deps?: RequestDepsFactory<TDeps>;
|
|
124
|
-
|
|
125
|
-
/** Optional scroll restoration — same semantics as `provideRealRouter`. */
|
|
126
|
-
scrollRestoration?: ScrollRestorationOptions;
|
|
127
|
-
|
|
128
|
-
/** Optional scroll spy — same semantics as `provideRealRouter`. */
|
|
129
|
-
scrollSpy?: ScrollSpyOptions;
|
|
130
|
-
|
|
131
|
-
/** Optional view transitions — same semantics as `provideRealRouter`. */
|
|
132
|
-
viewTransitions?: boolean;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* `provideRealRouterFactory` — environment providers for SSR / SSG scenarios.
|
|
137
|
-
*
|
|
138
|
-
* Unlike `provideRealRouter(router)` (single instance via `useValue`), this
|
|
139
|
-
* factory uses `useFactory` to produce a per-request router clone:
|
|
140
|
-
*
|
|
141
|
-
* 1. Reads Angular's `REQUEST` token (`{ optional: true }`).
|
|
142
|
-
* 2. Calls `cloneRouter(baseRouter, deps?.(request))` to create a request-scoped clone.
|
|
143
|
-
* 3. Applies plugins (`plugins` array or `plugins(request)` factory).
|
|
144
|
-
* 4. Registers `provideAppInitializer` that calls `await router.start(url)`.
|
|
145
|
-
* 5. Schedules `router.dispose()` via `DestroyRef.onDestroy` — the request
|
|
146
|
-
* Injector is destroyed after the response is sent, releasing all
|
|
147
|
-
* subscriptions and plugins.
|
|
148
|
-
*
|
|
149
|
-
* Use cases:
|
|
150
|
-
* - Angular SSR with `@angular/ssr` (`outputMode: "server"`).
|
|
151
|
-
* - SSG build-time render via `renderApplication` + `platformProviders` `REQUEST` mock.
|
|
152
|
-
* - Multi-tenant request-scoped routing.
|
|
153
|
-
*
|
|
154
|
-
* Existing single-instance scenarios (SPA, SSG client after hydration) continue
|
|
155
|
-
* to use `provideRealRouter(router)` — both APIs ship in parallel.
|
|
156
|
-
*
|
|
157
|
-
* @param options - Factory configuration — see `RealRouterFactoryOptions`.
|
|
158
|
-
* @returns `EnvironmentProviders` to spread into `ApplicationConfig.providers`.
|
|
159
|
-
*/
|
|
160
|
-
export function provideRealRouterFactory<
|
|
161
|
-
TDeps extends DefaultDependencies = DefaultDependencies,
|
|
162
|
-
>(options: RealRouterFactoryOptions<TDeps>): EnvironmentProviders {
|
|
163
|
-
const {
|
|
164
|
-
baseRouter,
|
|
165
|
-
plugins,
|
|
166
|
-
deps,
|
|
167
|
-
scrollRestoration,
|
|
168
|
-
scrollSpy,
|
|
169
|
-
viewTransitions,
|
|
170
|
-
} = options;
|
|
171
|
-
|
|
172
|
-
const providers: Parameters<typeof makeEnvironmentProviders>[0] = [
|
|
173
|
-
{
|
|
174
|
-
provide: ROUTER,
|
|
175
|
-
useFactory: (): Router => {
|
|
176
|
-
const request = inject(REQUEST, { optional: true });
|
|
177
|
-
const requestDeps = deps?.(request);
|
|
178
|
-
const router = cloneRouter(baseRouter, requestDeps);
|
|
179
|
-
|
|
180
|
-
const pluginList =
|
|
181
|
-
typeof plugins === "function" ? plugins(request) : plugins;
|
|
182
|
-
|
|
183
|
-
if (pluginList && pluginList.length > 0) {
|
|
184
|
-
// Variadic — `usePlugin` accepts `(PluginFactory<D> | false | null | undefined)[]`.
|
|
185
|
-
router.usePlugin(...pluginList);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// Per-request cleanup. The application Injector is destroyed:
|
|
189
|
-
// - On server: after `writeResponseToNodeResponse` finishes the response
|
|
190
|
-
// (request scope ends).
|
|
191
|
-
// - On client: at `ApplicationRef.destroy` (rare in SPA, common in TestBed).
|
|
192
|
-
// - In SSG build: after each `renderApplication` resolves.
|
|
193
|
-
inject(DestroyRef).onDestroy(() => {
|
|
194
|
-
router.dispose();
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
return router as unknown as Router;
|
|
198
|
-
},
|
|
199
|
-
},
|
|
200
|
-
{
|
|
201
|
-
provide: NAVIGATOR,
|
|
202
|
-
useFactory: () => getNavigator(inject(ROUTER)),
|
|
203
|
-
},
|
|
204
|
-
{
|
|
205
|
-
provide: ROUTE,
|
|
206
|
-
useFactory: (): RouteSignals => {
|
|
207
|
-
const router = inject(ROUTER);
|
|
208
|
-
|
|
209
|
-
return {
|
|
210
|
-
routeState: sourceToSignal(createRouteSource(router)),
|
|
211
|
-
navigator: inject(NAVIGATOR),
|
|
212
|
-
};
|
|
213
|
-
},
|
|
214
|
-
},
|
|
215
|
-
// Async bootstrap — runs before the first component renders. Three
|
|
216
|
-
// branches based on TransferState population:
|
|
217
|
-
//
|
|
218
|
-
// 1. **Client after hydration** — server populated TransferState with
|
|
219
|
-
// the SSR-resolved router state. Consume it via `hydrateRouter`,
|
|
220
|
-
// which deposits the parsed state into the one-shot
|
|
221
|
-
// `RouterInternals.hydrationState` scratchpad before invoking
|
|
222
|
-
// `router.start(state.path)`. SSR loader plugins
|
|
223
|
-
// (`@real-router/ssr-data-plugin`, `@real-router/rsc-server-plugin`)
|
|
224
|
-
// read the scratchpad and skip the loader on first paint — parity
|
|
225
|
-
// with the other 5 adapters that consume `<script>__SSR_STATE__</script>` (#596, #599).
|
|
226
|
-
//
|
|
227
|
-
// 2. **Server / SSG** — TransferState empty; run the regular
|
|
228
|
-
// `router.start(path)`. After it resolves, write the serialized
|
|
229
|
-
// state back into TransferState so the matching client run lands
|
|
230
|
-
// in branch 1. Angular's `TransferState` infrastructure
|
|
231
|
-
// (provided by `provideClientHydration()`) carries this blob to
|
|
232
|
-
// the client as a `<script id="ng-state">` payload.
|
|
233
|
-
//
|
|
234
|
-
// 3. **Pure CSR** — TransferState empty (never populated by a server
|
|
235
|
-
// pass), and `inject(REQUEST, { optional: true })` returns null.
|
|
236
|
-
// Falls into the same `router.start(path)` branch as server-side
|
|
237
|
-
// but skips the TransferState write (no client to hand off to).
|
|
238
|
-
//
|
|
239
|
-
// Errors propagate (Option A from RFC §10): the bootstrap fails and the
|
|
240
|
-
// server returns 500. Custom error pages should be wired via
|
|
241
|
-
// `RouterErrorBoundary` on subsequent renders.
|
|
242
|
-
provideAppInitializer(async () => {
|
|
243
|
-
const router = inject(ROUTER);
|
|
244
|
-
const request = inject(REQUEST, { optional: true });
|
|
245
|
-
const transferState = inject(TransferState);
|
|
246
|
-
|
|
247
|
-
const ssrJson = transferState.get(ROUTER_STATE_KEY, null);
|
|
248
|
-
|
|
249
|
-
if (ssrJson !== null) {
|
|
250
|
-
// Branch 1: client after hydration — reuse server-resolved state.
|
|
251
|
-
await hydrateRouter(router, ssrJson);
|
|
252
|
-
// One-shot semantic, parity with `delete window.__SSR_STATE__`.
|
|
253
|
-
transferState.remove(ROUTER_STATE_KEY);
|
|
254
|
-
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// Branches 2 & 3: regular start.
|
|
259
|
-
// Browser-plugin's `start` interceptor (when registered) wraps this call
|
|
260
|
-
// with location-derived path. We always pass an explicit string — the
|
|
261
|
-
// interceptor uses the explicit value because `next(path ?? location)`
|
|
262
|
-
// short-circuits when `path` is non-nullish.
|
|
263
|
-
const path = deriveStartPath(request);
|
|
264
|
-
const state = await router.start(path);
|
|
265
|
-
|
|
266
|
-
if (request !== null) {
|
|
267
|
-
// Branch 2: running inside `@angular/ssr`'s request handler — write
|
|
268
|
-
// serialized state to TransferState so the matching client run can
|
|
269
|
-
// skip the loader on first paint.
|
|
270
|
-
transferState.set(ROUTER_STATE_KEY, serializeRouterState(state));
|
|
271
|
-
}
|
|
272
|
-
}),
|
|
273
|
-
];
|
|
274
|
-
|
|
275
|
-
if (scrollRestoration) {
|
|
276
|
-
providers.push(
|
|
277
|
-
provideEnvironmentInitializer(() => {
|
|
278
|
-
installScrollRestoration(scrollRestoration);
|
|
279
|
-
}),
|
|
280
|
-
);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
if (scrollSpy && scrollSpy.selector !== "") {
|
|
284
|
-
providers.push(
|
|
285
|
-
provideEnvironmentInitializer(() => {
|
|
286
|
-
installScrollSpy(scrollSpy);
|
|
287
|
-
}),
|
|
288
|
-
);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
if (viewTransitions === true) {
|
|
292
|
-
providers.push(provideEnvironmentInitializer(installViewTransitions));
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
return makeEnvironmentProviders(providers);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/**
|
|
299
|
-
* Derive the path passed to `router.start(path)`:
|
|
300
|
-
* - Server / SSG: `request.url` → pathname + search.
|
|
301
|
-
* - Client: `window.location` if available.
|
|
302
|
-
* - Fallback: `"/"` (only reachable in synthetic non-browser non-SSR setups).
|
|
303
|
-
*/
|
|
304
|
-
function deriveStartPath(request: Request | null): string {
|
|
305
|
-
if (request) {
|
|
306
|
-
const url = new URL(request.url);
|
|
307
|
-
|
|
308
|
-
return url.pathname + url.search;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
if (typeof globalThis.window !== "undefined") {
|
|
312
|
-
return globalThis.location.pathname + globalThis.location.search;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
return "/";
|
|
316
|
-
}
|
package/src/sourceToSignal.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { signal, type Signal, inject, DestroyRef } from "@angular/core";
|
|
2
|
-
|
|
3
|
-
import type { RouterSource } from "@real-router/sources";
|
|
4
|
-
|
|
5
|
-
/** Must be called within an injection context (constructor, field initializer, runInInjectionContext). */
|
|
6
|
-
export function sourceToSignal<T>(source: RouterSource<T>): Signal<T> {
|
|
7
|
-
const sig = signal<T>(source.getSnapshot());
|
|
8
|
-
const destroyRef = inject(DestroyRef);
|
|
9
|
-
|
|
10
|
-
const unsubscribe = source.subscribe(() => {
|
|
11
|
-
sig.set(source.getSnapshot());
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
destroyRef.onDestroy(() => {
|
|
15
|
-
// `try/finally` guarantees `source.destroy()` runs even if `unsubscribe`
|
|
16
|
-
// throws. Cached sources from `@real-router/sources` keep `destroy()` as
|
|
17
|
-
// a no-op (so they survive multi-consumer teardown), but non-cached
|
|
18
|
-
// sources rely on this call to release their router subscription —
|
|
19
|
-
// skipping it on an unsubscribe throw would leak the listener.
|
|
20
|
-
try {
|
|
21
|
-
unsubscribe();
|
|
22
|
-
} finally {
|
|
23
|
-
source.destroy();
|
|
24
|
-
}
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
return sig.asReadonly();
|
|
28
|
-
}
|
package/src/types.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import type { Signal } from "@angular/core";
|
|
2
|
-
import type { Navigator, Params, RouterError } from "@real-router/core";
|
|
3
|
-
import type { RouteSnapshot } from "@real-router/sources";
|
|
4
|
-
|
|
5
|
-
export interface RouteSignals<P extends Params = Params> {
|
|
6
|
-
readonly routeState: Signal<RouteSnapshot<P>>;
|
|
7
|
-
readonly navigator: Navigator;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface ErrorContext {
|
|
11
|
-
$implicit: RouterError;
|
|
12
|
-
resetError: () => void;
|
|
13
|
-
}
|