From 27c93d4515ad925092ac10f80ec4f14711d84004 Mon Sep 17 00:00:00 2001 From: difanta Date: Wed, 10 May 2023 19:15:56 +0200 Subject: [PATCH 1/2] first typed fetch, working in some cases --- packages/kit/src/core/sync/sync.js | 4 + packages/kit/src/core/sync/write_api.js | 152 ++++++++++++++++++ .../kit/src/core/sync/write_types/index.js | 17 +- packages/kit/src/exports/index.js | 4 +- packages/kit/src/exports/public.d.ts | 49 ++++-- 5 files changed, 203 insertions(+), 23 deletions(-) create mode 100644 packages/kit/src/core/sync/write_api.js diff --git a/packages/kit/src/core/sync/sync.js b/packages/kit/src/core/sync/sync.js index 3d1f0f9e57e9..92ad72cf442e 100644 --- a/packages/kit/src/core/sync/sync.js +++ b/packages/kit/src/core/sync/sync.js @@ -6,6 +6,7 @@ import { write_tsconfig } from './write_tsconfig.js'; import { write_types, write_all_types } from './write_types/index.js'; import { write_ambient } from './write_ambient.js'; import { write_server } from './write_server.js'; +import { write_api } from './write_api.js'; /** * Initialize SvelteKit's generated files. @@ -30,6 +31,7 @@ export async function create(config) { write_server(config, output); write_root(manifest_data, output); await write_all_types(config, manifest_data); + write_api(config, manifest_data); return { manifest_data }; } @@ -44,6 +46,7 @@ export async function create(config) { */ export async function update(config, manifest_data, file) { await write_types(config, manifest_data, file); + write_api(config, manifest_data); return { manifest_data }; } @@ -67,6 +70,7 @@ export async function all_types(config, mode) { init(config, mode); const manifest_data = create_manifest_data({ config }); await write_all_types(config, manifest_data); + write_api(config, manifest_data); } /** diff --git a/packages/kit/src/core/sync/write_api.js b/packages/kit/src/core/sync/write_api.js new file mode 100644 index 000000000000..4a1c583adcc1 --- /dev/null +++ b/packages/kit/src/core/sync/write_api.js @@ -0,0 +1,152 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { posixify } from '../../utils/filesystem.js'; +import { ts } from './ts.js'; +import { replace_ext_with_js } from './write_types/index.js'; + +/** + * Creates types for all endpoint routes + * @param {import('types').ValidatedConfig} config + * @param {import('types').ManifestData} manifest_data + */ +export function write_api(config, manifest_data) { + if (!ts) return; + + const types_dir = `${config.kit.outDir}/types`; + + try { + fs.mkdirSync(types_dir, { recursive: true }); + } catch {} + + const api_imports = [`import type * as Kit from '@sveltejs/kit';`]; + + /** @type {string[]} */ + const api_declarations = []; + + api_declarations.push( + `export interface TypedRequestInit extends RequestInit { \n\tmethod: Method; \n}` + ); + + api_declarations.push( + `type GetEndpointType any> = Awaited> extends Kit.TypedResponse ? T : never;` + ); + + api_declarations.push( + `type ExpandMethods = { [Method in keyof T]: T[Method] extends (...args: any) => any ? GetEndpointType : never; };` + ); + + api_declarations.push( + `type optional_trailing = \`\` | \`/\` | \`/?\${string}\` | \`?\${string}\` ` + ); + + /** @type {string[]} */ + const api_endpoints = ['export type Endpoints = {']; + + let index = 0; + + for (const route of manifest_data.routes) { + if (route.endpoint) { + const route_import_path = posixify( + path.relative(types_dir, replace_ext_with_js(route.endpoint.file)) + ); + api_endpoints.push(`\t${index}: { `); + api_endpoints.push(`\t\tpath: \`${fixSlugs(posixify(route.id))}\${optional_trailing}\`; `); + api_endpoints.push(`\t\tmethods: ExpandMethods`); + api_endpoints.push(`\t};`); + ++index; + } + } + + api_endpoints.push('};'); + + /** @type {string[]} */ + const api_utility_types = []; + + api_utility_types.push(`type EndpointsPaths = Endpoints[keyof Endpoints]["path"];`); + + api_utility_types.push(`type MatchedPaths = { + [Index in keyof Endpoints as S extends Endpoints[Index]["path"] + ? "matched" + : never]: Endpoints[Index]; + };`); + + api_utility_types.push( + `type ExtractMethodsFromMatched = T extends { matched: { methods: infer K } } ? K : {};` + ); + + api_utility_types.push( + `type ValidMethod = string & keyof ExtractMethodsFromMatched>;` + ); + + api_utility_types.push( + `type TypedResponseFromPath> = Kit.TypedResponse>[Method]>;` + ); + + /** @type {string[]} */ + const api_exports = []; + + api_exports.push( + `export function TypedFetch< + S extends EndpointsPaths, + Method extends ValidMethod + >( + input: S, + init: TypedRequestInit + ): Promise>;` + ); + + api_exports.push( + `export function TypedFetch< + S extends EndpointsPaths, + Method extends ValidMethod = "GET" extends ValidMethod ? "GET" : ValidMethod + >( + input: S, + ...init: "GET" extends ValidMethod ? [init?: TypedRequestInit] : [init: TypedRequestInit] + ): Promise>;` + ); + + /* + // weaker typed option (you can fetch any url without type info if outside of TypedFetch scope) + // this leads to little to no intellisense while writing a typed fetch call because the standard fetch absorbs all partial inputs like { method: ""} + api_exports.push(`export function TypedFetch( + input: URL | RequestInfo, + init?: RequestInit + ): Promise; + `);*/ + + // stronger typed but more opinionated option (you cannot fetch from urls that match enpoints without going through TypedFetch typing) + // this provides strong intellisense on both urls and methods, and informative error when fetching from a endpoint wihtout a default "GET" + api_exports.push(`export function TypedFetch( + input: S extends EndpointsPaths ? {error: \`method is required for endpoint \${S} without GET handler\`, available_methods: \` \${ValidMethod} \`} : S | URL | RequestInfo, + init?: RequestInit + ): Promise;`); + + const output = [ + api_imports.join('\n'), + api_declarations.join('\n'), + api_endpoints.join('\n'), + api_utility_types.join('\n'), + api_exports.join('\n\n') + ] + .filter(Boolean) + .join('\n\n'); + + fs.writeFileSync(`${types_dir}/$api.d.ts`, output); +} + +/** + * Creates types for all endpoint routes + * TODO: cover all routing cases (optional, typed and Rest parameters slugs) + * @param {string} str + * @returns {string} + */ +function fixSlugs(str) { + return str.replace(/\[.*?\]/g, '${string}').replace(/[\<].*?\>/g, '${string}'); +} + +/** + * There are some typing bugs with endpoints, for example : + * 1: 'endpoint method' extends Kit.EventHandler is falsy during runtime. + * 2: 'enpoint method' extends (...args: any) => any, if the endpoint is unpacking parameters in function head the returned type from fetch is TypedResponse + * as opposed to being correctly typed if instead the function head has 'event: RequestEvent' and then the unpacking is done in the function body + */ diff --git a/packages/kit/src/core/sync/write_types/index.js b/packages/kit/src/core/sync/write_types/index.js index d21030e01109..c872a337a4db 100644 --- a/packages/kit/src/core/sync/write_types/index.js +++ b/packages/kit/src/core/sync/write_types/index.js @@ -235,6 +235,11 @@ function update_types(config, routes, route, to_delete = new Set()) { ); } + const api_types_path = posixify( + path.relative(outdir, path.join(config.kit.outDir, 'types', '$api')) // TODO: potential failure point if api file is moved + ); + declarations.push(`type FetchType = typeof import('${api_types_path}').TypedFetch;`); + if (route.leaf) { let route_info = routes.get(route.leaf); if (!route_info) { @@ -263,10 +268,10 @@ function update_types(config, routes, route, to_delete = new Set()) { if (route.leaf.server) { exports.push( - 'export type Action | void = Record | void> = Kit.Action' + 'export type Action | void = Record | void> = Kit.Action' ); exports.push( - 'export type Actions | void = Record | void> = Kit.Actions' + 'export type Actions | void = Record | void> = Kit.Actions' ); } } @@ -339,7 +344,7 @@ function update_types(config, routes, route, to_delete = new Set()) { } if (route.leaf?.server || route.layout?.server || route.endpoint) { - exports.push('export type RequestEvent = Kit.RequestEvent;'); + exports.push('export type RequestEvent = Kit.RequestEvent;'); } const output = [imports.join('\n'), declarations.join('\n'), exports.join('\n')] @@ -398,7 +403,7 @@ function process_node(node, outdir, is_page, proxies, all_pages_have_load = true ? 'Partial & Record | void' : `OutputDataShape<${parent_type}>`; exports.push( - `export type ${prefix}ServerLoad = Kit.ServerLoad<${params}, ${parent_type}, OutputData, ${route_id}>;` + `export type ${prefix}ServerLoad = Kit.ServerLoad<${params}, ${parent_type}, OutputData, ${route_id}, FetchType>;` ); exports.push(`export type ${prefix}ServerLoadEvent = Parameters<${prefix}ServerLoad>[0];`); @@ -452,7 +457,7 @@ function process_node(node, outdir, is_page, proxies, all_pages_have_load = true ? 'Partial & Record | void' : `OutputDataShape<${parent_type}>`; exports.push( - `export type ${prefix}Load = Kit.Load<${params}, ${prefix}ServerData, ${parent_type}, OutputData, ${route_id}>;` + `export type ${prefix}Load = Kit.Load<${params}, ${prefix}ServerData, ${parent_type}, OutputData, ${route_id}, FetchType>;` ); exports.push(`export type ${prefix}LoadEvent = Parameters<${prefix}Load>[0];`); @@ -569,7 +574,7 @@ function path_to_original(outdir, file_path) { /** * @param {string} file_path */ -function replace_ext_with_js(file_path) { +export function replace_ext_with_js(file_path) { // Another extension than `.js` (or nothing, but that fails with node16 moduleResolution) // will result in TS failing to lookup the file const ext = path.extname(file_path); diff --git a/packages/kit/src/exports/index.js b/packages/kit/src/exports/index.js index 7be910b8074d..dd1cc75bf1f7 100644 --- a/packages/kit/src/exports/index.js +++ b/packages/kit/src/exports/index.js @@ -50,8 +50,10 @@ export function redirect(status, location) { /** * Create a JSON `Response` object from the supplied data. - * @param {any} data The value that will be serialized as JSON. + * @template {any} [T=any] + * @param {T} data The value that will be serialized as JSON. * @param {ResponseInit} [init] Options such as `status` and `headers` that will be added to the response. `Content-Type: application/json` and `Content-Length` headers will be added automatically. + * @returns {import('./public.js').TypedResponse} */ export function json(data, init) { // TODO deprecate this in favour of `Response.json` when it's diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index 77f15a391898..240eec5fe2ca 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -681,8 +681,11 @@ export type Load< InputData extends Record | null = Record | null, ParentData extends Record = Record, OutputData extends Record | void = Record | void, - RouteId extends string | null = string | null -> = (event: LoadEvent) => MaybePromise; + RouteId extends string | null = string | null, + FetchType extends typeof fetch = typeof fetch +> = ( + event: LoadEvent +) => MaybePromise; /** * The generic form of `PageLoadEvent` and `LayoutLoadEvent`. You should import those from `./$types` (see [generated types](https://kit.svelte.dev/docs/types#generated-types)) @@ -692,7 +695,8 @@ export interface LoadEvent< Params extends Partial> = Partial>, Data extends Record | null = Record | null, ParentData extends Record = Record, - RouteId extends string | null = string | null + RouteId extends string | null = string | null, + FetchType extends typeof fetch = typeof fetch > extends NavigationEvent { /** * `fetch` is equivalent to the [native `fetch` web API](https://developer.mozilla.org/en-US/docs/Web/API/fetch), with a few additional features: @@ -705,7 +709,7 @@ export interface LoadEvent< * * You can learn more about making credentialed requests with cookies [here](https://kit.svelte.dev/docs/load#cookies) */ - fetch: typeof fetch; + fetch: FetchType; /** * Contains the data returned by the route's server `load` function (in `+layout.server.js` or `+page.server.js`), if any. */ @@ -963,7 +967,8 @@ export type ParamMatcher = (param: string) => boolean; export interface RequestEvent< Params extends Partial> = Partial>, - RouteId extends string | null = string | null + RouteId extends string | null = string | null, + FetchType extends typeof fetch = typeof fetch > { /** * Get or set cookies related to the current request @@ -980,7 +985,7 @@ export interface RequestEvent< * * You can learn more about making credentialed requests with cookies [here](https://kit.svelte.dev/docs/load#cookies) */ - fetch: typeof fetch; + fetch: FetchType; /** * The client's IP address, set by the adapter. */ @@ -1055,8 +1060,9 @@ export interface RequestEvent< */ export type RequestHandler< Params extends Partial> = Partial>, - RouteId extends string | null = string | null -> = (event: RequestEvent) => MaybePromise; + RouteId extends string | null = string | null, + FetchType extends typeof fetch = typeof fetch +> = (event: RequestEvent) => MaybePromise; export interface ResolveOptions { /** @@ -1129,14 +1135,16 @@ export type ServerLoad< Params extends Partial> = Partial>, ParentData extends Record = Record, OutputData extends Record | void = Record | void, - RouteId extends string | null = string | null -> = (event: ServerLoadEvent) => MaybePromise; + RouteId extends string | null = string | null, + FetchType extends typeof fetch = typeof fetch +> = (event: ServerLoadEvent) => MaybePromise; export interface ServerLoadEvent< Params extends Partial> = Partial>, ParentData extends Record = Record, - RouteId extends string | null = string | null -> extends RequestEvent { + RouteId extends string | null = string | null, + FetchType extends typeof fetch = typeof fetch +> extends RequestEvent { /** * `await parent()` returns data from parent `+layout.server.js` `load` functions. * @@ -1190,8 +1198,9 @@ export interface ServerLoadEvent< export type Action< Params extends Partial> = Partial>, OutputData extends Record | void = Record | void, - RouteId extends string | null = string | null -> = (event: RequestEvent) => MaybePromise; + RouteId extends string | null = string | null, + FetchType extends typeof fetch = typeof fetch +> = (event: RequestEvent) => MaybePromise; /** * Shape of the `export const actions = {..}` object in `+page.server.js`. @@ -1200,8 +1209,9 @@ export type Action< export type Actions< Params extends Partial> = Partial>, OutputData extends Record | void = Record | void, - RouteId extends string | null = string | null -> = Record>; + RouteId extends string | null = string | null, + FetchType extends typeof fetch = typeof fetch +> = Record>; /** * When calling a form action via fetch, the response will be one of these shapes. @@ -1242,6 +1252,13 @@ export interface Redirect { location: string; } +/** + * The object returned by the typed json and typed fetch functions + */ +export interface TypedResponse extends Response { + json(): Promise; +} + export type SubmitFunction< Success extends Record | undefined = Record, Failure extends Record | undefined = Record From 3b79bccde40e25e6dbd9374873cbdc698ad9e013 Mon Sep 17 00:00:00 2001 From: difanta Date: Thu, 11 May 2023 13:39:44 +0200 Subject: [PATCH 2/2] Path generation for pages; path checking of redirect, goto, fetch; fetch type safety when fetching from endpoints --- .changeset/afraid-cougars-return.md | 5 + packages/kit/src/core/sync/sync.js | 4 - packages/kit/src/core/sync/write_api.js | 152 ---------- packages/kit/src/core/sync/write_tsconfig.js | 1 + .../kit/src/core/sync/write_tsconfig.spec.js | 1 + .../kit/src/core/sync/write_types/index.js | 10 +- .../test/{ => actions}/tsconfig.json | 4 +- .../test/layout-advanced/tsconfig.json | 19 ++ .../write_types/test/layout/tsconfig.json | 19 ++ .../tsconfig.json | 19 ++ .../simple-page-server-only/tsconfig.json | 19 ++ .../simple-page-shared-only/tsconfig.json | 19 ++ .../tsconfig.json | 19 ++ .../sync/write_types/test/slugs/tsconfig.json | 19 ++ .../core/sync/write_types/write_api/index.js | 263 ++++++++++++++++++ packages/kit/src/exports/index.js | 3 +- packages/kit/src/exports/public.d.ts | 43 ++- packages/kit/src/runtime/app/navigation.js | 5 +- packages/kit/src/types/ambient.d.ts | 7 +- .../test/apps/amp/src/routes/valid/+page.js | 6 +- .../apps/basics/src/routes/+layout.server.js | 1 + .../kit/test/apps/basics/src/routes/+page.js | 6 +- .../routes/load/change-detection/+layout.js | 1 + .../load/static-file-with-hash/+page.js | 2 +- .../apps/basics/src/routes/routing/b/+page.js | 2 +- .../routing/preloading/preloaded/+page.js | 4 +- .../test/apps/basics/src/routes/xss/+page.js | 1 + .../basics/src/routes/fetch-404/+page.js | 1 + 28 files changed, 482 insertions(+), 173 deletions(-) create mode 100644 .changeset/afraid-cougars-return.md delete mode 100644 packages/kit/src/core/sync/write_api.js rename packages/kit/src/core/sync/write_types/test/{ => actions}/tsconfig.json (75%) create mode 100644 packages/kit/src/core/sync/write_types/test/layout-advanced/tsconfig.json create mode 100644 packages/kit/src/core/sync/write_types/test/layout/tsconfig.json create mode 100644 packages/kit/src/core/sync/write_types/test/simple-page-server-and-shared/tsconfig.json create mode 100644 packages/kit/src/core/sync/write_types/test/simple-page-server-only/tsconfig.json create mode 100644 packages/kit/src/core/sync/write_types/test/simple-page-shared-only/tsconfig.json create mode 100644 packages/kit/src/core/sync/write_types/test/slugs-layout-not-all-pages-have-load/tsconfig.json create mode 100644 packages/kit/src/core/sync/write_types/test/slugs/tsconfig.json create mode 100644 packages/kit/src/core/sync/write_types/write_api/index.js diff --git a/.changeset/afraid-cougars-return.md b/.changeset/afraid-cougars-return.md new file mode 100644 index 000000000000..538756878144 --- /dev/null +++ b/.changeset/afraid-cougars-return.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': minor +--- + +feat: typed responses when fetching from endpoints diff --git a/packages/kit/src/core/sync/sync.js b/packages/kit/src/core/sync/sync.js index 92ad72cf442e..3d1f0f9e57e9 100644 --- a/packages/kit/src/core/sync/sync.js +++ b/packages/kit/src/core/sync/sync.js @@ -6,7 +6,6 @@ import { write_tsconfig } from './write_tsconfig.js'; import { write_types, write_all_types } from './write_types/index.js'; import { write_ambient } from './write_ambient.js'; import { write_server } from './write_server.js'; -import { write_api } from './write_api.js'; /** * Initialize SvelteKit's generated files. @@ -31,7 +30,6 @@ export async function create(config) { write_server(config, output); write_root(manifest_data, output); await write_all_types(config, manifest_data); - write_api(config, manifest_data); return { manifest_data }; } @@ -46,7 +44,6 @@ export async function create(config) { */ export async function update(config, manifest_data, file) { await write_types(config, manifest_data, file); - write_api(config, manifest_data); return { manifest_data }; } @@ -70,7 +67,6 @@ export async function all_types(config, mode) { init(config, mode); const manifest_data = create_manifest_data({ config }); await write_all_types(config, manifest_data); - write_api(config, manifest_data); } /** diff --git a/packages/kit/src/core/sync/write_api.js b/packages/kit/src/core/sync/write_api.js deleted file mode 100644 index 4a1c583adcc1..000000000000 --- a/packages/kit/src/core/sync/write_api.js +++ /dev/null @@ -1,152 +0,0 @@ -import fs from 'node:fs'; -import path from 'node:path'; -import { posixify } from '../../utils/filesystem.js'; -import { ts } from './ts.js'; -import { replace_ext_with_js } from './write_types/index.js'; - -/** - * Creates types for all endpoint routes - * @param {import('types').ValidatedConfig} config - * @param {import('types').ManifestData} manifest_data - */ -export function write_api(config, manifest_data) { - if (!ts) return; - - const types_dir = `${config.kit.outDir}/types`; - - try { - fs.mkdirSync(types_dir, { recursive: true }); - } catch {} - - const api_imports = [`import type * as Kit from '@sveltejs/kit';`]; - - /** @type {string[]} */ - const api_declarations = []; - - api_declarations.push( - `export interface TypedRequestInit extends RequestInit { \n\tmethod: Method; \n}` - ); - - api_declarations.push( - `type GetEndpointType any> = Awaited> extends Kit.TypedResponse ? T : never;` - ); - - api_declarations.push( - `type ExpandMethods = { [Method in keyof T]: T[Method] extends (...args: any) => any ? GetEndpointType : never; };` - ); - - api_declarations.push( - `type optional_trailing = \`\` | \`/\` | \`/?\${string}\` | \`?\${string}\` ` - ); - - /** @type {string[]} */ - const api_endpoints = ['export type Endpoints = {']; - - let index = 0; - - for (const route of manifest_data.routes) { - if (route.endpoint) { - const route_import_path = posixify( - path.relative(types_dir, replace_ext_with_js(route.endpoint.file)) - ); - api_endpoints.push(`\t${index}: { `); - api_endpoints.push(`\t\tpath: \`${fixSlugs(posixify(route.id))}\${optional_trailing}\`; `); - api_endpoints.push(`\t\tmethods: ExpandMethods`); - api_endpoints.push(`\t};`); - ++index; - } - } - - api_endpoints.push('};'); - - /** @type {string[]} */ - const api_utility_types = []; - - api_utility_types.push(`type EndpointsPaths = Endpoints[keyof Endpoints]["path"];`); - - api_utility_types.push(`type MatchedPaths = { - [Index in keyof Endpoints as S extends Endpoints[Index]["path"] - ? "matched" - : never]: Endpoints[Index]; - };`); - - api_utility_types.push( - `type ExtractMethodsFromMatched = T extends { matched: { methods: infer K } } ? K : {};` - ); - - api_utility_types.push( - `type ValidMethod = string & keyof ExtractMethodsFromMatched>;` - ); - - api_utility_types.push( - `type TypedResponseFromPath> = Kit.TypedResponse>[Method]>;` - ); - - /** @type {string[]} */ - const api_exports = []; - - api_exports.push( - `export function TypedFetch< - S extends EndpointsPaths, - Method extends ValidMethod - >( - input: S, - init: TypedRequestInit - ): Promise>;` - ); - - api_exports.push( - `export function TypedFetch< - S extends EndpointsPaths, - Method extends ValidMethod = "GET" extends ValidMethod ? "GET" : ValidMethod - >( - input: S, - ...init: "GET" extends ValidMethod ? [init?: TypedRequestInit] : [init: TypedRequestInit] - ): Promise>;` - ); - - /* - // weaker typed option (you can fetch any url without type info if outside of TypedFetch scope) - // this leads to little to no intellisense while writing a typed fetch call because the standard fetch absorbs all partial inputs like { method: ""} - api_exports.push(`export function TypedFetch( - input: URL | RequestInfo, - init?: RequestInit - ): Promise; - `);*/ - - // stronger typed but more opinionated option (you cannot fetch from urls that match enpoints without going through TypedFetch typing) - // this provides strong intellisense on both urls and methods, and informative error when fetching from a endpoint wihtout a default "GET" - api_exports.push(`export function TypedFetch( - input: S extends EndpointsPaths ? {error: \`method is required for endpoint \${S} without GET handler\`, available_methods: \` \${ValidMethod} \`} : S | URL | RequestInfo, - init?: RequestInit - ): Promise;`); - - const output = [ - api_imports.join('\n'), - api_declarations.join('\n'), - api_endpoints.join('\n'), - api_utility_types.join('\n'), - api_exports.join('\n\n') - ] - .filter(Boolean) - .join('\n\n'); - - fs.writeFileSync(`${types_dir}/$api.d.ts`, output); -} - -/** - * Creates types for all endpoint routes - * TODO: cover all routing cases (optional, typed and Rest parameters slugs) - * @param {string} str - * @returns {string} - */ -function fixSlugs(str) { - return str.replace(/\[.*?\]/g, '${string}').replace(/[\<].*?\>/g, '${string}'); -} - -/** - * There are some typing bugs with endpoints, for example : - * 1: 'endpoint method' extends Kit.EventHandler is falsy during runtime. - * 2: 'enpoint method' extends (...args: any) => any, if the endpoint is unpacking parameters in function head the returned type from fetch is TypedResponse - * as opposed to being correctly typed if instead the function head has 'event: RequestEvent' and then the unpacking is done in the function body - */ diff --git a/packages/kit/src/core/sync/write_tsconfig.js b/packages/kit/src/core/sync/write_tsconfig.js index 33aa98adb18e..3fe958c000a7 100644 --- a/packages/kit/src/core/sync/write_tsconfig.js +++ b/packages/kit/src/core/sync/write_tsconfig.js @@ -88,6 +88,7 @@ export function get_tsconfig(kit, include_base_url) { const include = new Set([ 'ambient.d.ts', './types/**/$types.d.ts', + './types/$api.d.ts', config_relative('vite.config.js'), config_relative('vite.config.ts') ]); diff --git a/packages/kit/src/core/sync/write_tsconfig.spec.js b/packages/kit/src/core/sync/write_tsconfig.spec.js index 6efa9368791e..62248a5d16bb 100644 --- a/packages/kit/src/core/sync/write_tsconfig.spec.js +++ b/packages/kit/src/core/sync/write_tsconfig.spec.js @@ -99,6 +99,7 @@ test('Creates tsconfig include from kit.files', () => { expect(include).toEqual([ 'ambient.d.ts', './types/**/$types.d.ts', + './types/$api.d.ts', '../vite.config.js', '../vite.config.ts', '../app/**/*.js', diff --git a/packages/kit/src/core/sync/write_types/index.js b/packages/kit/src/core/sync/write_types/index.js index c872a337a4db..85b6a2a321ee 100644 --- a/packages/kit/src/core/sync/write_types/index.js +++ b/packages/kit/src/core/sync/write_types/index.js @@ -4,6 +4,7 @@ import MagicString from 'magic-string'; import { posixify, rimraf, walk } from '../../../utils/filesystem.js'; import { compact } from '../../../utils/array.js'; import { ts } from '../ts.js'; +import { write_api } from './write_api/index.js'; /** * @typedef {{ @@ -123,6 +124,8 @@ export async function write_all_types(config, manifest_data) { } } + write_api(config, manifest_data); + fs.writeFileSync(meta_data_file, JSON.stringify(meta_data, null, '\t')); } @@ -148,6 +151,7 @@ export async function write_types(config, manifest_data, file) { if (!route.leaf && !route.layout && !route.endpoint) return; // nothing to do update_types(config, create_routes_map(manifest_data), route); + write_api(config, manifest_data); } /** @@ -238,7 +242,7 @@ function update_types(config, routes, route, to_delete = new Set()) { const api_types_path = posixify( path.relative(outdir, path.join(config.kit.outDir, 'types', '$api')) // TODO: potential failure point if api file is moved ); - declarations.push(`type FetchType = typeof import('${api_types_path}').TypedFetch;`); + declarations.push(`type FetchType = typeof import('${api_types_path}').fetch;`); if (route.leaf) { let route_info = routes.get(route.leaf); @@ -340,7 +344,9 @@ function update_types(config, routes, route, to_delete = new Set()) { } if (route.endpoint) { - exports.push('export type RequestHandler = Kit.RequestHandler;'); + exports.push( + 'export type RequestHandler = Kit.RequestHandler;' + ); } if (route.leaf?.server || route.layout?.server || route.endpoint) { diff --git a/packages/kit/src/core/sync/write_types/test/tsconfig.json b/packages/kit/src/core/sync/write_types/test/actions/tsconfig.json similarity index 75% rename from packages/kit/src/core/sync/write_types/test/tsconfig.json rename to packages/kit/src/core/sync/write_types/test/actions/tsconfig.json index fc3cf322453b..7a56d7920357 100644 --- a/packages/kit/src/core/sync/write_types/test/tsconfig.json +++ b/packages/kit/src/core/sync/write_types/test/actions/tsconfig.json @@ -10,8 +10,8 @@ "allowSyntheticDefaultImports": true, "baseUrl": ".", "paths": { - "@sveltejs/kit": ["../../../../exports/public"], - "types": ["../../../../types/internal"] + "@sveltejs/kit": ["../../../../../exports/public"], + "types": ["../../../../../types/internal"] } }, "include": ["./**/*.js"], diff --git a/packages/kit/src/core/sync/write_types/test/layout-advanced/tsconfig.json b/packages/kit/src/core/sync/write_types/test/layout-advanced/tsconfig.json new file mode 100644 index 000000000000..7a56d7920357 --- /dev/null +++ b/packages/kit/src/core/sync/write_types/test/layout-advanced/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "noEmit": true, + "strict": true, + "target": "es2020", + "module": "es2022", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "baseUrl": ".", + "paths": { + "@sveltejs/kit": ["../../../../../exports/public"], + "types": ["../../../../../types/internal"] + } + }, + "include": ["./**/*.js"], + "exclude": ["./**/.svelte-kit/**"] +} diff --git a/packages/kit/src/core/sync/write_types/test/layout/tsconfig.json b/packages/kit/src/core/sync/write_types/test/layout/tsconfig.json new file mode 100644 index 000000000000..7a56d7920357 --- /dev/null +++ b/packages/kit/src/core/sync/write_types/test/layout/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "noEmit": true, + "strict": true, + "target": "es2020", + "module": "es2022", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "baseUrl": ".", + "paths": { + "@sveltejs/kit": ["../../../../../exports/public"], + "types": ["../../../../../types/internal"] + } + }, + "include": ["./**/*.js"], + "exclude": ["./**/.svelte-kit/**"] +} diff --git a/packages/kit/src/core/sync/write_types/test/simple-page-server-and-shared/tsconfig.json b/packages/kit/src/core/sync/write_types/test/simple-page-server-and-shared/tsconfig.json new file mode 100644 index 000000000000..7a56d7920357 --- /dev/null +++ b/packages/kit/src/core/sync/write_types/test/simple-page-server-and-shared/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "noEmit": true, + "strict": true, + "target": "es2020", + "module": "es2022", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "baseUrl": ".", + "paths": { + "@sveltejs/kit": ["../../../../../exports/public"], + "types": ["../../../../../types/internal"] + } + }, + "include": ["./**/*.js"], + "exclude": ["./**/.svelte-kit/**"] +} diff --git a/packages/kit/src/core/sync/write_types/test/simple-page-server-only/tsconfig.json b/packages/kit/src/core/sync/write_types/test/simple-page-server-only/tsconfig.json new file mode 100644 index 000000000000..7a56d7920357 --- /dev/null +++ b/packages/kit/src/core/sync/write_types/test/simple-page-server-only/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "noEmit": true, + "strict": true, + "target": "es2020", + "module": "es2022", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "baseUrl": ".", + "paths": { + "@sveltejs/kit": ["../../../../../exports/public"], + "types": ["../../../../../types/internal"] + } + }, + "include": ["./**/*.js"], + "exclude": ["./**/.svelte-kit/**"] +} diff --git a/packages/kit/src/core/sync/write_types/test/simple-page-shared-only/tsconfig.json b/packages/kit/src/core/sync/write_types/test/simple-page-shared-only/tsconfig.json new file mode 100644 index 000000000000..7a56d7920357 --- /dev/null +++ b/packages/kit/src/core/sync/write_types/test/simple-page-shared-only/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "noEmit": true, + "strict": true, + "target": "es2020", + "module": "es2022", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "baseUrl": ".", + "paths": { + "@sveltejs/kit": ["../../../../../exports/public"], + "types": ["../../../../../types/internal"] + } + }, + "include": ["./**/*.js"], + "exclude": ["./**/.svelte-kit/**"] +} diff --git a/packages/kit/src/core/sync/write_types/test/slugs-layout-not-all-pages-have-load/tsconfig.json b/packages/kit/src/core/sync/write_types/test/slugs-layout-not-all-pages-have-load/tsconfig.json new file mode 100644 index 000000000000..7a56d7920357 --- /dev/null +++ b/packages/kit/src/core/sync/write_types/test/slugs-layout-not-all-pages-have-load/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "noEmit": true, + "strict": true, + "target": "es2020", + "module": "es2022", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "baseUrl": ".", + "paths": { + "@sveltejs/kit": ["../../../../../exports/public"], + "types": ["../../../../../types/internal"] + } + }, + "include": ["./**/*.js"], + "exclude": ["./**/.svelte-kit/**"] +} diff --git a/packages/kit/src/core/sync/write_types/test/slugs/tsconfig.json b/packages/kit/src/core/sync/write_types/test/slugs/tsconfig.json new file mode 100644 index 000000000000..7a56d7920357 --- /dev/null +++ b/packages/kit/src/core/sync/write_types/test/slugs/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "noEmit": true, + "strict": true, + "target": "es2020", + "module": "es2022", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "baseUrl": ".", + "paths": { + "@sveltejs/kit": ["../../../../../exports/public"], + "types": ["../../../../../types/internal"] + } + }, + "include": ["./**/*.js"], + "exclude": ["./**/.svelte-kit/**"] +} diff --git a/packages/kit/src/core/sync/write_types/write_api/index.js b/packages/kit/src/core/sync/write_types/write_api/index.js new file mode 100644 index 000000000000..bbfe64322138 --- /dev/null +++ b/packages/kit/src/core/sync/write_types/write_api/index.js @@ -0,0 +1,263 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { posixify } from '../../../../utils/filesystem.js'; +import { ts } from '../../ts.js'; +import { replace_ext_with_js } from '../index.js'; + +/** + * Creates types for all endpoint routes + * @param {import('types').ValidatedConfig} config + * @param {import('types').ManifestData} manifest_data + */ +export function write_api(config, manifest_data) { + if (!ts) return; + + const types_dir = `${config.kit.outDir}/types`; + + try { + fs.mkdirSync(types_dir, { recursive: true }); + } catch {} + + const api_imports = ['import type * as Kit from "@sveltejs/kit";']; + + /** @type {string[]} */ + const api_declarations = []; + + api_declarations.push("declare module '__sveltekit/paths' {"); + api_declarations.push( + `\tinterface paths { \n\t\tbase: '${config.kit.paths.base}', \n\t\tassets: '${config.kit.paths.assets}'\n\t}` + ); + api_declarations.push('}'); + + api_declarations.push( + 'interface TypedRequestInit extends RequestInit { \n\tmethod?: Method; \n}' + ); + api_declarations.push( + 'interface TypedRequestInitRequired extends RequestInit { \n\tmethod: Method; \n}' + ); + api_declarations.push( + 'type GetEndpointType any> = Awaited> extends Kit.TypedResponse ? Kit.Jsonify : never;' + ); + api_declarations.push( + 'type ExpandMethods = { [Method in keyof T]: T[Method] extends (...args: any) => any ? GetEndpointType : never; };' + ); + api_declarations.push('type optional_trailing = `` | `?${string}`;'); + api_declarations.push('type restricted_characters = ` ` | `/` | `\\\\`;'); + api_declarations.push('type restricted_characters_rest = ` ` | `\\\\`;'); + api_declarations.push( + 'type RemoveTrailingSlash = S extends "/" ? S : S extends `${infer _S}/` ? _S : S;' + ); + api_declarations.push( + `\n/** Check if is not empty, and does not contain restricted characters */ \ntype ValidateRequired = FixDynamicStr extends \`/\${infer Slug}\` ? + Slug extends ('' | \`\${string}\${restricted_characters}\${string}\`) ? + false + : true + : false;\n` + ); + api_declarations.push( + `/** Check if does not contain restricted characters. If the check fails, \n * check if it has a starting slash and take the content after it as a slug parameter and validate it */ \ntype ValidateOptional = FixDynamicStr extends \`/\${infer Slug}\` ? + Slug extends (\`\${string}\${restricted_characters}\${string}\`) ? + false + : true + : FixDynamicStr extends '' ? + true + : false;\n` + ); + api_declarations.push( + `/** Almost no check, just exclude restricted characters */ \ntype ValidateRest = FixDynamicStr extends \`/\${infer Slug}\` ? + Slug extends (\`\${string}\${restricted_characters_rest}\${string}\`) ? + false + : true + : FixDynamicStr extends '' ? + true + : false;\n` + ); + + api_declarations.push( + `\n/** Check if is empty or starts with '#' or '?' and does not contain restricted characters */ \ntype ValidateTrailing = FixDynamicTrailing extends '' | \`?\${infer Slug}\` | \`#\${infer Slug}\` ? + Slug extends (\`\${string}\${restricted_characters}\${string}\`) ? + false + : true + : false;\n` + ); + api_declarations.push( + 'type UnionAllTrue = [B] extends [true] ? true : false;' + ); + api_declarations.push( + 'type FixDynamicStr = S extends undefined | null ? "" : Kit.Equals extends true ? `/${string}` : S;' + ); + api_declarations.push( + 'type FixDynamicTrailing = S extends undefined | null ? "" : Kit.Equals extends true ? `?${string}` : S;' + ); + + /** @type {string[]} */ + const api_endpoints = [ + 'declare module "@sveltejs/kit" {', + '\tinterface ValidURLs {' + ]; + + let index = 1; + + for (const route of manifest_data.routes) { + if (route.endpoint || route.leaf) { + const { matcher_str, validator_str } = parseSlugs(route.id); + api_endpoints.push(`\t\t${index}: { `); + api_endpoints.push(`\t\t\tid: \`${route.id}\`; `); + api_endpoints.push( + `\t\t\tdoes_match: RemoveTrailingSlash extends \`${config.kit.paths.base}${matcher_str}\` ? ${validator_str} : false; ` + ); + if (route.endpoint) { + const route_import_path = posixify( + path.relative(types_dir, replace_ext_with_js(route.endpoint.file)) + ); + api_endpoints.push(`\t\t\tmethods: ExpandMethods`); + api_endpoints.push('\t\t\tendpoint: true'); + } else { + api_endpoints.push('\t\t\tendpoint: false'); + } + if (route.leaf) api_endpoints.push('\t\t\tleaf: true'); + else api_endpoints.push('\t\t\tleaf: false'); + api_endpoints.push('\t\t};'); + ++index; + } + } + + api_endpoints.push('\t}'); + api_endpoints.push('}'); + + /** @type {string[]} */ + const api_utility_types = []; + + api_utility_types.push(`type MatchedEndpoints = { + [Index in keyof Kit.ValidURLs as Kit.ValidURLs[Index]["does_match"] extends true + ? Kit.ValidURLs[Index]["endpoint"] extends true + ? "matched" : never : never]: Kit.ValidURLs[Index]; + };`); + + api_utility_types.push( + 'type ExtractMethodsFromMatched = T extends { matched: { methods: infer K } } ? K : {};' + ); + + api_utility_types.push( + "type ExtractIdFromMatched = T extends { matched: { id: infer K } } ? K extends string ? K : '' : '';" + ); + + api_utility_types.push( + 'type ValidMethod = string & keyof ExtractMethodsFromMatched>;' + ); + + api_utility_types.push( + 'type TypedResponseFromPath> = Kit.TypedResponse>[Method], true> | Kit.TypedResponse;' + ); + + /** @type {string[]} */ + const api_exports = []; + + api_exports.push( + `export declare function fetch< + S, + Method extends ValidMethod = "GET" & ValidMethod + >( + input: S extends string ? Kit.IsRelativePath extends true ? MatchedEndpoints extends { matched: any } ? S : \`no matched endpoints with id: \${S}\` : never : never, + ...init: "GET" extends ValidMethod ? [init?: TypedRequestInit>] : [init: TypedRequestInitRequired] + ): Promise>;` + ); + + api_exports.push(`export declare function fetch( + input: S extends string ? Kit.IsRelativePath extends true ? MatchedEndpoints extends { matched: any } ? \`invalid method for endpoint: \${ExtractIdFromMatched>}, available_methods: \${ValidMethod} \` : Kit.Equals extends true ? S : \`no matched endpoints with id: \${S}\` : URL | RequestInfo : URL | RequestInfo, + init?: RequestInit + ): Promise;`); + + api_exports.push( + `declare global { + function fetch< + S, + Method extends ValidMethod = "GET" & ValidMethod + >( + input: S extends string ? Kit.IsRelativePath extends true ? MatchedEndpoints extends { matched: any } ? S : \`no matched endpoints with id: \${S}\` : never : never, + ...init: "GET" extends ValidMethod ? [init?: TypedRequestInit>] : [init: TypedRequestInitRequired] + ): Promise>; +}` + ); + + const output = [ + api_imports.join('\n'), + api_declarations.join('\n'), + api_endpoints.join('\n'), + api_utility_types.join('\n'), + api_exports.join('\n\n') + ] + .filter(Boolean) + .join('\n\n'); + + fs.writeFileSync(`${types_dir}/$api.d.ts`, output); +} + +/** + * Creates types for all endpoint routes + * TODO: cover all routing cases (optional, typed and Rest parameters slugs) + * @param {string} str + */ +function parseSlugs(str) { + let changed; + let index = 0; + const validators = []; + do { + changed = false; + if (str.search(/\(.*?\)\/?/) !== -1) { + changed = true; + str = str.replace(/\(.*?\)\/?/, ''); + } + if (str.search(/\/?\[\[.*?\]\]/) !== -1) { + changed = true; + str = str.replace(/\/?\[\[.*?\]\]/, `\${infer OptionalSlug${index}}`); + validators.push(`ValidateOptional`); + } + if (str.search(/\/?<<.*?>>/) !== -1) { + changed = true; + str = str.replace(/\/?<<.*?>>/, `\${infer OptionalSlug${index}}`); + validators.push(`ValidateOptional`); + } + if (str.search(/\/?\/?\[\.\.\..*?\]/) !== -1) { + changed = true; + str = str.replace(/\/?\[\.\.\..*?\]/, `\${infer RestSlug${index}}`); + validators.push(`ValidateRest`); + } + if (str.search(/\/?<\.\.\..*?>/) !== -1) { + changed = true; + str = str.replace(/\/?<\.\.\..*?>/, `\${infer RestSlug${index}}`); + validators.push(`ValidateRest`); + } + if (str.search(/\[.*?\]/) !== -1) { + changed = true; + str = str.replace(/\/?\[.*?\]/, `\${infer RequiredSlug${index}}`); + validators.push(`ValidateRequired`); + } + if (str.search(/\/?<.*?>/) !== -1) { + changed = true; + str = str.replace(/\/?<.*?>/, `\${infer RequiredSlug${index}}`); + validators.push(`ValidateRequired`); + } + } while (changed); + if (!endsWithSlug(str)) { + str += '${infer Trailing}'; + validators.push('ValidateTrailing'); + } + const validator_str = + validators.length > 0 + ? `UnionAllTrue<${validators.length > 1 ? validators.join(' | ') : validators.join('')}>` + : 'true'; + + return { + matcher_str: str, + validator_str + }; +} + +/** + * + * @param {string} str + */ +function endsWithSlug(str) { + return str.endsWith('}'); +} diff --git a/packages/kit/src/exports/index.js b/packages/kit/src/exports/index.js index dd1cc75bf1f7..52a33998be8f 100644 --- a/packages/kit/src/exports/index.js +++ b/packages/kit/src/exports/index.js @@ -37,8 +37,9 @@ export function error(status, body) { /** * Create a `Redirect` object. If thrown during request handling, SvelteKit will return a redirect response. * Make sure you're not catching the thrown redirect, which would prevent SvelteKit from handling it. + * @template S * @param {300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308} status The [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#redirection_messages). Must be in the range 300-308. - * @param {string | URL} location The location to redirect to. + * @param {import('@sveltejs/kit').IsRelativePath extends true ? import('@sveltejs/kit').MatchedPaths extends { matched: any } ? S & string : import('@sveltejs/kit').Equals extends true ? S & string : `no matched path with id: ${S & string}` : string} location The location to redirect to. */ export function redirect(status, location) { if ((!BROWSER || DEV) && (isNaN(status) || status < 300 || status > 308)) { diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index 240eec5fe2ca..e6f42638b111 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -24,6 +24,46 @@ import type { PluginOptions } from '@sveltejs/vite-plugin-svelte'; export { PrerenderOption } from '../types/private.js'; export { ActionFailure }; +export interface ValidURLs { + 0: { + id: string; + does_match: ToCheck extends '' ? false : false; + methods: never; + endpoint: false; + leaf: false; + }; +} + +export type MatchedLeafs = { + [Index in keyof ValidURLs as ValidURLs[Index]['does_match'] extends true + ? ValidURLs[Index]['leaf'] extends true + ? 'matched' + : never + : never]: ValidURLs[Index]; +}; + +export type MatchedPaths = { + [Index in keyof ValidURLs as ValidURLs[Index]['does_match'] extends true + ? 'matched' + : never]: ValidURLs[Index]; +}; + +export type Jsonify = T extends { toJSON(): infer U } + ? U + : T extends object + ? { [k in keyof T]: Jsonify } + : T; + +export type Equals = (() => T extends X ? 1 : 2) extends () => T extends Y ? 1 : 2 + ? true + : false; + +export type IsRelativePath = S extends string + ? S extends `/${string}` | '' + ? true + : false + : false; + /** * [Adapters](https://kit.svelte.dev/docs/adapters) are responsible for taking the production build and turning it into something that can be deployed to a platform of your choosing. */ @@ -1255,7 +1295,8 @@ export interface Redirect { /** * The object returned by the typed json and typed fetch functions */ -export interface TypedResponse extends Response { +export interface TypedResponse extends Response { + ok: Ok; json(): Promise; } diff --git a/packages/kit/src/runtime/app/navigation.js b/packages/kit/src/runtime/app/navigation.js index 0da8f93e1599..aa9536f0e034 100644 --- a/packages/kit/src/runtime/app/navigation.js +++ b/packages/kit/src/runtime/app/navigation.js @@ -11,14 +11,15 @@ export const disableScrollHandling = /* @__PURE__ */ client_method('disable_scro * Returns a Promise that resolves when SvelteKit navigates (or fails to navigate, in which case the promise rejects) to the specified `url`. * For external URLs, use `window.location = url` instead of calling `goto(url)`. * - * @type {(url: string | URL, opts?: { + * @type {(url: S extends string ? import('@sveltejs/kit').MatchedLeafs extends { matched: any } ? S : import('@sveltejs/kit').Equals extends true ? S : `no matched routes with id: ${S}` : string | URL, opts?: { * replaceState?: boolean; * noScroll?: boolean; * keepFocus?: boolean; * invalidateAll?: boolean; * state?: any * }) => Promise} - * @param {string | URL} url Where to navigate to. Note that if you've set [`config.kit.paths.base`](https://kit.svelte.dev/docs/configuration#paths) and the URL is root-relative, you need to prepend the base path if you want to navigate within the app. + * @template S + * @param {S extends string ? import('@sveltejs/kit').MatchedLeafs extends { matched: any } ? S : import('@sveltejs/kit').Equals extends true ? S : { error: `no matched routes with id: ${S}` } : string | URL} url Where to navigate to. Note that if you've set [`config.kit.paths.base`](https://kit.svelte.dev/docs/configuration#paths) and the URL is root-relative, you need to prepend the base path if you want to navigate within the app. * @param {Object} [opts] Options related to the navigation * @param {boolean} [opts.replaceState] If `true`, will replace the current `history` entry rather than creating a new one with `pushState` * @param {boolean} [opts.noScroll] If `true`, the browser will maintain its scroll position rather than scrolling to the top of the page after navigation diff --git a/packages/kit/src/types/ambient.d.ts b/packages/kit/src/types/ambient.d.ts index b7331402a369..81913f034199 100644 --- a/packages/kit/src/types/ambient.d.ts +++ b/packages/kit/src/types/ambient.d.ts @@ -89,18 +89,21 @@ declare module '__sveltekit/environment' { /** Internal version of $app/paths */ declare module '__sveltekit/paths' { + interface paths {} /** * A string that matches [`config.kit.paths.base`](https://kit.svelte.dev/docs/configuration#paths). * * Example usage: `Link` */ - export let base: '' | `/${string}`; + export let base: paths extends { base: infer T } ? T : '' | `/${string}`; /** * An absolute path that matches [`config.kit.paths.assets`](https://kit.svelte.dev/docs/configuration#paths). * * > If a value for `config.kit.paths.assets` is specified, it will be replaced with `'/_svelte_kit_assets'` during `vite dev` or `vite preview`, since the assets don't yet live at their eventual URL. */ - export let assets: '' | `https://${string}` | `http://${string}` | '/_svelte_kit_assets'; + export let assets: paths extends { assets: infer T } + ? T + : '' | `https://${string}` | `http://${string}` | '/_svelte_kit_assets'; export let relative: boolean | undefined; // TODO in 2.0, make this a `boolean` that defaults to `true` export function reset(): void; export function override(paths: { base: string; assets: string }): void; diff --git a/packages/kit/test/apps/amp/src/routes/valid/+page.js b/packages/kit/test/apps/amp/src/routes/valid/+page.js index aaf112e513dc..ba609862edeb 100644 --- a/packages/kit/test/apps/amp/src/routes/valid/+page.js +++ b/packages/kit/test/apps/amp/src/routes/valid/+page.js @@ -1,6 +1,8 @@ /** @type {import('@sveltejs/kit').Load} */ export async function load({ fetch }) { const res = await fetch('/valid.json'); - const { answer } = await res.json(); - return { answer }; + if (res.ok) { + const { answer } = await res.json(); + return { answer }; + } } diff --git a/packages/kit/test/apps/basics/src/routes/+layout.server.js b/packages/kit/test/apps/basics/src/routes/+layout.server.js index 5700a931f816..652eeb12c705 100644 --- a/packages/kit/test/apps/basics/src/routes/+layout.server.js +++ b/packages/kit/test/apps/basics/src/routes/+layout.server.js @@ -18,6 +18,7 @@ export async function load({ cookies, locals, fetch }) { } if (locals.url?.pathname === '/non-existent-route-loop') { + // @ts-expect-error non existent route await fetch('/non-existent-route-loop'); } diff --git a/packages/kit/test/apps/basics/src/routes/+page.js b/packages/kit/test/apps/basics/src/routes/+page.js index b3c5e181aac7..331040d0ff9c 100644 --- a/packages/kit/test/apps/basics/src/routes/+page.js +++ b/packages/kit/test/apps/basics/src/routes/+page.js @@ -1,6 +1,8 @@ /** @type {import('@sveltejs/kit').Load}*/ export async function load({ fetch }) { const res = await fetch('/answer.json'); - const { answer } = await res.json(); - return { answer }; + if (res.ok) { + const { answer } = await res.json(); + return { answer }; + } } diff --git a/packages/kit/test/apps/basics/src/routes/load/change-detection/+layout.js b/packages/kit/test/apps/basics/src/routes/load/change-detection/+layout.js index 09db5461edd6..84f74853cbd2 100644 --- a/packages/kit/test/apps/basics/src/routes/load/change-detection/+layout.js +++ b/packages/kit/test/apps/basics/src/routes/load/change-detection/+layout.js @@ -3,6 +3,7 @@ let count = 0; /** @type {import('@sveltejs/kit').Load} */ export async function load({ fetch, depends }) { const res = await fetch('/load/change-detection/data.json'); + if (!res.ok) throw new Error('Failed to fetch data.json'); const { type } = await res.json(); count += 1; diff --git a/packages/kit/test/apps/basics/src/routes/load/static-file-with-hash/+page.js b/packages/kit/test/apps/basics/src/routes/load/static-file-with-hash/+page.js index df92a5908cf0..14b75cc1dbe1 100644 --- a/packages/kit/test/apps/basics/src/routes/load/static-file-with-hash/+page.js +++ b/packages/kit/test/apps/basics/src/routes/load/static-file-with-hash/+page.js @@ -1,7 +1,7 @@ /** @type {import('./$types').PageLoad} */ export async function load({ fetch }) { + // @ts-ignore, path does not exist what is this?? const res = await fetch('/load/assets/a#b.txt'); - return { status: res.status }; diff --git a/packages/kit/test/apps/basics/src/routes/routing/b/+page.js b/packages/kit/test/apps/basics/src/routes/routing/b/+page.js index 695fb251b466..791064b9348f 100644 --- a/packages/kit/test/apps/basics/src/routes/routing/b/+page.js +++ b/packages/kit/test/apps/basics/src/routes/routing/b/+page.js @@ -1,5 +1,5 @@ /** @type {import('@sveltejs/kit').Load} */ export async function load({ fetch }) { - const letter = await fetch('/routing/b.json').then((r) => r.json()); + const letter = await fetch('/routing/b.json').then((r) => (r.ok ? r.json() : '')); return { letter }; } diff --git a/packages/kit/test/apps/basics/src/routes/routing/preloading/preloaded/+page.js b/packages/kit/test/apps/basics/src/routes/routing/preloading/preloaded/+page.js index 5d7154c4dc7e..098d7985da09 100644 --- a/packages/kit/test/apps/basics/src/routes/routing/preloading/preloaded/+page.js +++ b/packages/kit/test/apps/basics/src/routes/routing/preloading/preloaded/+page.js @@ -1,5 +1,7 @@ /** @type {import('@sveltejs/kit').Load} */ export async function load({ fetch }) { - const message = await fetch('/routing/preloading/preloaded.json').then((r) => r.json()); + const message = await fetch('/routing/preloading/preloaded.json').then((r) => + r.ok ? r.json() : '' + ); return { message }; } diff --git a/packages/kit/test/apps/basics/src/routes/xss/+page.js b/packages/kit/test/apps/basics/src/routes/xss/+page.js index bebc49e0026f..3bb366a5aec1 100644 --- a/packages/kit/test/apps/basics/src/routes/xss/+page.js +++ b/packages/kit/test/apps/basics/src/routes/xss/+page.js @@ -1,6 +1,7 @@ /** @type {import('@sveltejs/kit').Load} */ export async function load({ fetch }) { const res = await fetch('/xss.json'); + if (!res.ok) throw new Error('Error fetching /xss.json'); const user = await res.json(); return { user }; } diff --git a/packages/kit/test/prerendering/basics/src/routes/fetch-404/+page.js b/packages/kit/test/prerendering/basics/src/routes/fetch-404/+page.js index 56d013d62144..70d3000a3818 100644 --- a/packages/kit/test/prerendering/basics/src/routes/fetch-404/+page.js +++ b/packages/kit/test/prerendering/basics/src/routes/fetch-404/+page.js @@ -1,5 +1,6 @@ /** @type {import('./$types').PageLoad} */ export async function load({ fetch }) { + // @ts-expect-error: non existing path to evoke 404 const { status } = await fetch('/missing.json'); return { status };