diff --git a/.changeset/famous-bars-search.md b/.changeset/famous-bars-search.md new file mode 100644 index 000000000000..3e1e8b8af2ab --- /dev/null +++ b/.changeset/famous-bars-search.md @@ -0,0 +1,6 @@ +--- +'@sveltejs/kit': major +--- + +breaking: rename `Pathname` type to `Path` and `Asset` to `AssetPath` +breaking: remove leading `/` from `Path` and `AssetPath` diff --git a/documentation/docs/98-reference/20-$app-types.md b/documentation/docs/98-reference/20-$app-types.md index 97053e762869..0663a89ea385 100644 --- a/documentation/docs/98-reference/20-$app-types.md +++ b/documentation/docs/98-reference/20-$app-types.md @@ -13,14 +13,14 @@ This module contains generated types for the routes in your app. import type { RouteId, RouteParams, LayoutParams } from '$app/types'; ``` -## Asset +## AssetPath -A union of all the filenames of assets contained in your `static` directory, plus a `string` wildcard for asset paths generated from `import` declarations. +A union of all the filenames of assets contained in your `static` directory, relative to the `base` path.
```dts -type Asset = '/favicon.png' | '/robots.txt' | (string & {}); +type AssetPath = 'favicon.png' | 'robots.txt' | (string & {}); ```
@@ -37,21 +37,21 @@ type RouteId = '/' | '/my-route' | '/my-other-route/[param]'; -## Pathname +## Path -A union of all valid pathnames in your app. +A union of all valid paths in your app, relative to the `base` path.
```dts -type Pathname = '/' | '/my-route' | `/my-other-route/${string}` & {}; +type Path = '' | 'my-route' | `my-other-route/${string}` & {}; ```
## ResolvedPathname -Similar to `Pathname`, but possibly prefixed with a [base path](configuration#paths). Used for `page.url.pathname`. +Similar to `Path`, but prefixed with a [base path](configuration#paths). Used for `page.url.pathname`.
diff --git a/packages/kit/src/core/sync/write_non_ambient.js b/packages/kit/src/core/sync/write_non_ambient.js index 9dd4126aab83..3010cc281f7f 100644 --- a/packages/kit/src/core/sync/write_non_ambient.js +++ b/packages/kit/src/core/sync/write_non_ambient.js @@ -22,10 +22,6 @@ const remove_group_segments = (/** @type {string} */ id) => { * @returns {string[]} */ function get_pathnames_for_trailing_slash(pathname, route) { - if (pathname === '/') { - return [pathname]; - } - /** @type {Set} */ const pathnames = new Set(); @@ -201,7 +197,7 @@ function generate_app_types(manifest_data, config) { for (const route of manifest_data.routes) { const pathname = remove_group_segments(route.id); - let normalized_pathname = pathname; + let normalized_pathname = pathname.slice(1); /** @type {(path: string) => string} */ let serialise = s; @@ -215,7 +211,7 @@ function generate_app_types(manifest_data, config) { dynamic_routes.push(route_type); - normalized_pathname = replace_required_params(replace_optional_params(pathname)); + normalized_pathname = replace_required_params(replace_optional_params(pathname)).slice(1); serialise = (p) => `\`${p}\` & {}`; } @@ -240,7 +236,7 @@ function generate_app_types(manifest_data, config) { layouts.push(`${s(route.id)}: ${layout_type}`); } - const assets = manifest_data.assets.map((asset) => s('/' + asset.file)); + const assets = manifest_data.assets.map((asset) => s(asset.file)); return [ 'declare module "$app/types" {', @@ -248,9 +244,9 @@ function generate_app_types(manifest_data, config) { `\t\tRouteId(): ${manifest_data.routes.map((r) => s(r.id)).join(' | ')};`, `\t\tRouteParams(): {\n\t\t\t${dynamic_routes.join(';\n\t\t\t')}\n\t\t};`, `\t\tLayoutParams(): {\n\t\t\t${layouts.join(';\n\t\t\t')}\n\t\t};`, - `\t\tPathname(): ${Array.from(pathnames).join(' | ')};`, - '\t\tResolvedPathname(): `${"" | `/${string}`}${ReturnType}`;', - `\t\tAsset(): ${assets.concat('string & {}').join(' | ')};`, + `\t\tPath(): ${Array.from(pathnames).join(' | ')};`, + '\t\tResolvedPathname(): `${"/" | `/${string}/`}${ReturnType}`;', + `\t\tAssetPath(): ${assets.join(' | ') || 'never'};`, '\t}', '}' ].join('\n'); diff --git a/packages/kit/src/core/sync/write_types/test/app-types/+page.js b/packages/kit/src/core/sync/write_types/test/app-types/+page.js index 2bbf8cfe3455..ee5b3a316821 100644 --- a/packages/kit/src/core/sync/write_types/test/app-types/+page.js +++ b/packages/kit/src/core/sync/write_types/test/app-types/+page.js @@ -67,44 +67,44 @@ const matcherParentLayoutParams = {}; matcherParentLayoutParams.locale = 'fr'; // any string -/** @type {import('$app/types').Pathname} */ +/** @type {import('$app/types').Path} */ let pathname; // @ts-expect-error route doesn't exist -pathname = '/nope'; +pathname = 'nope'; // @ts-expect-error route doesn't exist -pathname = '/foo'; +pathname = 'foo'; // @ts-expect-error route doesn't exist -pathname = '/foo/'; -pathname = '/foo/1/2'; // okay -pathname = '/foo/1/2/'; // okay +pathname = 'foo/'; +pathname = 'foo/1/2'; // okay +pathname = 'foo/1/2/'; // okay // Test layout groups -pathname = '/path-a'; +pathname = 'path-a'; // @ts-expect-error default trailing slash is never, so we should not have it here -pathname = '/path-a/'; +pathname = 'path-a/'; // @ts-expect-error layout group names are NOT part of the pathname type -pathname = '/(group)/path-a'; +pathname = '(group)/path-a'; // Test trailing-slash - always -pathname = '/path-a/trailing-slash/always/'; -pathname = '/path-a/trailing-slash/always/endpoint/'; -pathname = '/path-a/trailing-slash/always/layout/inside/'; +pathname = 'path-a/trailing-slash/always/'; +pathname = 'path-a/trailing-slash/always/endpoint/'; +pathname = 'path-a/trailing-slash/always/layout/inside/'; // Test trailing-slash - ignore -pathname = '/path-a/trailing-slash/ignore'; -pathname = '/path-a/trailing-slash/ignore/'; -pathname = '/path-a/trailing-slash/ignore/endpoint'; -pathname = '/path-a/trailing-slash/ignore/endpoint/'; -pathname = '/path-a/trailing-slash/ignore/layout/inside'; -pathname = '/path-a/trailing-slash/ignore/layout/inside/'; +pathname = 'path-a/trailing-slash/ignore'; +pathname = 'path-a/trailing-slash/ignore/'; +pathname = 'path-a/trailing-slash/ignore/endpoint'; +pathname = 'path-a/trailing-slash/ignore/endpoint/'; +pathname = 'path-a/trailing-slash/ignore/layout/inside'; +pathname = 'path-a/trailing-slash/ignore/layout/inside/'; // Test trailing-slash - never (default) -pathname = '/path-a/trailing-slash/never'; -pathname = '/path-a/trailing-slash/never/endpoint'; -pathname = '/path-a/trailing-slash/never/layout/inside'; +pathname = 'path-a/trailing-slash/never'; +pathname = 'path-a/trailing-slash/never/endpoint'; +pathname = 'path-a/trailing-slash/never/layout/inside'; // Test trailing-slash - always (endpoint) and never (page) -pathname = '/path-a/trailing-slash/mixed'; +pathname = 'path-a/trailing-slash/mixed'; // eslint-disable-next-line @typescript-eslint/no-unused-vars -pathname = '/path-a/trailing-slash/mixed/'; +pathname = 'path-a/trailing-slash/mixed/'; diff --git a/packages/kit/src/runtime/app/paths/client.js b/packages/kit/src/runtime/app/paths/client.js index 53eb2a7d2fda..1da65040b6b3 100644 --- a/packages/kit/src/runtime/app/paths/client.js +++ b/packages/kit/src/runtime/app/paths/client.js @@ -1,8 +1,9 @@ -/** @import { Asset, RouteId, RouteIdWithSearchOrHash, Pathname, PathnameWithSearchOrHash, ResolvedPathname, RouteParams } from '$app/types' */ +/** @import { AssetPath, RouteId, RouteIdWithSearchOrHash, Path, PathnameWithSearchOrHash, ResolvedPathname, RouteParams } from '$app/types' */ /** @import { ResolveArgs } from './types.js' */ import { base, assets, hash_routing } from './internal/client.js'; import { resolve_route } from '../../../utils/routing.js'; import { get_navigation_intent } from '../../client/client.js'; +import { DEV } from 'esm-env'; /** * Resolve the URL of an asset in your `static` directory, by prefixing it with [`config.paths.assets`](https://svelte.dev/docs/kit/configuration#paths) if configured, or otherwise by prefixing it with the base path. @@ -15,15 +16,24 @@ import { get_navigation_intent } from '../../client/client.js'; * import { asset } from '$app/paths'; * * - * a potato + * a potato * ``` * @since 2.26 * - * @param {Asset} file + * @param {AssetPath} file * @returns {string} */ export function asset(file) { - return (assets || base) + file; + // TODO 4.0 remove this + if (file[0] === '/') { + if (DEV) { + console.warn(`\`asset('${file}')\` should now be \`asset('${file.slice(1)}')\``); + } + + file = file.slice(1); + } + + return (assets || base) + '/' + file; } const pathname_prefix = hash_routing ? '#' : ''; @@ -38,7 +48,7 @@ const pathname_prefix = hash_routing ? '#' : ''; * import { resolve } from '$app/paths'; * * // using a pathname - * const resolved = resolve(`/blog/hello-world`); + * const resolved = resolve(`blog/hello-world`); * * // using a route ID plus parameters * const resolved = resolve('/blog/[slug]', { @@ -52,14 +62,18 @@ const pathname_prefix = hash_routing ? '#' : ''; * @returns {ResolvedPathname} */ export function resolve(...args) { - if (!args[0].startsWith('/')) { - throw new Error( - `Cannot use \`resolve(...)\` with a non-absolute pathname or route ID (got "${args[0]}"). ` + - '`resolve` is only for internal pathnames and route IDs; external URLs should be used directly.' - ); + if (args[0][0] === '/') { + const [id, params] = args; + + // route ID + if (id.includes('[') && !params) { + throw new Error(`Missing params for dynamic route ID ${id}`); + } + + return base + pathname_prefix + resolve_route(args[0], args[1] ?? {}); } - return base + pathname_prefix + resolve_route(args[0], args[1] ?? {}); + return base + pathname_prefix + '/' + args[0]; } /** @@ -69,7 +83,7 @@ export function resolve(...args) { * ```js * import { match } from '$app/paths'; * - * const route = await match('/blog/hello-world'); + * const route = await match('blog/hello-world'); * * if (route?.id === '/blog/[slug]') { * const slug = route.params.slug; @@ -79,7 +93,7 @@ export function resolve(...args) { * ``` * @since 2.52.0 * - * @param {Pathname | URL | (string & {})} url + * @param {Path | URL | (string & {})} url * @returns {Promise<{ [K in RouteId]: { id: K; params: RouteParams; } }[RouteId] | null>} */ export async function match(url) { diff --git a/packages/kit/src/runtime/app/paths/server.js b/packages/kit/src/runtime/app/paths/server.js index cc559123eb4d..863f0629768d 100644 --- a/packages/kit/src/runtime/app/paths/server.js +++ b/packages/kit/src/runtime/app/paths/server.js @@ -5,23 +5,36 @@ import { add_data_suffix } from '../../pathname.js'; import { try_get_request_store } from '@sveltejs/kit/internal/server'; import { manifest } from '../../server/internal.js'; import { get_hooks } from '__SERVER__/internal.js'; +import { DEV } from 'esm-env'; /** @type {import('./client.js').asset} */ export function asset(file) { - // @ts-expect-error we use the `resolve` mechanism, but with the 'wrong' input - return assets && assets !== base ? assets + file : resolve(file); + // TODO 4.0 remove this + if (file[0] === '/') { + if (DEV) { + console.warn(`\`asset('${file}')\` should now be \`asset('${file.slice(1)}')\``); + } + + file = file.slice(1); + } + + return assets !== base ? `${assets}/${file}` : resolve(file); } /** @type {import('./client.js').resolve} */ export function resolve(id, params) { - if (!id.startsWith('/')) { - throw new Error( - `Cannot use \`resolve(...)\` with a non-absolute pathname or route ID (got "${id}"). ` + - '`resolve` is only for internal pathnames and route IDs; external URLs should be used directly.' - ); - } + let resolved; + + if (id[0] === '/') { + // route ID + if (id.includes('[') && !params) { + throw new Error(`Missing params for dynamic route ID ${id}`); + } - const resolved = resolve_route(id, params ?? {}); + resolved = resolve_route(id, params ?? {}); + } else { + resolved = '/' + id; + } if (relative) { const store = try_get_request_store(); diff --git a/packages/kit/src/runtime/app/paths/types.d.ts b/packages/kit/src/runtime/app/paths/types.d.ts index f8bad8f8cf21..9301985fb447 100644 --- a/packages/kit/src/runtime/app/paths/types.d.ts +++ b/packages/kit/src/runtime/app/paths/types.d.ts @@ -1,23 +1,15 @@ -import { - PathnameWithSearchOrHash, - RouteId, - RouteIdWithSearchOrHash, - RouteParams -} from '$app/types'; +import { RouteId, RouteParams } from '$app/types'; -type StripSearchOrHash = T extends `${infer Pathname}?${string}` - ? Pathname - : T extends `${infer Pathname}#${string}` - ? Pathname +type StripSearchOrHash = T extends `${infer U}?${string}` + ? U + : T extends `${infer U}#${string}` + ? U : T; -export type ResolveArgs = - T extends RouteId - ? RouteParams extends Record +export type ResolveArgs = T extends `/${string}` + ? StripSearchOrHash extends infer U extends RouteId + ? RouteParams extends Record ? [route: T] - : [route: T, params: RouteParams] - : StripSearchOrHash extends infer U extends RouteId - ? RouteParams extends Record - ? [route: T] - : [route: T, params: RouteParams] - : [route: T]; + : [route: T, params: RouteParams] + : [never] + : [pathname: T]; diff --git a/packages/kit/src/types/ambient.d.ts b/packages/kit/src/types/ambient.d.ts index d01f3b4a426d..ddb218ac68d4 100644 --- a/packages/kit/src/types/ambient.d.ts +++ b/packages/kit/src/types/ambient.d.ts @@ -99,9 +99,9 @@ declare module '$app/types' { RouteId(): string; RouteParams(): Record>; LayoutParams(): Record>; - Pathname(): string; + Path(): string; ResolvedPathname(): string; - Asset(): string; + AssetPath(): string; } /** @@ -129,25 +129,22 @@ declare module '$app/types' { : Record; /** - * A union of all valid pathnames in your app. + * A union of all valid paths in your app, relative to the `base` path. */ - export type Pathname = ReturnType; + export type Path = ReturnType; /** - * `Pathname`, but possibly suffixed with a search string and/or hash. + * `Path`, but possibly suffixed with a search string and/or hash. */ - export type PathnameWithSearchOrHash = - | Pathname - | `${Pathname}?${string}` - | `${Pathname}#${string}`; + export type PathnameWithSearchOrHash = Path | `${Path}?${string}` | `${Path}#${string}`; /** - * `Pathname`, but possibly prefixed with a base path. Used for `page.url.pathname`. + * `Path`, but prefixed with a base path. Used for `page.url.pathname`. */ export type ResolvedPathname = ReturnType; /** - * A union of all the filenames of assets contained in your `static` directory. + * A union of all the filenames of assets contained in your `static` directory, relative to the `base` path. */ - export type Asset = ReturnType; + export type AssetPath = ReturnType; } diff --git a/packages/kit/test/apps/options-2/src/routes/+error.svelte b/packages/kit/test/apps/options-2/src/routes/+error.svelte index c8320ab59160..e62e63708643 100644 --- a/packages/kit/test/apps/options-2/src/routes/+error.svelte +++ b/packages/kit/test/apps/options-2/src/routes/+error.svelte @@ -5,5 +5,5 @@

{page.status}

-

base: {resolve('/')}

-

assets: {asset('/')}

+

base: {resolve('')}

+

assets: {asset('answer.txt').replace('answer.txt', '')}

diff --git a/packages/kit/test/apps/options-2/src/routes/+page.svelte b/packages/kit/test/apps/options-2/src/routes/+page.svelte index 33b8352a72f8..823bce98e12f 100644 --- a/packages/kit/test/apps/options-2/src/routes/+page.svelte +++ b/packages/kit/test/apps/options-2/src/routes/+page.svelte @@ -4,8 +4,8 @@

Hello

-

base: {resolve('/')}

-

assets: {asset('/')}

+

base: {resolve('')}

+

assets: {asset('answer.txt').replace('answer.txt', '')}

Go to /hello diff --git a/packages/kit/test/apps/options-2/src/routes/deeply/nested/page/+page.svelte b/packages/kit/test/apps/options-2/src/routes/deeply/nested/page/+page.svelte index f645edd52e3d..6c2701fa0f46 100644 --- a/packages/kit/test/apps/options-2/src/routes/deeply/nested/page/+page.svelte +++ b/packages/kit/test/apps/options-2/src/routes/deeply/nested/page/+page.svelte @@ -4,8 +4,8 @@

Hello

-

base: {resolve('/')}

-

assets: {asset('/')}

+

base: {resolve('')}

+

assets: {asset('answer.txt').replace('answer.txt', '')}