Repository: sveltejs/kit (js)

Issue:
More information in `Navigation`
### Describe the problem

I've occasionally wondered if the `navigating` store, and the `from` and `to` objects passed to `beforeNavigate` and `afterNavigate`, need more information than just the URL.

#6331 provides a good example of a use case. Inside `beforeNavigate` we need to know the before/after `routeId`.

### Describe the proposed solution

Changing `Navigation` from this...

```ts
export interface Navigation {
  from: URL | null;
  to: URL | null;
  type: NavigationType;
  delta?: number;
}
```

...to this:

```ts
export interface NavigationTarget {
  params: Params;
  routeId: string;
  url: URL;
}

export interface Navigation {
  from: NavigationTarget | null;
  to: NavigationTarget | null;
  type: NavigationType;
  delta?: number;
}
```

### Alternatives considered

`fromRouteId`, `toRouteId`, `fromParams`, `toParams`, etc. Ugh.

### Importance

nice to have

### Additional Information

_No response_

Pull request:
augment `Navigation` type
closes #6551 — gives `beforeNavigate` and `afterNavigate` more information about which route they are navigating from and to

### Please don't delete this checklist! Before submitting the PR, please make sure you do the following:
- [x] It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs
- [x] This message body should clearly illustrate what problems it solves.
- [x] Ideally, include a test that fails without this PR but passes with it.

### Tests
- [x] Run the tests with `pnpm test` and lint the project with `pnpm lint` and `pnpm check`

### Changesets
- [x] If your PR makes a change that should be noted in one or more packages' changelogs, generate a changeset by running `pnpm changeset` and following the prompts. All changesets should be `patch` until SvelteKit 1.0


