Repository: sveltejs/kit (js)

Issue:
Ability to access `$app/stores` at any time in the browser
### Describe the problem

There is sometimes code that I know will only ever be run on the browser (for example, if it's only called from an event handler), and having to jump through the same hoops as I do on the server in order to associate that with the appropriate (and only) instances of the stores is annoying.

### Describe the proposed solution

Store values on the browser should live in a singleton module (or something similar) rather than in the Svelte context for the root component. `getStores()` should be able to be called at any time, not just during component initialization. If it wouldn't cause too much of a divergence between browser and server code, we could also remove the transparent calling of `getStores()` during the autosubscription to any of the individual stores.

### Alternatives considered

Right now, I'm using a fairly ugly workaround where I serialize the values I need to a `data-foo` attribute in my root layout, which I then read and deserialize elsewhere.

Another option presumably would be to have an `onMount` call in my root layout that sets the store(s) as a global on `window`.

### Importance

would make my life easier

### Additional Information

_No response_

Pull request description:
keep browser stores in always-accessible singleton
Resolves #6043.

This lets you use `$app/stores` everywhere on the browser, not just where you would be allowed to on the server. We don't need to worry about mixing data for different requests like we do on the server - there's only ever one set of stores at once - so we can be a lot more relaxed about this.

### 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.
- [ ] 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
- [ ] 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/gentle-years-leave.md b/.changeset/gentle-years-leave.md
new file mode 100644
index 000000000..9b512b8dc
--- /dev/null
+++ b/.changeset/gentle-years-leave.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+Allow `$app/stores` to be used from anywhere on the browser
diff --git a/.changeset/kind-tools-shop.md b/.changeset/kind-tools-shop.md
new file mode 100644
index 000000000..6e6c04ccf
--- /dev/null
+++ b/.changeset/kind-tools-shop.md
@@ -0,0 +1,5 @@
+---
+'create-svelte': patch
+---
+
+setup prettier plugin-search-dirs to enable use with pnpm
diff --git a/.changeset/ninety-walls-heal.md b/.changeset/ninety-walls-heal.md
new file mode 100644
index 000000000..3c270bbb0
--- /dev/null
+++ b/.changeset/ninety-walls-heal.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+use `enumerable: false` on "[x] no longer exists" getters so that they are not triggered by spreading
diff --git a/.changeset/rich-walls-doubt.md b/.changeset/rich-walls-doubt.md
new file mode 100644
index 000000000..c56fd899f
--- /dev/null
+++ b/.changeset/rich-walls-doubt.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+Fix pattern matching for routes starting with an encoded `@` symbol
diff --git a/.changeset/tidy-mice-kick.md b/.changeset/tidy-mice-kick.md
new file mode 100644
index 000000000..bf30dfd9c
--- /dev/null
+++ b/.changeset/tidy-mice-kick.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+Fix typos in generating `LayoutServerLoad.{name}` type
diff --git a/.changeset/tricky-cheetahs-warn.md b/.changeset/tricky-cheetahs-warn.md
new file mode 100644
index 000000000..f201e5659
--- /dev/null
+++ b/.changeset/tricky-cheetahs-warn.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+Fix sorting of files into +layout, +error, everything else.
diff --git a/.changeset/wet-crews-smoke.md b/.changeset/wet-crews-smoke.md
new file mode 100644
index 000000000..b6d9f4417
--- /dev/null
+++ b/.changeset/wet-crews-smoke.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/adapter-vercel': patch
+---
+
+Use `platform: 'browser'` for esbuild in Vercel Edge Functions.
diff --git a/documentation/docs/15-configuration.md b/documentation/docs/15-configuration.md
index 2f99bd282..e94002412 100644
--- a/documentation/docs/15-configuration.md
+++ b/documentation/docs/15-configuration.md
@@ -120,6 +120,8 @@ const config = {
 
 > The built-in `$lib` alias is controlled by `config.kit.files.lib` as it is used for packaging.
 
+> You will need to run `npm run dev` to have SvelteKit automatically generate the required alias configuration in `jsconfig.json` or `tsconfig.json`.
+
 ### appDir
 
 The directory relative to `paths.assets` where the built JS and CSS (and imported assets) are served from. (The filenames therein contain content-based hashes, meaning they can be cached indefinitely). Must not start or end with `/`.
diff --git a/documentation/faq/50-aliases.md b/documentation/faq/50-aliases.md
deleted file mode 100644
index fccd8d641..000000000
--- a/documentation/faq/50-aliases.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-title: How do I setup a path alias?
----
-
-Aliases can be set in `svelte.config.js` as described in the [`configuration`](/docs/configuration#alias) docs.
-
-Then run `npm run prepare` or `npm run dev` (both will execute a sync command). SvelteKit will automatically generate the required alias configuration in `jsconfig.json` or `tsconfig.json`.
diff --git a/packages/adapter-vercel/index.js b/packages/adapter-vercel/index.js
index fc55c079f..532d3534e 100644
--- a/packages/adapter-vercel/index.js
+++ b/packages/adapter-vercel/index.js
@@ -186,7 +186,7 @@ export default function ({ external = [], edge, split } = {}) {
 					outfile: `${dirs.functions}/${name}.func/index.js`,
 					target: 'es2020', // TODO verify what the edge runtime supports
 					bundle: true,
-					platform: 'node',
+					platform: 'browser',
 					format: 'esm',
 					external,
 					sourcemap: 'linked'
diff --git a/packages/create-svelte/shared/+eslint+prettier/package.json b/packages/create-svelte/shared/+eslint+prettier/package.json
index b66bc9c09..a8b937b25 100644
--- a/packages/create-svelte/shared/+eslint+prettier/package.json
+++ b/packages/create-svelte/shared/+eslint+prettier/package.json
@@ -3,7 +3,7 @@
 		"eslint-config-prettier": "^8.3.0"
 	},
 	"scripts": {
-		"lint": "prettier --check --plugin-search-dir=. . && eslint .",
-		"format": "prettier --write --plugin-search-dir=. ."
+		"lint": "prettier --check . && eslint .",
+		"format": "prettier --write ."
 	}
 }
diff --git a/packages/create-svelte/shared/+prettier/.prettierrc b/packages/create-svelte/shared/+prettier/.prettierrc
index ff2677efd..d177cd62d 100644
--- a/packages/create-svelte/shared/+prettier/.prettierrc
+++ b/packages/create-svelte/shared/+prettier/.prettierrc
@@ -2,5 +2,7 @@
 	"useTabs": true,
 	"singleQuote": true,
 	"trailingComma": "none",
-	"printWidth": 100
+	"printWidth": 100,
+	"pluginSearchDirs": ["."],
+	"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
 }
diff --git a/packages/create-svelte/shared/-eslint+prettier/package.json b/packages/create-svelte/shared/-eslint+prettier/package.json
index e9103a7e2..bcdff7cf9 100644
--- a/packages/create-svelte/shared/-eslint+prettier/package.json
+++ b/packages/create-svelte/shared/-eslint+prettier/package.json
@@ -1,6 +1,6 @@
 {
 	"scripts": {
-		"lint": "prettier --check --plugin-search-dir=. .",
-		"format": "prettier --write --plugin-search-dir=. ."
+		"lint": "prettier --check .",
+		"format": "prettier --write ."
 	}
 }
diff --git a/packages/kit/src/core/sync/create_manifest_data/index.js b/packages/kit/src/core/sync/create_manifest_data/index.js
index 32de6c264..ce8944511 100644
--- a/packages/kit/src/core/sync/create_manifest_data/index.js
+++ b/packages/kit/src/core/sync/create_manifest_data/index.js
@@ -478,9 +478,9 @@ function list_files(dir, path = '', files = []) {
 			// sort each directory in (+layout, +error, everything else) order
 			// so that we can trace layouts/errors immediately
 
-			if (a.startsWith('+layout')) {
-				if (!b.startsWith('+layout')) return -1;
-			} else if (b.startsWith('+layout')) {
+			if (a.startsWith('+layout') || a.startsWith('+error')) {
+				if (!b.startsWith('+layout') && !b.startsWith('+error')) return -1;
+			} else if (b.startsWith('+layout') || b.startsWith('+error')) {
 				return 1;
 			} else if (a.startsWith('__')) {
 				if (!b.startsWith('__')) return -1;
diff --git a/packages/kit/src/core/sync/write_root.js b/packages/kit/src/core/sync/write_root.js
index c71e30f5b..0dd8536bd 100644
--- a/packages/kit/src/core/sync/write_root.js
+++ b/packages/kit/src/core/sync/write_root.js
@@ -43,6 +43,7 @@ export function write_root(manifest_data, output) {
 			<!-- This file is generated by @sveltejs/kit — do not edit it! -->
 			<script>
 				import { setContext, afterUpdate, onMount } from 'svelte';
+				import { browser } from '$app/env';
 
 				// stores
 				export let stores;
@@ -52,7 +53,9 @@ export function write_root(manifest_data, output) {
 				${levels.map((l) => `export let data_${l} = null;`).join('\n\t\t\t\t')}
 				export let errors;
 
-				setContext('__svelte__', stores);
+				if (!browser) {
+					setContext('__svelte__', stores);
+				}
 
 				$: stores.page.set(page);
 				afterUpdate(stores.page.notify);
diff --git a/packages/kit/src/core/sync/write_types.js b/packages/kit/src/core/sync/write_types.js
index 814e732c5..624223027 100644
--- a/packages/kit/src/core/sync/write_types.js
+++ b/packages/kit/src/core/sync/write_types.js
@@ -363,7 +363,7 @@ function write_types_for_dir(config, manifest_data, routes_dir, dir, groups, ts)
 				}
 				if (server_load) {
 					server_load_exports.push(
-						`export type ${name}<OutputData extends Record<string, any> | void = Record<string, any> | void> = ${load};`
+						`export type ${name}<OutputData extends Record<string, any> | void = Record<string, any> | void> = ${server_load};`
 					);
 					server_load_event_exports.push(
 						`export type ${name} = Parameters<LayoutServerLoad.${name}>[0];`
diff --git a/packages/kit/src/runtime/app/stores.js b/packages/kit/src/runtime/app/stores.js
index d98e71715..8f1e21984 100644
--- a/packages/kit/src/runtime/app/stores.js
+++ b/packages/kit/src/runtime/app/stores.js
@@ -1,5 +1,6 @@
 import { getContext } from 'svelte';
 import { browser } from './env.js';
+import { stores as browser_stores } from '../client/singletons.js';
 
 // TODO remove this (for 1.0? after 1.0?)
 let warned = false;
@@ -15,29 +16,39 @@ export function stores() {
  * @type {import('$app/stores').getStores}
  */
 export const getStores = () => {
-	const stores = getContext('__svelte__');
+	const stores = browser ? browser_stores : getContext('__svelte__');
 
-	return {
+	const readonly_stores = {
 		page: {
 			subscribe: stores.page.subscribe
 		},
 		navigating: {
 			subscribe: stores.navigating.subscribe
 		},
-		// TODO remove this (for 1.0? after 1.0?)
-		// @ts-expect-error - deprecated, not part of type definitions, but still callable
-		get preloading() {
-			console.error('stores.preloading is deprecated; use stores.navigating instead');
-			return {
-				subscribe: stores.navigating.subscribe
-			};
-		},
-		get session() {
-			removed_session();
-			return {};
-		},
 		updated: stores.updated
 	};
+
+	// TODO remove this for 1.0
+	Object.defineProperties(readonly_stores, {
+		preloading: {
+			get() {
+				console.error('stores.preloading is deprecated; use stores.navigating instead');
+				return {
+					subscribe: stores.navigating.subscribe
+				};
+			},
+			enumerable: false
+		},
+		session: {
+			get() {
+				removed_session();
+				return {};
+			},
+			enumerable: false
+		}
+	});
+
+	return readonly_stores;
 };
 
 /** @type {typeof import('$app/stores').page} */
diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js
index 806214c2f..214d8843c 100644
--- a/packages/kit/src/runtime/client/client.js
+++ b/packages/kit/src/runtime/client/client.js
@@ -1,15 +1,7 @@
 import { onMount, tick } from 'svelte';
-import { writable } from 'svelte/store';
 import { normalize_error } from '../../utils/error.js';
 import { LoadURL, decode_params, normalize_path } from '../../utils/url.js';
-import {
-	create_updated_store,
-	find_anchor,
-	get_base_uri,
-	get_href,
-	notifiable_store,
-	scroll_state
-} from './utils.js';
+import { find_anchor, get_base_uri, get_href, scroll_state } from './utils.js';
 import { lock_fetch, unlock_fetch, initial_fetch, native_fetch } from './fetcher.js';
 import { parse } from './parse.js';
 import { error } from '../../index/index.js';
@@ -17,6 +9,7 @@ import { error } from '../../index/index.js';
 import Root from '__GENERATED__/root.svelte';
 import { nodes, dictionary, matchers } from '__GENERATED__/client-manifest.js';
 import { HttpError, Redirect } from '../../index/private.js';
+import { stores } from './singletons.js';
 
 const SCROLL_KEY = 'sveltekit:scroll';
 const INDEX_KEY = 'sveltekit:index';
@@ -59,13 +52,6 @@ export function create_client({ target, base, trailing_slash }) {
 	/** @type {Array<((href: string) => boolean)>} */
 	const invalidated = [];
 
-	const stores = {
-		url: notifiable_store({}),
-		page: notifiable_store({}),
-		navigating: writable(/** @type {import('types').Navigation | null} */ (null)),
-		updated: create_updated_store()
-	};
-
 	/** @type {{id: string | null, promise: Promise<import('./types').NavigationResult | undefined> | null}} */
 	const load_cache = {
 		id: null,
@@ -583,25 +569,36 @@ export function create_client({ target, base, trailing_slash }) {
 					// does await parent() inside an if branch which wasn't executed yet.
 					uses.parent = true;
 					return parent;
+				}
+			};
+
+			// TODO remove this for 1.0
+			Object.defineProperties(load_input, {
+				props: {
+					get() {
+						throw new Error(
+							'@migration task: Replace `props` with `data` stuff https://github.com/sveltejs/kit/discussions/5774#discussioncomment-3292693'
+						);
+					},
+					enumerable: false
 				},
-				// @ts-expect-error
-				get props() {
-					throw new Error(
-						'@migration task: Replace `props` with `data` stuff https://github.com/sveltejs/kit/discussions/5774#discussioncomment-3292693'
-					);
-				},
-				get session() {
-					// TODO remove this for 1.0
-					throw new Error(
-						'session is no longer available. See https://github.com/sveltejs/kit/discussions/5883'
-					);
+				session: {
+					get() {
+						throw new Error(
+							'session is no longer available. See https://github.com/sveltejs/kit/discussions/5883'
+						);
+					},
+					enumerable: false
 				},
-				get stuff() {
-					throw new Error(
-						'@migration task: Remove stuff https://github.com/sveltejs/kit/discussions/5774#discussioncomment-3292693'
-					);
+				stuff: {
+					get() {
+						throw new Error(
+							'@migration task: Remove stuff https://github.com/sveltejs/kit/discussions/5774#discussioncomment-3292693'
+						);
+					},
+					enumerable: false
 				}
-			};
+			});
 
 			if (import.meta.env.DEV) {
 				try {
diff --git a/packages/kit/src/runtime/client/singletons.js b/packages/kit/src/runtime/client/singletons.js
index 6cf3e8417..f1ab3986c 100644
--- a/packages/kit/src/runtime/client/singletons.js
+++ b/packages/kit/src/runtime/client/singletons.js
@@ -1,3 +1,6 @@
+import { writable } from 'svelte/store';
+import { create_updated_store, notifiable_store } from './utils.js';
+
 /** @type {import('./types').Client} */
 export let client;
 
@@ -9,3 +12,10 @@ export let client;
 export function init(opts) {
 	client = opts.client;
 }
+
+export const stores = {
+	url: notifiable_store({}),
+	page: notifiable_store({}),
+	navigating: writable(/** @type {import('types').Navigation | null} */ (null)),
+	updated: create_updated_store()
+};
diff --git a/packages/kit/src/runtime/server/page/load_data.js b/packages/kit/src/runtime/server/page/load_data.js
index 2562356fd..03422217c 100644
--- a/packages/kit/src/runtime/server/page/load_data.js
+++ b/packages/kit/src/runtime/server/page/load_data.js
@@ -55,23 +55,31 @@ export async function load_data({ event, fetcher, node, parent, server_data_prom
 		return server_data;
 	}
 
-	const data = await node.shared.load.call(null, {
+	const load_input = {
 		url: state.prerendering ? new PrerenderingURL(event.url) : new LoadURL(event.url),
 		params: event.params,
 		data: server_data,
 		routeId: event.routeId,
-		get session() {
-			// TODO remove for 1.0
-			throw new Error(
-				'session is no longer available. See https://github.com/sveltejs/kit/discussions/5883'
-			);
-		},
 		fetch: fetcher,
 		setHeaders: event.setHeaders,
 		depends: () => {},
 		parent
+	};
+
+	// TODO remove this for 1.0
+	Object.defineProperties(load_input, {
+		session: {
+			get() {
+				throw new Error(
+					'session is no longer available. See https://github.com/sveltejs/kit/discussions/5883'
+				);
+			},
+			enumerable: false
+		}
 	});
 
+	const data = await node.shared.load.call(null, load_input);
+
 	return data ? unwrap_promises(data) : null;
 }
 
diff --git a/packages/kit/src/utils/routing.js b/packages/kit/src/utils/routing.js
index fb6d219ad..826c75a4d 100644
--- a/packages/kit/src/utils/routing.js
+++ b/packages/kit/src/utils/routing.js
@@ -16,11 +16,12 @@ export function parse_route_id(id) {
 		id === ''
 			? /^\/$/
 			: new RegExp(
-					`^${decodeURIComponent(id)
+					`^${id
 						.split(/(?:@[a-zA-Z0-9_-]+)?(?:\/|$)/)
 						.map((segment, i, segments) => {
+							const decoded_segment = decodeURIComponent(segment);
 							// special case — /[...rest]/ could contain zero segments
-							const match = /^\[\.\.\.(\w+)(?:=(\w+))?\]$/.exec(segment);
+							const match = /^\[\.\.\.(\w+)(?:=(\w+))?\]$/.exec(decoded_segment);
 							if (match) {
 								names.push(match[1]);
 								types.push(match[2]);
@@ -30,9 +31,9 @@ export function parse_route_id(id) {
 							const is_last = i === segments.length - 1;
 
 							return (
-								segment &&
+								decoded_segment &&
 								'/' +
-									segment
+									decoded_segment
 										.split(/\[(.+?)\]/)
 										.map((content, i) => {
 											if (i % 2) {
diff --git a/packages/kit/src/utils/routing.spec.js b/packages/kit/src/utils/routing.spec.js
index dd14241c0..3c14ce927 100644
--- a/packages/kit/src/utils/routing.spec.js
+++ b/packages/kit/src/utils/routing.spec.js
@@ -42,6 +42,16 @@ const tests = {
 		pattern: /^\/matched\/([^/]+?)\/?$/,
 		names: ['id'],
 		types: ['uuid']
+	},
+	'%23hash-encoded': {
+		pattern: /^\/%23hash-encoded\/?$/,
+		names: [],
+		types: []
+	},
+	'%40at-encoded/[id]': {
+		pattern: /^\/@at-encoded\/([^/]+?)\/?$/,
+		names: ['id'],
+		types: [undefined]
 	}
 };
 
diff --git a/packages/kit/types/ambient.d.ts b/packages/kit/types/ambient.d.ts
index 25981f86b..678de3ebe 100644
--- a/packages/kit/types/ambient.d.ts
+++ b/packages/kit/types/ambient.d.ts
@@ -216,24 +216,16 @@ declare module '$app/paths' {
  * import { getStores, navigating, page, updated } from '$app/stores';
  * ```
  *
- * Stores are _contextual_ — they are added to the [context](https://svelte.dev/tutorial/context-api) of your root component. This means that `page` is unique to each request on the server, rather than shared between multiple requests handled by the same server simultaneously.
+ * Stores on the server are _contextual_ — they are added to the [context](https://svelte.dev/tutorial/context-api) of your root component. This means that `page` is unique to each request, rather than shared between multiple requests handled by the same server simultaneously.
  *
  * Because of that, you must subscribe to the stores during component initialization (which happens automatically if you reference the store value, e.g. as `$page`, in a component) before you can use them.
+ *
+ * In the browser, we don't need to worry about this, and stores can be accessed from anywhere. Code that will only ever run on the browser can refer to (or subscribe to) any of these stores at any time.
  */
 declare module '$app/stores' {
 	import { Readable } from 'svelte/store';
 	import { Navigation, Page } from '@sveltejs/kit';
 
-	/**
-	 * A convenience function around `getContext`. Must be called during component initialization.
-	 * Only use this if you need to defer store subscription until after the component has mounted, for some reason.
-	 */
-	export function getStores(): {
-		navigating: typeof navigating;
-		page: typeof page;
-		updated: typeof updated;
-	};
-
 	/**
 	 * A readable store whose value contains page data.
 	 */
@@ -248,6 +240,16 @@ declare module '$app/stores' {
 	 *  A readable store whose initial value is `false`. If [`version.pollInterval`](https://kit.svelte.dev/docs/configuration#version) is a non-zero value, SvelteKit will poll for new versions of the app and update the store value to `true` when it detects one. `updated.check()` will force an immediate check, regardless of polling.
 	 */
 	export const updated: Readable<boolean> & { check: () => boolean };
+
+	/**
+	 * A function that returns all of the contextual stores. On the server, this must be called during component initialization.
+	 * Only use this if you need to defer store subscription until after the component has mounted, for some reason.
+	 */
+	export function getStores(): {
+		navigating: typeof navigating;
+		page: typeof page;
+		updated: typeof updated;
+	};
 }
 
 /**


Test patch:
diff --git a/packages/kit/test/apps/basics/src/routes/store/client-access/+page.svelte b/packages/kit/test/apps/basics/src/routes/store/client-access/+page.svelte
new file mode 100644
index 000000000..d6fb29263
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/store/client-access/+page.svelte
@@ -0,0 +1,13 @@
+<script>
+	import { getStores } from '$app/stores';
+
+	let pathname;
+</script>
+
+<h1>{pathname}</h1>
+
+<button
+	on:click={() => {
+		getStores().page.subscribe(($page) => pathname = $page.url.pathname)();
+	}}>click</button
+>
diff --git a/packages/kit/test/apps/basics/test/client.test.js b/packages/kit/test/apps/basics/test/client.test.js
index 05be5cf42..f95a9568d 100644
--- a/packages/kit/test/apps/basics/test/client.test.js
+++ b/packages/kit/test/apps/basics/test/client.test.js
@@ -585,3 +585,10 @@ test('Can use browser-only global on client-only page', async ({ page, read_erro
 	await expect(page.locator('p')).toHaveText('Works');
 	expect(read_errors('/no-ssr/browser-only-global')).toBe(undefined);
 });
+
+test('can use $app/stores from anywhere on client', async ({ page }) => {
+	await page.goto('/store/client-access');
+	await expect(page.locator('h1')).toHaveText('undefined');
+	await page.click('button');
+	await expect(page.locator('h1')).toHaveText('/store/client-access');
+});