@real-router/core 0.54.5 → 0.55.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/dist/cjs/Router-C7eE1kIK.js +6 -0
- package/dist/cjs/Router-C7eE1kIK.js.map +1 -0
- package/dist/cjs/{Router-CJihdrWA.d.ts → Router-Dg-zk8AS.d.ts} +1 -1
- package/dist/cjs/{Router-CJihdrWA.d.ts.map → Router-Dg-zk8AS.d.ts.map} +1 -1
- package/dist/cjs/{RouterError-Bm9YnZ6e.d.ts → RouterError-WhCzIWuc.d.ts} +1 -1
- package/dist/cjs/{RouterError-Bm9YnZ6e.d.ts.map → RouterError-WhCzIWuc.d.ts.map} +1 -1
- package/dist/cjs/api.d.ts +2 -2
- package/dist/cjs/api.d.ts.map +1 -1
- package/dist/cjs/api.js +1 -1
- package/dist/cjs/api.js.map +1 -1
- package/dist/cjs/{cloneRouter-D5bMudso.js → cloneRouter-C9Rth_8U.js} +2 -2
- package/dist/cjs/{cloneRouter-D5bMudso.js.map → cloneRouter-C9Rth_8U.js.map} +1 -1
- package/dist/cjs/{index-EwbhzRQw.d.ts → index-C-i6vx5Y.d.ts} +1 -1
- package/dist/cjs/{index-EwbhzRQw.d.ts.map → index-C-i6vx5Y.d.ts.map} +1 -1
- package/dist/cjs/{index-8oPDJBQc.d.ts → index-K1U_fqfJ.d.ts} +2 -2
- package/dist/cjs/{index-8oPDJBQc.d.ts.map → index-K1U_fqfJ.d.ts.map} +1 -1
- package/dist/cjs/index.d.ts +3 -3
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/{internals-CM6oaz9n.js → internals-CWMOL1B8.js} +2 -2
- package/dist/cjs/{internals-CM6oaz9n.js.map → internals-CWMOL1B8.js.map} +1 -1
- package/dist/cjs/utils.d.ts +1 -1
- package/dist/cjs/utils.js +1 -1
- package/dist/cjs/validation.d.ts +4 -4
- package/dist/cjs/validation.js +1 -1
- package/dist/esm/{Router-BmhiDQUJ.d.mts → Router-Dg-zk8AS.d.mts} +1 -1
- package/dist/esm/{Router-BmhiDQUJ.d.mts.map → Router-Dg-zk8AS.d.mts.map} +1 -1
- package/dist/esm/Router-DiZbYMLx.mjs +6 -0
- package/dist/esm/Router-DiZbYMLx.mjs.map +1 -0
- package/dist/esm/{RouterError-hhfSVGtY.d.mts → RouterError-WhCzIWuc.d.mts} +1 -1
- package/dist/esm/{RouterError-hhfSVGtY.d.mts.map → RouterError-WhCzIWuc.d.mts.map} +1 -1
- package/dist/esm/api.d.mts +2 -2
- package/dist/esm/api.d.mts.map +1 -1
- package/dist/esm/api.mjs +1 -1
- package/dist/esm/api.mjs.map +1 -1
- package/dist/esm/cloneRouter-BYNiwchg.mjs +2 -0
- package/dist/esm/{cloneRouter-DM59kihh.mjs.map → cloneRouter-BYNiwchg.mjs.map} +1 -1
- package/dist/esm/{index-DNjaY7KH.d.mts → index-C-i6vx5Y.d.mts} +1 -1
- package/dist/esm/{index-DNjaY7KH.d.mts.map → index-C-i6vx5Y.d.mts.map} +1 -1
- package/dist/esm/{index-r_JTvSBH.d.mts → index-DKzxav48.d.mts} +2 -2
- package/dist/esm/{index-r_JTvSBH.d.mts.map → index-DKzxav48.d.mts.map} +1 -1
- package/dist/esm/index.d.mts +3 -3
- package/dist/esm/index.mjs +1 -1
- package/dist/esm/{internals-C59msvHY.mjs → internals-DT4mneSz.mjs} +1 -1
- package/dist/esm/{internals-C59msvHY.mjs.map → internals-DT4mneSz.mjs.map} +1 -1
- package/dist/esm/utils.d.mts +1 -1
- package/dist/esm/utils.mjs +1 -1
- package/dist/esm/validation.d.mts +4 -4
- package/dist/esm/validation.mjs +1 -1
- package/package.json +3 -3
- package/src/api/getRoutesApi.ts +47 -88
- package/src/namespaces/EventBusNamespace/EventBusNamespace.ts +68 -3
- package/src/namespaces/NavigationNamespace/types.ts +1 -1
- package/src/namespaces/RoutesNamespace/routesStore.ts +272 -52
- package/dist/cjs/Router-BzV5P1lA.js +0 -6
- package/dist/cjs/Router-BzV5P1lA.js.map +0 -1
- package/dist/esm/Router-_BDnheMY.mjs +0 -6
- package/dist/esm/Router-_BDnheMY.mjs.map +0 -1
- package/dist/esm/cloneRouter-DM59kihh.mjs +0 -2
package/src/api/getRoutesApi.ts
CHANGED
|
@@ -6,16 +6,17 @@ import { createInterceptable, getInternals } from "../internals";
|
|
|
6
6
|
import {
|
|
7
7
|
clearConfigEntries,
|
|
8
8
|
removeFromDefinitions,
|
|
9
|
-
sanitizeRoute,
|
|
10
9
|
} from "../namespaces/RoutesNamespace/helpers";
|
|
11
10
|
import {
|
|
12
11
|
validateClearRoutes,
|
|
13
12
|
validateRemoveRoute,
|
|
14
13
|
} from "../namespaces/RoutesNamespace/routeGuards";
|
|
15
14
|
import {
|
|
16
|
-
|
|
15
|
+
adoptRouteArtifacts,
|
|
16
|
+
assertAddable,
|
|
17
|
+
buildAddArtifacts,
|
|
18
|
+
buildReplaceArtifacts,
|
|
17
19
|
refreshForwardMap,
|
|
18
|
-
registerAllRouteHandlers,
|
|
19
20
|
} from "../namespaces/RoutesNamespace/routesStore";
|
|
20
21
|
|
|
21
22
|
import type { RoutesApi } from "./types";
|
|
@@ -36,32 +37,6 @@ import type { RouteDefinition, RouteTree } from "route-tree";
|
|
|
36
37
|
// Helpers
|
|
37
38
|
// ============================================================================
|
|
38
39
|
|
|
39
|
-
/**
|
|
40
|
-
* Recursively finds a route definition by its full dotted name.
|
|
41
|
-
*/
|
|
42
|
-
function findDefinition(
|
|
43
|
-
definitions: RouteDefinition[],
|
|
44
|
-
fullName: string,
|
|
45
|
-
parentPrefix = "",
|
|
46
|
-
): RouteDefinition | undefined {
|
|
47
|
-
for (const def of definitions) {
|
|
48
|
-
const currentFullName = parentPrefix
|
|
49
|
-
? `${parentPrefix}.${def.name}`
|
|
50
|
-
: def.name;
|
|
51
|
-
|
|
52
|
-
if (currentFullName === fullName) {
|
|
53
|
-
return def;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (def.children && fullName.startsWith(`${currentFullName}.`)) {
|
|
57
|
-
return findDefinition(def.children, fullName, currentFullName);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/* v8 ignore next -- @preserve: defensive return, callers validate route exists before calling */
|
|
62
|
-
return undefined;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
40
|
/**
|
|
66
41
|
* Clears all config entries and lifecycle handlers for a removed route
|
|
67
42
|
* (and all its descendants).
|
|
@@ -116,20 +91,36 @@ function updateForwardTo<
|
|
|
116
91
|
name: string,
|
|
117
92
|
forwardTo: string | ForwardToCallback<Dependencies> | null,
|
|
118
93
|
config: RouteConfig,
|
|
119
|
-
refreshForwardMapFn: (config: RouteConfig) => Record<string, string>,
|
|
120
94
|
): Record<string, string> {
|
|
95
|
+
// Prepare-then-commit (issue #698): apply the change to CLONES of the forward
|
|
96
|
+
// maps, resolve the chain (a cycle throws here), and only then swap the clones
|
|
97
|
+
// in — so a rejected update never leaves config.forwardMap poisoned.
|
|
98
|
+
const forwardMap = Object.assign(
|
|
99
|
+
Object.create(null) as RouteConfig["forwardMap"],
|
|
100
|
+
config.forwardMap,
|
|
101
|
+
);
|
|
102
|
+
const forwardFnMap = Object.assign(
|
|
103
|
+
Object.create(null) as RouteConfig["forwardFnMap"],
|
|
104
|
+
config.forwardFnMap,
|
|
105
|
+
);
|
|
106
|
+
|
|
121
107
|
if (forwardTo === null) {
|
|
122
|
-
delete
|
|
123
|
-
delete
|
|
108
|
+
delete forwardMap[name];
|
|
109
|
+
delete forwardFnMap[name];
|
|
124
110
|
} else if (typeof forwardTo === "string") {
|
|
125
|
-
delete
|
|
126
|
-
|
|
111
|
+
delete forwardFnMap[name];
|
|
112
|
+
forwardMap[name] = forwardTo;
|
|
127
113
|
} else {
|
|
128
|
-
delete
|
|
129
|
-
|
|
114
|
+
delete forwardMap[name];
|
|
115
|
+
forwardFnMap[name] = forwardTo;
|
|
130
116
|
}
|
|
131
117
|
|
|
132
|
-
|
|
118
|
+
const resolved = refreshForwardMap({ ...config, forwardMap });
|
|
119
|
+
|
|
120
|
+
config.forwardMap = forwardMap;
|
|
121
|
+
config.forwardFnMap = forwardFnMap;
|
|
122
|
+
|
|
123
|
+
return resolved;
|
|
133
124
|
}
|
|
134
125
|
|
|
135
126
|
/**
|
|
@@ -212,37 +203,19 @@ function addRoutes<
|
|
|
212
203
|
routes: Route<Dependencies>[],
|
|
213
204
|
parentName?: string,
|
|
214
205
|
): void {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
for (const route of routes) {
|
|
222
|
-
parentDef.children.push(sanitizeRoute(route));
|
|
223
|
-
}
|
|
224
|
-
} else {
|
|
225
|
-
for (const route of routes) {
|
|
226
|
-
store.definitions.push(sanitizeRoute(route));
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
registerAllRouteHandlers(
|
|
231
|
-
routes,
|
|
232
|
-
store.config,
|
|
233
|
-
store.routeCustomFields,
|
|
234
|
-
store.pendingCanActivate,
|
|
235
|
-
store.pendingCanDeactivate,
|
|
236
|
-
store.depsStore,
|
|
237
|
-
parentName ?? "",
|
|
238
|
-
);
|
|
239
|
-
|
|
240
|
-
store.treeOperations.commitTreeChanges(store);
|
|
206
|
+
// Prepare-then-commit (issue #698): reject the silent-corruption cases
|
|
207
|
+
// up front (dup name vs existing, missing parent), build the merged tree /
|
|
208
|
+
// config into locals (async/circular forwardTo + invalid constraint throw
|
|
209
|
+
// here), then swap atomically. A rejected add leaves the store untouched.
|
|
210
|
+
assertAddable(store, routes, parentName);
|
|
211
|
+
adoptRouteArtifacts(store, buildAddArtifacts(store, routes, parentName));
|
|
241
212
|
}
|
|
242
213
|
|
|
243
214
|
/**
|
|
244
|
-
* Atomically replaces all routes with a new set.
|
|
245
|
-
*
|
|
215
|
+
* Atomically replaces all routes with a new set (HMR / code-splitting).
|
|
216
|
+
* Prepare-then-commit (issue #698): the new set is fully built into locals
|
|
217
|
+
* first — a circular/async forwardTo or invalid path throws here, leaving the
|
|
218
|
+
* existing tree intact — then committed.
|
|
246
219
|
*/
|
|
247
220
|
function replaceRoutes<
|
|
248
221
|
Dependencies extends DefaultDependencies = DefaultDependencies,
|
|
@@ -253,32 +226,19 @@ function replaceRoutes<
|
|
|
253
226
|
currentPath: string | undefined,
|
|
254
227
|
previousTransition: TransitionMeta | undefined,
|
|
255
228
|
): void {
|
|
256
|
-
//
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
// Step 3: Clear definition lifecycle handlers (preserve external guards)
|
|
260
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- guaranteed set after wiring
|
|
261
|
-
store.lifecycleNamespace!.clearDefinitionGuards();
|
|
262
|
-
|
|
263
|
-
// Step 4: Register new routes
|
|
264
|
-
for (const route of routes) {
|
|
265
|
-
store.definitions.push(sanitizeRoute(route));
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
registerAllRouteHandlers(
|
|
229
|
+
// Build the whole new set BEFORE touching the store.
|
|
230
|
+
const artifacts = buildReplaceArtifacts(
|
|
269
231
|
routes,
|
|
270
|
-
store.
|
|
271
|
-
store.
|
|
272
|
-
store.pendingCanActivate,
|
|
273
|
-
store.pendingCanDeactivate,
|
|
274
|
-
store.depsStore,
|
|
275
|
-
"",
|
|
232
|
+
store.rootPath,
|
|
233
|
+
store.matcherOptions,
|
|
276
234
|
);
|
|
277
235
|
|
|
278
|
-
//
|
|
279
|
-
|
|
236
|
+
// Clear definition lifecycle handlers (preserve external guards), then swap.
|
|
237
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- guaranteed set after wiring
|
|
238
|
+
store.lifecycleNamespace!.clearDefinitionGuards();
|
|
239
|
+
adoptRouteArtifacts(store, artifacts);
|
|
280
240
|
|
|
281
|
-
//
|
|
241
|
+
// Revalidate state (preserve transition from previous state)
|
|
282
242
|
if (currentPath !== undefined) {
|
|
283
243
|
const revalidated = ctx.matchPath(currentPath, ctx.getOptions());
|
|
284
244
|
|
|
@@ -341,7 +301,6 @@ function updateRouteConfig<
|
|
|
341
301
|
name,
|
|
342
302
|
updates.forwardTo,
|
|
343
303
|
store.config,
|
|
344
|
-
(config) => refreshForwardMap(config),
|
|
345
304
|
);
|
|
346
305
|
}
|
|
347
306
|
|
|
@@ -264,6 +264,19 @@ export class EventBusNamespace {
|
|
|
264
264
|
return this.#currentToState;
|
|
265
265
|
}
|
|
266
266
|
|
|
267
|
+
/**
|
|
268
|
+
* Plugin-author API for subscribing to internal router events.
|
|
269
|
+
*
|
|
270
|
+
* @remarks
|
|
271
|
+
*
|
|
272
|
+
* **Duplicate-registration semantics — strict (throws).** Passing the same
|
|
273
|
+
* callback reference twice for the same event throws
|
|
274
|
+
* `Error("Duplicate listener for ...")` from the underlying `EventEmitter`.
|
|
275
|
+
* This is loud-on-misuse by design: plugin code is expected to register
|
|
276
|
+
* each callback once. The contract differs from {@link subscribe} /
|
|
277
|
+
* {@link subscribeLeave}, which are end-user surfaces and silently accept
|
|
278
|
+
* duplicates.
|
|
279
|
+
*/
|
|
267
280
|
addEventListener<E extends EventName>(
|
|
268
281
|
eventName: E,
|
|
269
282
|
cb: Plugin[EventMethodMap[E]],
|
|
@@ -274,6 +287,22 @@ export class EventBusNamespace {
|
|
|
274
287
|
);
|
|
275
288
|
}
|
|
276
289
|
|
|
290
|
+
/**
|
|
291
|
+
* End-user / UI-binding API for subscribing to successful transitions.
|
|
292
|
+
*
|
|
293
|
+
* @remarks
|
|
294
|
+
*
|
|
295
|
+
* **Duplicate-registration semantics — independent.** Each call wraps
|
|
296
|
+
* `listener` in a fresh closure and registers it as a distinct internal
|
|
297
|
+
* slot. `router.subscribe(fn)` twice produces **two** active subscriptions;
|
|
298
|
+
* `fn` fires twice per `TRANSITION_SUCCESS`. The returned `Unsubscribe` is
|
|
299
|
+
* paired with its specific call — invoking it removes exactly that
|
|
300
|
+
* registration.
|
|
301
|
+
*
|
|
302
|
+
* This contract differs from {@link addEventListener} (plugin API, throws
|
|
303
|
+
* on duplicate). End-user code that wants idempotent registration must
|
|
304
|
+
* gate itself, e.g. `if (!unsub) unsub = router.subscribe(fn);`.
|
|
305
|
+
*/
|
|
277
306
|
subscribe(listener: SubscribeFn): Unsubscribe {
|
|
278
307
|
return this.#emitter.on(
|
|
279
308
|
events.TRANSITION_SUCCESS,
|
|
@@ -283,6 +312,31 @@ export class EventBusNamespace {
|
|
|
283
312
|
);
|
|
284
313
|
}
|
|
285
314
|
|
|
315
|
+
/**
|
|
316
|
+
* End-user / UI-binding API for subscribing to confirmed route departures
|
|
317
|
+
* (`LEAVE_APPROVED` phase). Async listeners block the activation phase.
|
|
318
|
+
*
|
|
319
|
+
* @remarks
|
|
320
|
+
*
|
|
321
|
+
* **Duplicate-registration semantics — independent (with internal quirk).**
|
|
322
|
+
* Each call pushes `listener` onto the internal array; `router.subscribeLeave(fn)`
|
|
323
|
+
* twice produces two array entries. `fn` fires **once** per leave when
|
|
324
|
+
* iteration snapshots the array (a snapshot is taken on entry to
|
|
325
|
+
* `awaitLeaveListeners`), but the function reference is invoked once per
|
|
326
|
+
* array slot — so in practice the wrapper fires twice through the same
|
|
327
|
+
* closure (no observable difference for stateless `fn`).
|
|
328
|
+
*
|
|
329
|
+
* The returned `Unsubscribe` removes the **first** array entry matching the
|
|
330
|
+
* function reference (`indexOf` semantic), not the most recently added one.
|
|
331
|
+
* Net effect of N subscribes + M unsubscribes is correct (N - M entries
|
|
332
|
+
* remain), but the specific physical entry that survives is reverse of the
|
|
333
|
+
* unsubscribe-call order. Irrelevant in practice — the function reference
|
|
334
|
+
* is the same; observable behaviour is identical regardless of which
|
|
335
|
+
* physical entry is removed.
|
|
336
|
+
*
|
|
337
|
+
* Contract differs from {@link addEventListener} (throws on duplicate).
|
|
338
|
+
* For idempotent registration, gate at the call site.
|
|
339
|
+
*/
|
|
286
340
|
subscribeLeave(listener: LeaveFn): Unsubscribe {
|
|
287
341
|
this.#leaveListeners.push(listener);
|
|
288
342
|
|
|
@@ -308,16 +362,27 @@ export class EventBusNamespace {
|
|
|
308
362
|
return undefined;
|
|
309
363
|
}
|
|
310
364
|
|
|
311
|
-
|
|
365
|
+
// Freeze the payload wrapper so listeners cannot mutate it (`payload.route`
|
|
366
|
+
// is already deep-frozen via the State immutability invariant; this closes
|
|
367
|
+
// the wrapper-mutation gap surfaced by audit `probe-05-payload-frozen`).
|
|
368
|
+
const leaveState: LeaveState = Object.freeze({
|
|
312
369
|
route: fromState,
|
|
313
370
|
nextRoute: toState,
|
|
314
371
|
signal,
|
|
315
|
-
};
|
|
372
|
+
});
|
|
316
373
|
|
|
317
374
|
let promises: Promise<void>[] | undefined;
|
|
318
375
|
let firstSyncError: unknown;
|
|
319
376
|
|
|
320
|
-
|
|
377
|
+
// Snapshot before iteration — a listener that reentrantly calls
|
|
378
|
+
// `subscribeLeave(newFn)` or its own `unsubscribe()` must not affect the
|
|
379
|
+
// current emit cycle. Symmetric with the EventEmitter snapshot invariant
|
|
380
|
+
// (PR #666 / #659). Use `Array.from` rather than `[...array]` to keep the
|
|
381
|
+
// intent explicit (some lint rules treat spread-of-own-array as
|
|
382
|
+
// redundant and silently revert it).
|
|
383
|
+
const snapshot = [...this.#leaveListeners];
|
|
384
|
+
|
|
385
|
+
for (const listener of snapshot) {
|
|
321
386
|
try {
|
|
322
387
|
const result = listener(leaveState);
|
|
323
388
|
|
|
@@ -23,7 +23,7 @@ export interface NavigationContext {
|
|
|
23
23
|
*
|
|
24
24
|
* These are function references from other namespaces/facade,
|
|
25
25
|
* avoiding the need to pass the entire Router object.
|
|
26
|
-
|
|
26
|
+
*/
|
|
27
27
|
export interface NavigationDependencies {
|
|
28
28
|
/** Get router options */
|
|
29
29
|
getOptions: () => Options;
|