diff --git a/packages/kit/src/core/sync/write_types/index.js b/packages/kit/src/core/sync/write_types/index.js index d21030e01109..3ca04adc3f14 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 { dedent } from '../utils.js'; /** * @typedef {{ @@ -335,7 +336,41 @@ function update_types(config, routes, route, to_delete = new Set()) { } if (route.endpoint) { + // get the from/to for the proxy + const from = path_to_original(outdir, route.endpoint.file); + // We have to find out which methods are available/exported on the endpoint. + const proxy = createProxy(route.endpoint.file, 'endpoint'); + + if(proxy) { + // For each exported method, we create a type that represents the function signature. + exports.push('export type APIReturnType = T extends (...args: any[]) => Promise> ? Promise : never;'); + + exports.push(dedent` + declare module '$api' { + ${proxy.exports.map((method) => dedent` + export function api_fetch( + url: RouteId, + options: FetchOptions + ): APIReturnType;` + ).join('\n')} + }`); + } + + exports.push('export type RequestHandler = Kit.RequestHandler;'); + exports.push(dedent` + interface TypedResponse extends Response { + json(): Promise; + } + type Methods = "GET" | "POST" | "PUT" | "DELETE" | "HEAD" | "OPTIONS" | "PATCH"; + type FetchOptions< + Params = never, + Method extends Methods = Methods + > = Parameters[1] & { + params: Params, + method: Method + } + `) } if (route.leaf?.server || route.layout?.server || route.endpoint) { @@ -502,21 +537,21 @@ function process_node(node, outdir, is_page, proxies, all_pages_have_load = true */ function ensureProxies(node, proxies) { if (node.server && !proxies.server) { - proxies.server = createProxy(node.server, true); + proxies.server = createProxy(node.server, 'page.server'); } if (node.universal && !proxies.universal) { - proxies.universal = createProxy(node.universal, false); + proxies.universal = createProxy(node.universal, 'page'); } } /** * @param {string} file_path - * @param {boolean} is_server + * @param {"page.server" | "page" | "endpoint"} type * @returns {Proxy} */ -function createProxy(file_path, is_server) { - const proxy = tweak_types(fs.readFileSync(file_path, 'utf8'), is_server); +function createProxy(file_path, type) { + const proxy = tweak_types(fs.readFileSync(file_path, 'utf8'), type); if (proxy) { return { ...proxy, @@ -600,11 +635,24 @@ function generate_params_type(params, outdir, config) { /** * @param {string} content - * @param {boolean} is_server + * @param {"page" | "page.server" | "endpoint"} type * @returns {Omit, 'file_name'> | null} */ -export function tweak_types(content, is_server) { - const names = new Set(is_server ? ['load', 'actions'] : ['load']); +export function tweak_types(content, type) { + /** @type {Set} */ + let names; + + switch (type) { + case 'page.server': + names = new Set(['load', 'actions']); + break; + case 'page': + names = new Set(['load']); + break; + case 'endpoint': + names = new Set(['GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'OPTIONS', 'PATCH']); + break; + } try { let modified = false; @@ -754,7 +802,7 @@ export function tweak_types(content, is_server) { modified = true; } } else if ( - is_server && + type === 'page.server' && ts.isIdentifier(declaration.name) && declaration.name?.text === 'actions' && declaration.initializer diff --git a/packages/kit/src/core/sync/write_types/index.spec.js b/packages/kit/src/core/sync/write_types/index.spec.js index d50a4d12fa82..3c5dd5f276b8 100644 --- a/packages/kit/src/core/sync/write_types/index.spec.js +++ b/packages/kit/src/core/sync/write_types/index.spec.js @@ -58,7 +58,7 @@ test('Rewrites types for a TypeScript module', () => { }; `; - const rewritten = tweak_types(source, false); + const rewritten = tweak_types(source, 'page'); expect(rewritten?.exports).toEqual(['load']); assert.equal( @@ -83,7 +83,7 @@ test('Rewrites types for a TypeScript module without param', () => { }; `; - const rewritten = tweak_types(source, false); + const rewritten = tweak_types(source, 'page'); expect(rewritten?.exports).toEqual(['load']); assert.equal( @@ -109,7 +109,7 @@ test('Rewrites types for a TypeScript module without param and jsdoc without typ }; `; - const rewritten = tweak_types(source, false); + const rewritten = tweak_types(source, 'page'); expect(rewritten?.exports).toEqual(['load']); assert.equal( @@ -136,7 +136,7 @@ test('Rewrites types for a JavaScript module with `function`', () => { }; `; - const rewritten = tweak_types(source, false); + const rewritten = tweak_types(source, 'page'); expect(rewritten?.exports).toEqual(['load']); assert.equal( @@ -163,7 +163,7 @@ test('Rewrites types for a JavaScript module with `const`', () => { }; `; - const rewritten = tweak_types(source, false); + const rewritten = tweak_types(source, 'page'); expect(rewritten?.exports).toEqual(['load']); assert.equal( @@ -190,7 +190,7 @@ test('Appends @ts-nocheck after @ts-check', () => { }; `; - const rewritten = tweak_types(source, false); + const rewritten = tweak_types(source, 'page'); expect(rewritten?.exports).toEqual(['load']); assert.equal( @@ -219,7 +219,7 @@ test('Rewrites action types for a JavaScript module', () => { } `; - const rewritten = tweak_types(source, true); + const rewritten = tweak_types(source, 'page.server'); expect(rewritten?.exports).toEqual(['actions']); assert.equal( @@ -248,7 +248,7 @@ test('Rewrites action types for a TypeScript module', () => { } `; - const rewritten = tweak_types(source, true); + const rewritten = tweak_types(source, 'page.server'); expect(rewritten?.exports).toEqual(['actions']); assert.equal( @@ -281,7 +281,7 @@ test('Leaves satisfies operator untouched', () => { } satisfies Actions `; - const rewritten = tweak_types(source, true); + const rewritten = tweak_types(source, 'page.server'); expect(rewritten?.exports).toEqual(['load', 'actions']); assert.equal(rewritten?.modified, false); diff --git a/packages/kit/test/apps/basics/src/routes/endpoint-typing/foo/+server.ts b/packages/kit/test/apps/basics/src/routes/endpoint-typing/foo/+server.ts new file mode 100644 index 000000000000..e1ac371064a7 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/endpoint-typing/foo/+server.ts @@ -0,0 +1,14 @@ +import { json } from '@sveltejs/kit'; + +interface TypedResponse extends Response { + json(): Promise; +} + +// A typed json wrapper (this could probably just replace `json` +function typed_json(data: A): TypedResponse { + return json(data) +} + +export async function GET(): Promise> { + return typed_json({ foo: 'bar' }) +} \ No newline at end of file