Patch:
diff --git a/.changeset/breezy-ladybugs-dance.md b/.changeset/breezy-ladybugs-dance.md
new file mode 100644
index 000000000..c15821bb5
--- /dev/null
+++ b/.changeset/breezy-ladybugs-dance.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+[breaking] change Navigation type to include from/to.params and from/to.routeId
diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js
index 1e61ce2cc..ab452eb4b 100644
--- a/packages/kit/src/runtime/client/client.js
+++ b/packages/kit/src/runtime/client/client.js
@@ -145,7 +145,8 @@ export function create_client({ target, base, trailing_slash }) {
 			const url = new URL(location.href);
 
 			invalidating = Promise.resolve().then(async () => {
-				await update(url, []);
+				const intent = get_navigation_intent(url);
+				await update(intent, url, []);
 
 				invalidating = null;
 				force_invalidation = false;
@@ -200,14 +201,13 @@ export function create_client({ target, base, trailing_slash }) {
 
 	/**
 	 * Returns `true` if update completes, `false` if it is aborted
+	 * @param {import('./types').NavigationIntent | undefined} intent
 	 * @param {URL} url
 	 * @param {string[]} redirect_chain
 	 * @param {{hash?: string, scroll: { x: number, y: number } | null, keepfocus: boolean, details: { replaceState: boolean, state: any } | null}} [opts]
 	 * @param {() => void} [callback]
 	 */
-	async function update(url, redirect_chain, opts, callback) {
-		const intent = get_navigation_intent(url);
-
+	async function update(intent, url, redirect_chain, opts, callback) {
 		const current_token = (token = {});
 		let navigation_result = intent && (await load_route(intent));
 
@@ -399,7 +399,15 @@ export function create_client({ target, base, trailing_slash }) {
 		}
 
 		/** @type {import('types').Navigation} */
-		const navigation = { from: null, to: new URL(location.href), type: 'load' };
+		const navigation = {
+			from: null,
+			to: add_url_properties('to', {
+				params: current.params,
+				routeId: current.route?.id ?? null,
+				url: new URL(location.href)
+			}),
+			type: 'load'
+		};
 		callbacks.after_navigate.forEach((fn) => fn(navigation));
 
 		started = true;
@@ -413,7 +421,7 @@ export function create_client({ target, base, trailing_slash }) {
 	 *   branch: Array<import('./types').BranchNode | undefined>;
 	 *   status: number;
 	 *   error: HttpError | Error | null;
-	 *   routeId: string | null;
+	 *   route: import('types').CSRRoute | null;
 	 *   validation_errors?: Record<string, any> | null;
 	 * }} opts
 	 */
@@ -423,7 +431,7 @@ export function create_client({ target, base, trailing_slash }) {
 		branch,
 		status,
 		error,
-		routeId,
+		route,
 		validation_errors
 	}) {
 		const filtered = /** @type {import('./types').BranchNode[] } */ (branch.filter(Boolean));
@@ -436,6 +444,7 @@ export function create_client({ target, base, trailing_slash }) {
 				params,
 				branch,
 				error,
+				route,
 				session_id
 			},
 			props: {
@@ -470,7 +479,7 @@ export function create_client({ target, base, trailing_slash }) {
 			result.props.page = {
 				error,
 				params,
-				routeId,
+				routeId: route && route.id,
 				status,
 				url,
 				// The whole page store is updated, but this way the object reference stays the same
@@ -846,7 +855,7 @@ export function create_client({ target, base, trailing_slash }) {
 									branch: branch.slice(0, j + 1).concat(error_loaded),
 									status,
 									error,
-									routeId: route.id
+									route
 								});
 							} catch (e) {
 								continue;
@@ -872,7 +881,7 @@ export function create_client({ target, base, trailing_slash }) {
 			branch,
 			status: 200,
 			error: null,
-			routeId: route.id
+			route
 		});
 	}
 
@@ -941,7 +950,7 @@ export function create_client({ target, base, trailing_slash }) {
 			branch: [root_layout, root_error],
 			status,
 			error,
-			routeId
+			route: null
 		});
 	}
 
@@ -1000,10 +1009,20 @@ export function create_client({ target, base, trailing_slash }) {
 	}) {
 		let should_block = false;
 
+		const intent = get_navigation_intent(url);
+
 		/** @type {import('types').Navigation} */
 		const navigation = {
-			from: current.url,
-			to: url,
+			from: add_url_properties('from', {
+				params: current.params,
+				routeId: current.route?.id ?? null,
+				url: current.url
+			}),
+			to: add_url_properties('to', {
+				params: intent?.params ?? null,
+				routeId: intent?.route.id ?? null,
+				url
+			}),
 			type
 		};
 
@@ -1034,6 +1053,7 @@ export function create_client({ target, base, trailing_slash }) {
 		}
 
 		await update(
+			intent,
 			url,
 			redirect_chain,
 			{
@@ -1153,7 +1173,11 @@ export function create_client({ target, base, trailing_slash }) {
 
 				/** @type {import('types').Navigation & { cancel: () => void }} */
 				const navigation = {
-					from: current.url,
+					from: add_url_properties('from', {
+						params: current.params,
+						routeId: current.route?.id ?? null,
+						url: current.url
+					}),
 					to: null,
 					type: 'unload',
 					cancel: () => (should_block = true)
@@ -1379,7 +1403,7 @@ export function create_client({ target, base, trailing_slash }) {
 						  )
 						: original_error,
 					validation_errors,
-					routeId
+					route: routes.find((route) => route.id === routeId) ?? null
 				});
 			} catch (e) {
 				const error = normalize_error(e);
@@ -1434,3 +1458,37 @@ async function load_data(url, invalid) {
 
 	return server_data;
 }
+
+// TODO remove for 1.0
+const properties = [
+	'hash',
+	'href',
+	'host',
+	'hostname',
+	'origin',
+	'pathname',
+	'port',
+	'protocol',
+	'search',
+	'searchParams',
+	'toString',
+	'toJSON'
+];
+
+/**
+ * @param {'from' | 'to'} type
+ * @param {import('types').NavigationTarget} target
+ */
+function add_url_properties(type, target) {
+	for (const prop of properties) {
+		Object.defineProperty(target, prop, {
+			get() {
+				throw new Error(
+					`The navigation shape changed - ${type}.${prop} should now be ${type}.url.${prop}`
+				);
+			}
+		});
+	}
+
+	return target;
+}
diff --git a/packages/kit/src/runtime/client/types.d.ts b/packages/kit/src/runtime/client/types.d.ts
index f2f5a3617..ad59df44a 100644
--- a/packages/kit/src/runtime/client/types.d.ts
+++ b/packages/kit/src/runtime/client/types.d.ts
@@ -81,10 +81,11 @@ export interface DataNode {
 	uses: Uses;
 }
 
-export type NavigationState = {
+export interface NavigationState {
 	branch: Array<BranchNode | undefined>;
 	error: HttpError | Error | null;
 	params: Record<string, string>;
+	route: CSRRoute | null;
 	session_id: number;
 	url: URL;
-};
+}
diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts
index 7eb746c2d..c09238532 100644
--- a/packages/kit/types/index.d.ts
+++ b/packages/kit/types/index.d.ts
@@ -219,11 +219,17 @@ export interface LoadEvent<
 	depends: (...deps: string[]) => void;
 }
 
+export interface NavigationTarget {
+	params: Record<string, string> | null;
+	routeId: string | null;
+	url: URL;
+}
+
 export type NavigationType = 'load' | 'unload' | 'link' | 'goto' | 'popstate';
 
 export interface Navigation {
-	from: URL | null;
-	to: URL | null;
+	from: NavigationTarget | null;
+	to: NavigationTarget | null;
 	type: NavigationType;
 	delta?: number;
 }


Test patch:
diff --git a/packages/kit/test/apps/basics/src/routes/after-navigate/a/+page.svelte b/packages/kit/test/apps/basics/src/routes/after-navigate/a/+page.svelte
index 36560e6ef..70209b704 100644
--- a/packages/kit/test/apps/basics/src/routes/after-navigate/a/+page.svelte
+++ b/packages/kit/test/apps/basics/src/routes/after-navigate/a/+page.svelte
@@ -1,10 +1,10 @@
 <script>
 	import { afterNavigate } from '$app/navigation';
 
-	/** @type {URL | null} */
+	/** @type {import('@sveltejs/kit').NavigationTarget | null} */
 	let from;
 
-	/** @type {URL} */
+	/** @type {import('@sveltejs/kit').NavigationTarget} */
 	let to;
 
 	afterNavigate((navigation) => {
@@ -13,5 +13,5 @@
 	});
 </script>
 
-<h1>{from?.pathname} -> {to?.pathname}</h1>
+<h1>{from?.url.pathname} -> {to?.url.pathname}</h1>
 <a href="/after-navigate/b">/b</a>
diff --git a/packages/kit/test/apps/basics/src/routes/after-navigate/b/+page.svelte b/packages/kit/test/apps/basics/src/routes/after-navigate/b/+page.svelte
index c1e43e302..3db6a4022 100644
--- a/packages/kit/test/apps/basics/src/routes/after-navigate/b/+page.svelte
+++ b/packages/kit/test/apps/basics/src/routes/after-navigate/b/+page.svelte
@@ -1,10 +1,10 @@
 <script>
 	import { afterNavigate } from '$app/navigation';
 
-	/** @type {URL | null} */
+	/** @type {import('@sveltejs/kit').NavigationTarget | null} */
 	let from;
 
-	/** @type {URL} */
+	/** @type {import('@sveltejs/kit').NavigationTarget} */
 	let to;
 
 	afterNavigate((navigation) => {
@@ -13,5 +13,5 @@
 	});
 </script>
 
-<h1>{from?.pathname} -> {to?.pathname}</h1>
+<h1>{from?.url.pathname} -> {to?.url.pathname}</h1>
 <a href="/after-navigate/a">/a</a>
diff --git a/packages/kit/test/apps/basics/src/routes/store/navigating/+layout.svelte b/packages/kit/test/apps/basics/src/routes/store/navigating/+layout.svelte
index 706de9b32..c15660dbf 100644
--- a/packages/kit/test/apps/basics/src/routes/store/navigating/+layout.svelte
+++ b/packages/kit/test/apps/basics/src/routes/store/navigating/+layout.svelte
@@ -11,7 +11,7 @@
 <div id="nav-status">
 	{#if $navigating}
 		<p id="navigating">
-			navigating from {$navigating.from.pathname} to {$navigating.to.pathname} ({$navigating.type})
+			navigating from {$navigating.from.url.pathname} to {$navigating.to.url.pathname} ({$navigating.type})
 		</p>
 	{:else}
 		<p id="not-navigating">not currently navigating</p>