@real-router/navigation-plugin 0.7.0 → 0.7.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/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/index.d.mts +1 -1
- package/dist/esm/index.mjs +1 -1
- package/dist/esm/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/history-extensions.ts +11 -6
- package/src/navigate-handler.ts +69 -58
- package/src/navigation-browser.ts +25 -17
- package/src/plugin.ts +87 -29
- package/src/types.ts +1 -1
package/src/plugin.ts
CHANGED
|
@@ -4,11 +4,14 @@ import {
|
|
|
4
4
|
shouldReplaceHistory,
|
|
5
5
|
buildUrl,
|
|
6
6
|
urlToPath,
|
|
7
|
+
createPluginBuildUrl,
|
|
7
8
|
createStartInterceptor,
|
|
8
9
|
createReplaceHistoryState,
|
|
9
10
|
encodeHashFragment,
|
|
10
11
|
getDecodedHash,
|
|
11
12
|
normalizeHashInput,
|
|
13
|
+
safeParseUrl,
|
|
14
|
+
decodeHashFragment,
|
|
12
15
|
} from "./browser-env";
|
|
13
16
|
import {
|
|
14
17
|
peekBack,
|
|
@@ -35,7 +38,6 @@ import type {
|
|
|
35
38
|
} from "./types";
|
|
36
39
|
import type {
|
|
37
40
|
NavigationOptions,
|
|
38
|
-
Params,
|
|
39
41
|
Router,
|
|
40
42
|
State,
|
|
41
43
|
Plugin,
|
|
@@ -78,6 +80,21 @@ export class NavigationPlugin {
|
|
|
78
80
|
|
|
79
81
|
#capturedMeta: NavigationMeta | undefined;
|
|
80
82
|
#pendingTraverseKey: string | undefined;
|
|
83
|
+
// Always set together with #pendingTraverseKey; `""` means "destination has
|
|
84
|
+
// no fragment". Typed as `string` (not `string | undefined`) so the traverse
|
|
85
|
+
// branch reads it without a redundant `?? ""` fallback that coverage cannot
|
|
86
|
+
// exercise.
|
|
87
|
+
#pendingTraverseHash = "";
|
|
88
|
+
// Reusable buffer for the {name, params, path} payload passed to
|
|
89
|
+
// browser.navigate / browser.updateCurrentEntry. The Navigation API
|
|
90
|
+
// structured-clones state synchronously inside the call, so this object
|
|
91
|
+
// never escapes — same trick createReplaceHistoryState uses.
|
|
92
|
+
readonly #historyStateBuffer: { name: string; params: object; path: string } =
|
|
93
|
+
{
|
|
94
|
+
name: "",
|
|
95
|
+
params: {},
|
|
96
|
+
path: "",
|
|
97
|
+
};
|
|
81
98
|
|
|
82
99
|
constructor(
|
|
83
100
|
router: Router,
|
|
@@ -133,22 +150,7 @@ export class NavigationPlugin {
|
|
|
133
150
|
// bar entry: by the time onTransitionSuccess fires the browser already
|
|
134
151
|
// reflects the destination URL.
|
|
135
152
|
|
|
136
|
-
const pluginBuildUrl = (
|
|
137
|
-
route: string,
|
|
138
|
-
params?: Params,
|
|
139
|
-
opts?: { hash?: string },
|
|
140
|
-
) => {
|
|
141
|
-
const path = router.buildPath(route, params);
|
|
142
|
-
const url = buildUrl(path, options.base);
|
|
143
|
-
|
|
144
|
-
if (opts?.hash === undefined) {
|
|
145
|
-
return url;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const norm = normalizeHashInput(opts.hash);
|
|
149
|
-
|
|
150
|
-
return norm ? `${url}#${encodeHashFragment(norm)}` : url;
|
|
151
|
-
};
|
|
153
|
+
const pluginBuildUrl = createPluginBuildUrl(router, options.base);
|
|
152
154
|
|
|
153
155
|
this.#removeExtensions = api.extendRouter({
|
|
154
156
|
buildUrl: pluginBuildUrl,
|
|
@@ -216,7 +218,7 @@ export class NavigationPlugin {
|
|
|
216
218
|
// unmatched url — same three error branches the old inline checks
|
|
217
219
|
// produced. Extracted so the error paths can be unit-tested directly
|
|
218
220
|
// without namespace-level vi.spyOn gymnastics.
|
|
219
|
-
const { entry, matchedState } = resolveEntryToMatchedState(
|
|
221
|
+
const { entry, entryUrl, matchedState } = resolveEntryToMatchedState(
|
|
220
222
|
candidate,
|
|
221
223
|
routeName,
|
|
222
224
|
this.#api,
|
|
@@ -243,6 +245,10 @@ export class NavigationPlugin {
|
|
|
243
245
|
sourceElement: null,
|
|
244
246
|
};
|
|
245
247
|
this.#pendingTraverseKey = entry.key;
|
|
248
|
+
// Capture the destination entry's hash so onTransitionSuccess can populate
|
|
249
|
+
// state.context.url for the traverse branch — mirrors what navigate-handler
|
|
250
|
+
// does via navOptions.hash for browser-initiated navigation.
|
|
251
|
+
this.#pendingTraverseHash = extractHashFromEntryUrl(entryUrl);
|
|
246
252
|
|
|
247
253
|
return this.#router.navigate(matchedState.name, matchedState.params);
|
|
248
254
|
}
|
|
@@ -290,10 +296,26 @@ export class NavigationPlugin {
|
|
|
290
296
|
// The syncing flag is raised/lowered inside NavigationBrowser around
|
|
291
297
|
// each mutation, so we do not need to manage it here.
|
|
292
298
|
const traverseKey = this.#pendingTraverseKey;
|
|
299
|
+
const traverseHash = this.#pendingTraverseHash;
|
|
293
300
|
|
|
294
301
|
this.#pendingTraverseKey = undefined;
|
|
302
|
+
this.#pendingTraverseHash = "";
|
|
303
|
+
|
|
304
|
+
const publishedPrevHash = readPublishedHash(fromState);
|
|
295
305
|
|
|
296
306
|
if (traverseKey) {
|
|
307
|
+
// Mirror the urlClaim.write the `else` branch does for non-traverse
|
|
308
|
+
// navigations — without this, `router.traverseToLast(name)` leaves
|
|
309
|
+
// state.context.url undefined for subscribers (#urlClaim was set in
|
|
310
|
+
// navigate-handler for browser-driven traverse, but programmatic
|
|
311
|
+
// traverseToLast bypasses that path).
|
|
312
|
+
this.#urlClaim.write(
|
|
313
|
+
toState,
|
|
314
|
+
Object.freeze({
|
|
315
|
+
hash: traverseHash,
|
|
316
|
+
hashChanged: traverseHash !== publishedPrevHash,
|
|
317
|
+
}),
|
|
318
|
+
);
|
|
297
319
|
this.#browser.traverseTo(traverseKey);
|
|
298
320
|
} else {
|
|
299
321
|
// Tri-state hash resolution (#532).
|
|
@@ -312,9 +334,6 @@ export class NavigationPlugin {
|
|
|
312
334
|
// a true signal regardless of whether the value came from
|
|
313
335
|
// navOptions or the browser.
|
|
314
336
|
const browserHash = getDecodedHash(this.#browser);
|
|
315
|
-
const publishedPrevHash =
|
|
316
|
-
(fromState?.context as { url?: { hash?: string } } | undefined)?.url
|
|
317
|
-
?.hash ?? "";
|
|
318
337
|
|
|
319
338
|
const hash =
|
|
320
339
|
navOptions.hash === undefined
|
|
@@ -331,19 +350,29 @@ export class NavigationPlugin {
|
|
|
331
350
|
|
|
332
351
|
const url = buildUrl(toState.path, this.#options.base);
|
|
333
352
|
const finalUrl = hash ? `${url}#${encodeHashFragment(hash)}` : url;
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
};
|
|
353
|
+
|
|
354
|
+
this.#historyStateBuffer.name = toState.name;
|
|
355
|
+
this.#historyStateBuffer.params = toState.params;
|
|
356
|
+
this.#historyStateBuffer.path = toState.path;
|
|
339
357
|
|
|
340
358
|
if (toState.name === UNKNOWN_ROUTE) {
|
|
341
|
-
this.#browser.updateCurrentEntry({
|
|
359
|
+
this.#browser.updateCurrentEntry({
|
|
360
|
+
state: this.#historyStateBuffer,
|
|
361
|
+
});
|
|
342
362
|
} else {
|
|
343
|
-
|
|
363
|
+
// Initial transition (no fromState) means router.start() is
|
|
364
|
+
// resolving the cross-document load — the browser already created
|
|
365
|
+
// a history entry for it. A `push` here would duplicate that
|
|
366
|
+
// entry. Always `replace` on the first transition so the
|
|
367
|
+
// back/forward stack has only one entry (canGoBack === false).
|
|
368
|
+
// navigationType metadata stays "push"/"reload"/"replace" for
|
|
369
|
+
// downstream consumers (scroll restore, direction tracker).
|
|
370
|
+
const isInitialTransition = fromState === undefined;
|
|
371
|
+
const replace =
|
|
372
|
+
frozenMeta.navigationType !== "push" || isInitialTransition;
|
|
344
373
|
|
|
345
374
|
this.#browser.navigate(finalUrl, {
|
|
346
|
-
state:
|
|
375
|
+
state: this.#historyStateBuffer,
|
|
347
376
|
history: replace ? "replace" : "push",
|
|
348
377
|
});
|
|
349
378
|
}
|
|
@@ -353,11 +382,13 @@ export class NavigationPlugin {
|
|
|
353
382
|
onTransitionCancel: () => {
|
|
354
383
|
this.#capturedMeta = undefined;
|
|
355
384
|
this.#pendingTraverseKey = undefined;
|
|
385
|
+
this.#pendingTraverseHash = "";
|
|
356
386
|
},
|
|
357
387
|
|
|
358
388
|
onTransitionError: () => {
|
|
359
389
|
this.#capturedMeta = undefined;
|
|
360
390
|
this.#pendingTraverseKey = undefined;
|
|
391
|
+
this.#pendingTraverseHash = "";
|
|
361
392
|
},
|
|
362
393
|
};
|
|
363
394
|
}
|
|
@@ -372,6 +403,33 @@ interface NavigateLifecycleDeps {
|
|
|
372
403
|
shared: NavigationSharedState;
|
|
373
404
|
}
|
|
374
405
|
|
|
406
|
+
/**
|
|
407
|
+
* Reads the previously published hash from `fromState.context.url`.
|
|
408
|
+
* Returns `""` for the initial transition (no `fromState`), for states whose
|
|
409
|
+
* `context.url` namespace was not claimed yet, or for the documented `{ hash:
|
|
410
|
+
* "" }` cleared form. Extracted from `onTransitionSuccess` to share between
|
|
411
|
+
* the traverse and non-traverse branches.
|
|
412
|
+
*/
|
|
413
|
+
function readPublishedHash(fromState: State | undefined): string {
|
|
414
|
+
return (
|
|
415
|
+
(fromState?.context as { url?: { hash?: string } } | undefined)?.url
|
|
416
|
+
?.hash ?? ""
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Decodes the URL fragment from a NavigationHistoryEntry's url string.
|
|
422
|
+
* Returns `""` when no fragment is present. The caller (NavigationPlugin's
|
|
423
|
+
* `traverseToLast`) only reaches here AFTER `resolveEntryToMatchedState`,
|
|
424
|
+
* which has already rejected `entry.url === null`, so the input is guaranteed
|
|
425
|
+
* non-null at runtime.
|
|
426
|
+
*/
|
|
427
|
+
function extractHashFromEntryUrl(entryUrl: string): string {
|
|
428
|
+
const rawHash = safeParseUrl(entryUrl).hash;
|
|
429
|
+
|
|
430
|
+
return rawHash ? decodeHashFragment(rawHash.slice(1)) : "";
|
|
431
|
+
}
|
|
432
|
+
|
|
375
433
|
function createNavigateLifecycle(deps: NavigateLifecycleDeps): Plugin {
|
|
376
434
|
return {
|
|
377
435
|
onStart() {
|