@forinda/kickjs-client 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/index.d.ts +99 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/package.json +11 -11
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Felix Orinda
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
|
|
2
|
+
//#region src/index.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* The per-route shape `kick typegen` emits into `KickRoutes.Api`.
|
|
5
|
+
* Deliberately re-declares (NOT imports) @forinda/kickjs's `RouteShape`:
|
|
6
|
+
* frontends installing this client must never need the server package.
|
|
7
|
+
* Keep the field list in sync with `RouteShape` in kickjs http/context.ts.
|
|
8
|
+
*/
|
|
9
|
+
interface RouteShapeLike {
|
|
10
|
+
params: unknown;
|
|
11
|
+
body: unknown;
|
|
12
|
+
query: unknown;
|
|
13
|
+
response: unknown;
|
|
14
|
+
}
|
|
15
|
+
type ApiMap = Record<string, RouteShapeLike>;
|
|
16
|
+
/** HTTP verbs the route decorators produce. */
|
|
17
|
+
type ClientMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
|
18
|
+
/** Paths in the map registered under a given verb. */
|
|
19
|
+
type PathsFor<Api extends ApiMap, M extends ClientMethod> = { [K in keyof Api]: K extends `${M} ${infer P}` ? P : never }[keyof Api];
|
|
20
|
+
type ShapeOf<Api extends ApiMap, M extends ClientMethod, P extends string> = `${M} ${P}` extends keyof Api ? Api[`${M} ${P}`] : never;
|
|
21
|
+
type ParamsField<T> = unknown extends T ? {
|
|
22
|
+
params?: Record<string, string | number>;
|
|
23
|
+
} : Record<string, never> extends T ? {
|
|
24
|
+
params?: T;
|
|
25
|
+
} : {
|
|
26
|
+
params: T;
|
|
27
|
+
};
|
|
28
|
+
type BodyField<T> = unknown extends T ? {
|
|
29
|
+
body?: unknown;
|
|
30
|
+
} : {
|
|
31
|
+
body: T;
|
|
32
|
+
};
|
|
33
|
+
/** Loose fallback for routes without a statically-known query shape. */
|
|
34
|
+
type LooseQuery = Record<string, string | number | boolean | Array<string | number>>;
|
|
35
|
+
type QueryField<T> = unknown extends T ? {
|
|
36
|
+
query?: LooseQuery;
|
|
37
|
+
} : {
|
|
38
|
+
query?: T;
|
|
39
|
+
};
|
|
40
|
+
type RequestOptions<S extends RouteShapeLike> = {
|
|
41
|
+
/** Extra headers merged over the client-level ones. */headers?: Record<string, string>; /** AbortSignal for cancellation. */
|
|
42
|
+
signal?: AbortSignal;
|
|
43
|
+
} & ParamsField<S['params']> & BodyField<S['body']> & QueryField<S['query']>;
|
|
44
|
+
interface ClientOptions {
|
|
45
|
+
/**
|
|
46
|
+
* Base URL INCLUDING the mount prefix + version the server adds at
|
|
47
|
+
* bootstrap (default `/api/v1`) — `KickRoutes.Api` keys are module-mount
|
|
48
|
+
* relative: `'GET /users/:id'` + baseUrl `https://x/api/v1`.
|
|
49
|
+
*/
|
|
50
|
+
baseUrl: string;
|
|
51
|
+
/** Static headers, or a factory invoked per request (auth tokens). */
|
|
52
|
+
headers?: Record<string, string> | (() => Record<string, string> | Promise<Record<string, string>>);
|
|
53
|
+
/**
|
|
54
|
+
* Custom fetch — inject a platform fetch, a mock, or a KickJS web app's
|
|
55
|
+
* own handler for network-free tests: `{ fetch: app.fetch }`.
|
|
56
|
+
*/
|
|
57
|
+
fetch?: (request: Request) => Promise<Response>;
|
|
58
|
+
}
|
|
59
|
+
/** Non-2xx responses throw this — carries the parsed body (RFC 9457 problem details when the server used `ctx.problem`). */
|
|
60
|
+
declare class KickClientError extends Error {
|
|
61
|
+
readonly status: number;
|
|
62
|
+
readonly body: unknown;
|
|
63
|
+
readonly response: Response;
|
|
64
|
+
constructor(status: number, body: unknown, response: Response);
|
|
65
|
+
}
|
|
66
|
+
interface KickClient<Api extends ApiMap> {
|
|
67
|
+
get<P extends PathsFor<Api, 'GET'> & string>(path: P, options?: RequestOptions<ShapeOf<Api, 'GET', P>>): Promise<ShapeOf<Api, 'GET', P>['response']>;
|
|
68
|
+
post<P extends PathsFor<Api, 'POST'> & string>(path: P, options?: RequestOptions<ShapeOf<Api, 'POST', P>>): Promise<ShapeOf<Api, 'POST', P>['response']>;
|
|
69
|
+
put<P extends PathsFor<Api, 'PUT'> & string>(path: P, options?: RequestOptions<ShapeOf<Api, 'PUT', P>>): Promise<ShapeOf<Api, 'PUT', P>['response']>;
|
|
70
|
+
delete<P extends PathsFor<Api, 'DELETE'> & string>(path: P, options?: RequestOptions<ShapeOf<Api, 'DELETE', P>>): Promise<ShapeOf<Api, 'DELETE', P>['response']>;
|
|
71
|
+
patch<P extends PathsFor<Api, 'PATCH'> & string>(path: P, options?: RequestOptions<ShapeOf<Api, 'PATCH', P>>): Promise<ShapeOf<Api, 'PATCH', P>['response']>;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Create a typed client over a `KickRoutes.Api`-shaped map.
|
|
75
|
+
* Generic-only consumption — no runtime dependency on the generated file.
|
|
76
|
+
*/
|
|
77
|
+
declare function createClient<Api extends ApiMap>(options: ClientOptions): KickClient<Api>;
|
|
78
|
+
/** Anything with a web-standard fetch — a `createWebApp()` result, a Worker, a mock. */
|
|
79
|
+
interface FetchLike {
|
|
80
|
+
fetch(request: Request): Promise<Response>;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Typed client over an in-process app — network-free full-stack tests:
|
|
84
|
+
*
|
|
85
|
+
* ```ts
|
|
86
|
+
* const app = createWebApp({ h3, modules })
|
|
87
|
+
* const api = createTestClient<KickRoutes.Api>(app)
|
|
88
|
+
* expect(await api.get('/tasks/:id', { params: { id: '1' } })).toEqual(...)
|
|
89
|
+
* ```
|
|
90
|
+
*
|
|
91
|
+
* `baseUrl` defaults to `http://test/api/v1` (the bootstrap default prefix);
|
|
92
|
+
* override it when the server uses a custom `apiPrefix`/version.
|
|
93
|
+
*/
|
|
94
|
+
declare function createTestClient<Api extends ApiMap>(app: FetchLike, options?: Omit<ClientOptions, 'fetch' | 'baseUrl'> & {
|
|
95
|
+
baseUrl?: string;
|
|
96
|
+
}): KickClient<Api>;
|
|
97
|
+
//#endregion
|
|
98
|
+
export { ClientMethod, ClientOptions, FetchLike, KickClient, KickClientError, RequestOptions, RouteShapeLike, createClient, createTestClient };
|
|
99
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"mappings":"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"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @forinda/kickjs-client v0.1.0
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) Felix Orinda
|
|
5
|
+
*
|
|
6
|
+
* This source code is licensed under the MIT license found in the
|
|
7
|
+
* LICENSE file in the root directory of this source tree.
|
|
8
|
+
*
|
|
9
|
+
* @license MIT
|
|
10
|
+
*/
|
|
11
|
+
var KickClientError=class extends Error{status;body;response;constructor(status,body,response){super(typeof body==`object`&&body&&`detail`in body?String(body.detail):typeof body==`object`&&body&&`error`in body?String(body.error):`Request failed with status ${status}`),this.status=status,this.body=body,this.response=response,this.name=`KickClientError`}};function fillPath(path,params){return path.replace(/:([A-Za-z0-9_]+)/g,(_,name)=>{let value=params?.[name];if(value===void 0)throw Error(`@forinda/kickjs-client: missing path param ':${name}' for '${path}'`);return encodeURIComponent(String(value))})}function buildQuery(query){if(!query)return``;let qs=new URLSearchParams;for(let[key,value]of Object.entries(query))if(value!=null)if(Array.isArray(value))for(let v of value)qs.append(key,String(v));else qs.append(key,String(value));let s=qs.toString();return s?`?${s}`:``}function createClient(options){let baseUrl=options.baseUrl.replace(/\/$/,``),doFetch=options.fetch??(req=>fetch(req));async function run(method,path,opts={}){let clientHeaders=typeof options.headers==`function`?await options.headers():options.headers??{},headers=new Headers({...clientHeaders,...opts.headers}),hasBody=opts.body!==void 0;hasBody&&!headers.has(`content-type`)&&headers.set(`content-type`,`application/json`);let url=`${baseUrl}${fillPath(path,opts.params)}${buildQuery(opts.query)}`,request=new Request(url,{method,headers,body:hasBody?JSON.stringify(opts.body):void 0,signal:opts.signal}),response=await doFetch(request),contentType=response.headers.get(`content-type`)??``,body=response.status===204?void 0:contentType.includes(`json`)?await response.json().catch(()=>void 0):await response.text();if(!response.ok)throw new KickClientError(response.status,body,response);return body}return{get:(path,opts)=>run(`GET`,path,opts),post:(path,opts)=>run(`POST`,path,opts),put:(path,opts)=>run(`PUT`,path,opts),delete:(path,opts)=>run(`DELETE`,path,opts),patch:(path,opts)=>run(`PATCH`,path,opts)}}function createTestClient(app,options={}){return createClient({...options,baseUrl:options.baseUrl??`http://test/api/v1`,fetch:request=>app.fetch(request)})}export{KickClientError,createClient,createTestClient};
|
|
12
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["// @forinda/kickjs-client — typed fetch client for KickJS APIs.\n//\n// Consumes the flat `KickRoutes.Api` map that `kick typegen` emits\n// (response-inference-design.md R3): keys are `'METHOD /path'`, values are\n// the route shapes (`params` / `body` / `query` / `response`). The client is\n// a ~150-line fetch wrapper — every type below exists so the CALL SITE\n// infers exactly:\n//\n// ```ts\n// const api = createClient<KickRoutes.Api>({ baseUrl: 'https://x/api/v1' })\n// const task = await api.post('/tasks/:id', { params: { id }, body })\n// // ^ the handler's actual response type\n// ```\n//\n// Runtime-neutral: fetch/URL/Headers only — browsers, node ≥ 20, Bun, Deno,\n// and edge workers (pairs with `@forinda/kickjs/web` on the server side).\n\n/**\n * The per-route shape `kick typegen` emits into `KickRoutes.Api`.\n * Deliberately re-declares (NOT imports) @forinda/kickjs's `RouteShape`:\n * frontends installing this client must never need the server package.\n * Keep the field list in sync with `RouteShape` in kickjs http/context.ts.\n */\nexport interface RouteShapeLike {\n params: unknown\n body: unknown\n query: unknown\n response: unknown\n}\n\ntype ApiMap = Record<string, RouteShapeLike>\n\n/** HTTP verbs the route decorators produce. */\nexport type ClientMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'\n\n/** Paths in the map registered under a given verb. */\ntype PathsFor<Api extends ApiMap, M extends ClientMethod> = {\n [K in keyof Api]: K extends `${M} ${infer P}` ? P : never\n}[keyof Api]\n\n// Fallback is `never` ON PURPOSE: P is already constrained to PathsFor, so a\n// failed key lookup means the map and the client's key math drifted — that\n// must fail loudly at the call site, not silently degrade to unknown.\ntype ShapeOf<\n Api extends ApiMap,\n M extends ClientMethod,\n P extends string,\n> = `${M} ${P}` extends keyof Api ? Api[`${M} ${P}`] : never\n\n// Paramless routes emit `params: {}` and unschema'd bodies emit\n// `body: unknown` — both become OPTIONAL fields; a concrete shape is\n// required so forgetting `params` on `/users/:id` is a type error.\ntype ParamsField<T> = unknown extends T\n ? { params?: Record<string, string | number> }\n : Record<string, never> extends T\n ? { params?: T }\n : { params: T }\n\ntype BodyField<T> = unknown extends T ? { body?: unknown } : { body: T }\n\n/** Loose fallback for routes without a statically-known query shape. */\ntype LooseQuery = Record<string, string | number | boolean | Array<string | number>>\n\n// Routes with a typegen-known query shape (Zod schema or the\n// @ApiQueryParams-derived filter/sort/q/page/limit object) constrain\n// `query` — sort fields autocomplete as '-createdAt' | 'createdAt' | …\ntype QueryField<T> = unknown extends T ? { query?: LooseQuery } : { query?: T }\n\nexport type RequestOptions<S extends RouteShapeLike> = {\n /** Extra headers merged over the client-level ones. */\n headers?: Record<string, string>\n /** AbortSignal for cancellation. */\n signal?: AbortSignal\n} & ParamsField<S['params']> &\n BodyField<S['body']> &\n QueryField<S['query']>\n\nexport interface ClientOptions {\n /**\n * Base URL INCLUDING the mount prefix + version the server adds at\n * bootstrap (default `/api/v1`) — `KickRoutes.Api` keys are module-mount\n * relative: `'GET /users/:id'` + baseUrl `https://x/api/v1`.\n */\n baseUrl: string\n /** Static headers, or a factory invoked per request (auth tokens). */\n headers?:\n | Record<string, string>\n | (() => Record<string, string> | Promise<Record<string, string>>)\n /**\n * Custom fetch — inject a platform fetch, a mock, or a KickJS web app's\n * own handler for network-free tests: `{ fetch: app.fetch }`.\n */\n fetch?: (request: Request) => Promise<Response>\n}\n\n/** Non-2xx responses throw this — carries the parsed body (RFC 9457 problem details when the server used `ctx.problem`). */\nexport class KickClientError extends Error {\n constructor(\n readonly status: number,\n readonly body: unknown,\n readonly response: Response,\n ) {\n super(\n typeof body === 'object' && body !== null && 'detail' in body\n ? String((body as { detail: unknown }).detail)\n : typeof body === 'object' && body !== null && 'error' in body\n ? String((body as { error: unknown }).error)\n : `Request failed with status ${status}`,\n )\n this.name = 'KickClientError'\n }\n}\n\nexport interface KickClient<Api extends ApiMap> {\n get<P extends PathsFor<Api, 'GET'> & string>(\n path: P,\n options?: RequestOptions<ShapeOf<Api, 'GET', P>>,\n ): Promise<ShapeOf<Api, 'GET', P>['response']>\n post<P extends PathsFor<Api, 'POST'> & string>(\n path: P,\n options?: RequestOptions<ShapeOf<Api, 'POST', P>>,\n ): Promise<ShapeOf<Api, 'POST', P>['response']>\n put<P extends PathsFor<Api, 'PUT'> & string>(\n path: P,\n options?: RequestOptions<ShapeOf<Api, 'PUT', P>>,\n ): Promise<ShapeOf<Api, 'PUT', P>['response']>\n delete<P extends PathsFor<Api, 'DELETE'> & string>(\n path: P,\n options?: RequestOptions<ShapeOf<Api, 'DELETE', P>>,\n ): Promise<ShapeOf<Api, 'DELETE', P>['response']>\n patch<P extends PathsFor<Api, 'PATCH'> & string>(\n path: P,\n options?: RequestOptions<ShapeOf<Api, 'PATCH', P>>,\n ): Promise<ShapeOf<Api, 'PATCH', P>['response']>\n}\n\ninterface AnyRequestOptions {\n headers?: Record<string, string>\n signal?: AbortSignal\n query?: Record<string, unknown>\n params?: Record<string, string | number>\n body?: unknown\n}\n\n/** Substitute `:param` segments; throw on any left unfilled. */\nfunction fillPath(path: string, params?: Record<string, string | number>): string {\n const filled = path.replace(/:([A-Za-z0-9_]+)/g, (_, name: string) => {\n const value = params?.[name]\n if (value === undefined) {\n throw new Error(`@forinda/kickjs-client: missing path param ':${name}' for '${path}'`)\n }\n return encodeURIComponent(String(value))\n })\n return filled\n}\n\nfunction buildQuery(query?: Record<string, unknown>): string {\n if (!query) return ''\n const qs = new URLSearchParams()\n for (const [key, value] of Object.entries(query)) {\n if (value === undefined || value === null) continue\n if (Array.isArray(value)) for (const v of value) qs.append(key, String(v))\n else qs.append(key, String(value))\n }\n const s = qs.toString()\n return s ? `?${s}` : ''\n}\n\n/**\n * Create a typed client over a `KickRoutes.Api`-shaped map.\n * Generic-only consumption — no runtime dependency on the generated file.\n */\nexport function createClient<Api extends ApiMap>(options: ClientOptions): KickClient<Api> {\n const baseUrl = options.baseUrl.replace(/\\/$/, '')\n const doFetch = options.fetch ?? ((req: Request) => fetch(req))\n\n async function run(method: ClientMethod, path: string, opts: AnyRequestOptions = {}) {\n const clientHeaders =\n typeof options.headers === 'function' ? await options.headers() : (options.headers ?? {})\n const headers = new Headers({ ...clientHeaders, ...opts.headers })\n const hasBody = opts.body !== undefined\n if (hasBody && !headers.has('content-type')) {\n headers.set('content-type', 'application/json')\n }\n\n const url = `${baseUrl}${fillPath(path, opts.params)}${buildQuery(opts.query)}`\n const request = new Request(url, {\n method,\n headers,\n body: hasBody ? JSON.stringify(opts.body) : undefined,\n signal: opts.signal,\n })\n\n const response = await doFetch(request)\n const contentType = response.headers.get('content-type') ?? ''\n const body =\n response.status === 204\n ? undefined\n : contentType.includes('json')\n ? await response.json().catch(() => undefined)\n : await response.text()\n\n if (!response.ok) throw new KickClientError(response.status, body, response)\n return body\n }\n\n return {\n get: (path, opts) => run('GET', path, opts as AnyRequestOptions),\n post: (path, opts) => run('POST', path, opts as AnyRequestOptions),\n put: (path, opts) => run('PUT', path, opts as AnyRequestOptions),\n delete: (path, opts) => run('DELETE', path, opts as AnyRequestOptions),\n patch: (path, opts) => run('PATCH', path, opts as AnyRequestOptions),\n } as KickClient<Api>\n}\n\n/** Anything with a web-standard fetch — a `createWebApp()` result, a Worker, a mock. */\nexport interface FetchLike {\n fetch(request: Request): Promise<Response>\n}\n\n/**\n * Typed client over an in-process app — network-free full-stack tests:\n *\n * ```ts\n * const app = createWebApp({ h3, modules })\n * const api = createTestClient<KickRoutes.Api>(app)\n * expect(await api.get('/tasks/:id', { params: { id: '1' } })).toEqual(...)\n * ```\n *\n * `baseUrl` defaults to `http://test/api/v1` (the bootstrap default prefix);\n * override it when the server uses a custom `apiPrefix`/version.\n */\nexport function createTestClient<Api extends ApiMap>(\n app: FetchLike,\n options: Omit<ClientOptions, 'fetch' | 'baseUrl'> & { baseUrl?: string } = {},\n): KickClient<Api> {\n return createClient<Api>({\n ...options,\n // AFTER the spread — an explicit `baseUrl: undefined` key in options\n // must not clobber the default (spread copies undefined-valued keys).\n baseUrl: options.baseUrl ?? 'http://test/api/v1',\n fetch: (request) => app.fetch(request),\n })\n}\n"],"mappings":";;;;;;;;;;AAgGA,IAAa,gBAAb,cAAqC,KAAM,CAE9B,OACA,KACA,SAHX,YACE,OACA,KACA,SACA,CACA,MACE,OAAO,MAAS,UAAY,MAAiB,WAAY,KACrD,OAAQ,KAA6B,MAAM,EAC3C,OAAO,MAAS,UAAY,MAAiB,UAAW,KACtD,OAAQ,KAA4B,KAAK,EACzC,8BAA8B,QACtC,EAVS,KAAA,OAAA,OACA,KAAA,KAAA,KACA,KAAA,SAAA,SAST,KAAK,KAAO,iBACd,CACF,EAkCA,SAAS,SAAS,KAAc,OAAkD,CAQhF,OAPe,KAAK,QAAQ,qBAAsB,EAAG,OAAiB,CACpE,IAAM,MAAQ,SAAS,MACvB,GAAI,QAAU,IAAA,GACZ,MAAU,MAAM,gDAAgD,KAAK,SAAS,KAAK,EAAE,EAEvF,OAAO,mBAAmB,OAAO,KAAK,CAAC,CACzC,CACY,CACd,CAEA,SAAS,WAAW,MAAyC,CAC3D,GAAI,CAAC,MAAO,MAAO,GACnB,IAAM,GAAK,IAAI,gBACf,IAAK,GAAM,CAAC,IAAK,SAAU,OAAO,QAAQ,KAAK,EACzC,UAAiC,KACrC,GAAI,MAAM,QAAQ,KAAK,EAAG,IAAK,IAAM,KAAK,MAAO,GAAG,OAAO,IAAK,OAAO,CAAC,CAAC,OACpE,GAAG,OAAO,IAAK,OAAO,KAAK,CAAC,EAEnC,IAAM,EAAI,GAAG,SAAS,EACtB,OAAO,EAAI,IAAI,IAAM,EACvB,CAMA,SAAgB,aAAiC,QAAyC,CACxF,IAAM,QAAU,QAAQ,QAAQ,QAAQ,MAAO,EAAE,EAC3C,QAAU,QAAQ,QAAW,KAAiB,MAAM,GAAG,GAE7D,eAAe,IAAI,OAAsB,KAAc,KAA0B,CAAC,EAAG,CACnF,IAAM,cACJ,OAAO,QAAQ,SAAY,WAAa,MAAM,QAAQ,QAAQ,EAAK,QAAQ,SAAW,CAAC,EACnF,QAAU,IAAI,QAAQ,CAAE,GAAG,cAAe,GAAG,KAAK,OAAQ,CAAC,EAC3D,QAAU,KAAK,OAAS,IAAA,GAC1B,SAAW,CAAC,QAAQ,IAAI,cAAc,GACxC,QAAQ,IAAI,eAAgB,kBAAkB,EAGhD,IAAM,IAAM,GAAG,UAAU,SAAS,KAAM,KAAK,MAAM,IAAI,WAAW,KAAK,KAAK,IACtE,QAAU,IAAI,QAAQ,IAAK,CAC/B,OACA,QACA,KAAM,QAAU,KAAK,UAAU,KAAK,IAAI,EAAI,IAAA,GAC5C,OAAQ,KAAK,MACf,CAAC,EAEK,SAAW,MAAM,QAAQ,OAAO,EAChC,YAAc,SAAS,QAAQ,IAAI,cAAc,GAAK,GACtD,KACJ,SAAS,SAAW,IAChB,IAAA,GACA,YAAY,SAAS,MAAM,EACzB,MAAM,SAAS,KAAK,CAAC,CAAC,UAAY,IAAA,EAAS,EAC3C,MAAM,SAAS,KAAK,EAE5B,GAAI,CAAC,SAAS,GAAI,MAAM,IAAI,gBAAgB,SAAS,OAAQ,KAAM,QAAQ,EAC3E,OAAO,IACT,CAEA,MAAO,CACL,KAAM,KAAM,OAAS,IAAI,MAAO,KAAM,IAAyB,EAC/D,MAAO,KAAM,OAAS,IAAI,OAAQ,KAAM,IAAyB,EACjE,KAAM,KAAM,OAAS,IAAI,MAAO,KAAM,IAAyB,EAC/D,QAAS,KAAM,OAAS,IAAI,SAAU,KAAM,IAAyB,EACrE,OAAQ,KAAM,OAAS,IAAI,QAAS,KAAM,IAAyB,CACrE,CACF,CAmBA,SAAgB,iBACd,IACA,QAA2E,CAAC,EAC3D,CACjB,OAAO,aAAkB,CACvB,GAAG,QAGH,QAAS,QAAQ,SAAW,qBAC5B,MAAQ,SAAY,IAAI,MAAM,OAAO,CACvC,CAAC,CACH"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forinda/kickjs-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Typed fetch client for KickJS APIs — end-to-end response types from the kick typegen KickRoutes augmentation",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"kickjs",
|
|
@@ -23,19 +23,12 @@
|
|
|
23
23
|
"dist",
|
|
24
24
|
"README.md"
|
|
25
25
|
],
|
|
26
|
-
"scripts": {
|
|
27
|
-
"build": "tsdown",
|
|
28
|
-
"dev": "tsdown --watch",
|
|
29
|
-
"test": "vitest run --passWithNoTests",
|
|
30
|
-
"typecheck": "tsgo --noEmit",
|
|
31
|
-
"clean": "rm -rf dist .wireit"
|
|
32
|
-
},
|
|
33
26
|
"devDependencies": {
|
|
34
|
-
"@forinda/kickjs": "workspace:*",
|
|
35
27
|
"h3-v2": "npm:h3@2.0.1-rc.22",
|
|
36
28
|
"reflect-metadata": "^0.2.2",
|
|
37
29
|
"typescript": "^6.0.3",
|
|
38
|
-
"vitest": "^4.1.8"
|
|
30
|
+
"vitest": "^4.1.8",
|
|
31
|
+
"@forinda/kickjs": "6.2.0"
|
|
39
32
|
},
|
|
40
33
|
"publishConfig": {
|
|
41
34
|
"access": "public"
|
|
@@ -53,5 +46,12 @@
|
|
|
53
46
|
},
|
|
54
47
|
"bugs": {
|
|
55
48
|
"url": "https://github.com/forinda/kick-js/issues"
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "tsdown",
|
|
52
|
+
"dev": "tsdown --watch",
|
|
53
|
+
"test": "vitest run --passWithNoTests",
|
|
54
|
+
"typecheck": "tsgo --noEmit",
|
|
55
|
+
"clean": "rm -rf dist .wireit"
|
|
56
56
|
}
|
|
57
|
-
}
|
|
57
|
+
}
|