@sdk-it/typescript 0.36.1 → 0.37.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/README.md CHANGED
@@ -72,7 +72,7 @@ await generate(spec, {
72
72
 
73
73
  ```bash
74
74
  # using recent versions of node
75
- node --experimental-strip-types ./openapi.ts
75
+ node ./openapi.ts
76
76
 
77
77
  # using node < 22
78
78
  npx tsx ./openapi.ts
package/dist/index.js CHANGED
@@ -393,18 +393,31 @@ function toZod(schema, required) {
393
393
  // packages/typescript/src/lib/client.ts
394
394
  var client_default = (spec, style) => {
395
395
  const defaultHeaders = `{${spec.options.filter((value) => value.in === "header").map(
396
- (value) => `'${value.name}': this.options['${value["x-optionName"] ?? value.name}']`
396
+ (value) => `'${value.name}': options['${value["x-optionName"] ?? value.name}']`
397
397
  ).join(",\n")}}`;
398
398
  const defaultInputs = `{${spec.options.filter((value) => value.in === "input").map(
399
- (value) => `'${value.name}': this.options['${value["x-optionName"] ?? value.name}']`
399
+ (value) => `'${value.name}': options['${value["x-optionName"] ?? value.name}']`
400
400
  ).join(",\n")}}`;
401
+ const globalOptions = Object.fromEntries(
402
+ spec.options.map((value) => [
403
+ `'${value["x-optionName"] ?? value.name}'`,
404
+ { schema: toZod(value.schema, value.required) }
405
+ ])
406
+ );
401
407
  const specOptions = {
402
- ...Object.fromEntries(
403
- spec.options.map((value) => [
404
- `'${value["x-optionName"] ?? value.name}'`,
405
- { schema: toZod(value.schema, value.required) }
406
- ])
407
- ),
408
+ ...globalOptions,
409
+ ...globalOptions["'token'"] ? {
410
+ "'token'": {
411
+ schema: `z.union([z.string(),z.function().returns(z.union([z.string(), z.promise(z.string())])),]).optional()
412
+ .transform(async (token) => {
413
+ if (!token) return undefined;
414
+ if (typeof token === 'function') {
415
+ token = await Promise.resolve(token());
416
+ }
417
+ return \`Bearer \${token}\`;
418
+ })`
419
+ }
420
+ } : {},
408
421
  fetch: {
409
422
  schema: "fetchType"
410
423
  },
@@ -430,30 +443,34 @@ ${spec.servers.length ? `export const servers = ${JSON.stringify(spec.servers, n
430
443
  const optionsSchema = z.object(${toLitObject(specOptions, (x) => x.schema)});
431
444
  ${spec.servers.length ? `export type Servers = typeof servers[number];` : ""}
432
445
 
433
- type ${spec.name}Options = z.infer<typeof optionsSchema>;
446
+ type ${spec.name}Options = z.input<typeof optionsSchema>;
434
447
 
435
448
  export class ${spec.name} {
436
- public options: ${spec.name}Options
449
+ public options: ${spec.name}Options;
437
450
  constructor(options: ${spec.name}Options) {
438
- this.options = optionsSchema.parse(options);
451
+ this.options = options;
439
452
  }
440
453
 
441
454
  async request<const E extends keyof typeof schemas>(
442
455
  endpoint: E,
443
- input: z.infer<(typeof schemas)[E]['schema']>,
456
+ input: z.input<(typeof schemas)[E]['schema']>,
444
457
  options?: { signal?: AbortSignal, headers?: HeadersInit },
445
458
  ) ${style.errorAsValue ? `: Promise<Awaited<ReturnType<(typeof schemas)[E]['dispatch']>>| [never, ParseError<(typeof schemas)[E]['schema']>]>` : `: Promise<Awaited<ReturnType<(typeof schemas)[E]['dispatch']>>>`} {
446
459
  const route = schemas[endpoint];
447
- const withDefaultInputs = Object.assign({}, this.#defaultInputs, input);
460
+ const withDefaultInputs = Object.assign({}, await this.#defaultInputs(), input);
448
461
  const [parsedInput, parseError] = parseInput(route.schema, withDefaultInputs);
449
462
  if (parseError) {
450
463
  ${style.errorAsValue ? "return [null as never, parseError as never] as const;" : "throw parseError;"}
451
464
  }
465
+ const clientOptions = await optionsSchema.parseAsync(this.options);
452
466
  const result = await route.dispatch(parsedInput as never, {
453
- fetch: this.options.fetch,
467
+ fetch: clientOptions.fetch,
454
468
  interceptors: [
455
- createHeadersInterceptor(() => this.defaultHeaders, options?.headers ?? {}),
456
- createBaseUrlInterceptor(() => this.options.baseUrl),
469
+ createHeadersInterceptor(
470
+ await this.#defaultHeaders(),
471
+ options?.headers ?? {},
472
+ ),
473
+ createBaseUrlInterceptor(clientOptions.baseUrl),
457
474
  ],
458
475
  signal: options?.signal,
459
476
  });
@@ -462,7 +479,7 @@ export class ${spec.name} {
462
479
 
463
480
  async prepare<const E extends keyof typeof schemas>(
464
481
  endpoint: E,
465
- input: z.infer<(typeof schemas)[E]['schema']>,
482
+ input: z.input<(typeof schemas)[E]['schema']>,
466
483
  options?: { headers?: HeadersInit },
467
484
  ): ${style.errorAsValue ? `Promise<
468
485
  readonly [
@@ -474,14 +491,14 @@ export class ${spec.name} {
474
491
  >` : `Promise<RequestConfig & {
475
492
  parse: (response: Response) => ReturnType<typeof parse>;
476
493
  }>`} {
494
+ const clientOptions = await optionsSchema.parseAsync(this.options);
477
495
  const route = schemas[endpoint];
478
-
479
496
  const interceptors = [
480
497
  createHeadersInterceptor(
481
- () => this.defaultHeaders,
498
+ await this.#defaultHeaders(),
482
499
  options?.headers ?? {},
483
500
  ),
484
- createBaseUrlInterceptor(() => this.options.baseUrl),
501
+ createBaseUrlInterceptor(clientOptions.baseUrl),
485
502
  ];
486
503
  const [parsedInput, parseError] = parseInput(route.schema, input);
487
504
  if (parseError) {
@@ -498,23 +515,26 @@ export class ${spec.name} {
498
515
  return ${style.errorAsValue ? "[prepared, null as never] as const;" : "prepared as any"}
499
516
  }
500
517
 
501
- get defaultHeaders() {
502
- return { ...${defaultHeaders}, ...this.options.headers}
518
+ async #defaultHeaders() {
519
+ const options = await optionsSchema.parseAsync(this.options);
520
+ return {
521
+ ...${defaultHeaders},
522
+ ...options.headers,
523
+ };
503
524
  }
504
525
 
505
- get #defaultInputs() {
526
+ async #defaultInputs() {
527
+ const options = await optionsSchema.parseAsync(this.options);
506
528
  return ${defaultInputs}
507
529
  }
508
530
 
509
531
  setOptions(options: Partial<${spec.name}Options>) {
510
- const validated = optionsSchema.partial().parse(options);
511
-
512
- for (const key of Object.keys(validated) as (keyof ${spec.name}Options)[]) {
513
- if (validated[key] !== undefined) {
514
- (this.options[key] as typeof validated[typeof key]) = validated[key]!;
515
- }
516
- }
532
+ this.options = {
533
+ ...this.options,
534
+ ...options,
535
+ };
517
536
  }
537
+
518
538
  }`;
519
539
  };
520
540
 
@@ -955,7 +975,7 @@ function inputToPath(operation, inputs) {
955
975
  }
956
976
 
957
977
  // packages/typescript/src/lib/styles/github/endpoints.txt
958
- var endpoints_default = "type EndpointOutput<K extends keyof typeof schemas> = Extract<\n Unionize<(typeof schemas)[K]['output']>,\n SuccessfulResponse\n>;\n\ntype EndpointError<K extends keyof typeof schemas> = Extract<\n Unionize<(typeof schemas)[K]['output']>,\n ProblematicResponse\n>;\n\nexport type Endpoints = {\n [K in keyof typeof schemas]: {\n input: z.infer<(typeof schemas)[K]['schema']>;\n output: <% if (outputType === 'default') { %>EndpointOutput<K>['data']<% } else { %>EndpointOutput<K><% } %>;\n error: EndpointError<K> | ParseError<(typeof schemas)[K]['schema']>;\n };\n};";
978
+ var endpoints_default = "type EndpointOutput<K extends keyof typeof schemas> = Extract<\n Unionize<(typeof schemas)[K]['output']>,\n SuccessfulResponse\n>;\n\ntype EndpointError<K extends keyof typeof schemas> = Extract<\n Unionize<(typeof schemas)[K]['output']>,\n ProblematicResponse\n>;\n\nexport type Endpoints = {\n [K in keyof typeof schemas]: {\n input: z.input<(typeof schemas)[K]['schema']>;\n output: <% if (outputType === 'default') { %>EndpointOutput<K>['data']<% } else { %>EndpointOutput<K><% } %>;\n error: EndpointError<K> | ParseError<(typeof schemas)[K]['schema']>;\n };\n};";
959
979
 
960
980
  // packages/typescript/src/lib/generator.ts
961
981
  function coearceRequestInput(spec, operation, type) {
@@ -1215,7 +1235,7 @@ function operationSchema(ir, operation, type) {
1215
1235
  var dispatcher_default = "export type Unionize<T> = T extends [infer Single extends OutputType]\n ? InstanceType<Single>\n : T extends readonly [...infer Tuple extends OutputType[]]\n ? { [I in keyof Tuple]: InstanceType<Tuple[I]> }[number]\n : never;\n\nexport type InstanceType<T> =\n T extends Type<infer U>\n ? U\n : T extends { type: Type<infer U> }\n ? U\n : T extends Array<unknown>\n ? Unionize<T>\n : never;\n\nexport interface Type<T> {\n new (...args: any[]): T;\n}\nexport type Parser = (\n response: Response,\n) => Promise<unknown> | ReadableStream<any>;\nexport type OutputType =\n | Type<APIResponse>\n | { parser: Parser; type: Type<APIResponse> };\n\nexport const fetchType = z\n .function()\n .args(z.instanceof(Request))\n .returns(z.promise(z.instanceof(Response)))\n .optional();\n\nexport async function parse<T extends OutputType[]>(\n outputs: T,\n response: Response,\n) <% if(!throwError) { %>\n: Promise<\n [\n Extract<InstanceType<T>, SuccessfulResponse>['data'],\n Extract<InstanceType<T>, ProblematicResponse>['data'],\n ]\n>\n <% } %>\n\n\n\n {\n let output: typeof APIResponse | null = null;\n let parser: Parser = buffered;\n for (const outputType of outputs) {\n if ('parser' in outputType) {\n parser = outputType.parser;\n if (isTypeOf(outputType.type, APIResponse)) {\n if (response.status === outputType.type.status) {\n output = outputType.type;\n break;\n }\n }\n } else if (isTypeOf(outputType, APIResponse)) {\n if (response.status === outputType.status) {\n output = outputType;\n break;\n }\n }\n }\n\n\n if (response.ok) {\n const apiresponse = (output || APIResponse).create(\n response.status,\n await parser(response),\n );\n <% if(throwError) { %>\n return <% if (outputType === 'default') { %>apiresponse as Extract<InstanceType<T>, SuccessfulResponse><% } else { %>apiresponse as Extract<InstanceType<T>, SuccessfulResponse>;<% } %>;\n <% } else { %>\n return [<% if (outputType === 'default') { %>apiresponse.data as Extract<InstanceType<T>, SuccessfulResponse>['data']<% } else { %>apiresponse as Extract<InstanceType<T>, SuccessfulResponse><% } %>, null] as const;\n <% } %>\n }\n<% if(throwError) { %>\n throw (output || APIError).create(\n response.status,\n await parser(response),\n );\n<% } else { %>\n const data = (output || APIError).create(\n response.status,\n await parser(response),\n );\n return [null, data] as const;\n<% } %>\n}\n\nexport function isTypeOf<T extends Type<APIResponse>>(\n instance: any,\n baseType: T,\n): instance is T {\n if (instance === baseType) {\n return true;\n }\n const prototype = Object.getPrototypeOf(instance);\n if (prototype === null) {\n return false;\n }\n return isTypeOf(prototype, baseType);\n}\n\nexport class Dispatcher {\n #interceptors: Interceptor[] = [];\n #fetch: z.infer<typeof fetchType>;\n constructor(interceptors: Interceptor[], fetch?: z.infer<typeof fetchType>) {\n this.#interceptors = interceptors;\n this.#fetch = fetch;\n }\n\n async send<T extends OutputType[]>(\n config: RequestConfig,\n outputs: T,\n signal?: AbortSignal,\n ) {\n for (const interceptor of this.#interceptors) {\n if (interceptor.before) {\n config = await interceptor.before(config);\n }\n }\n\n let response = await (this.#fetch ?? fetch)(\n new Request(config.url, config.init),\n {\n ...config.init,\n signal: signal,\n },\n );\n\n for (let i = this.#interceptors.length - 1; i >= 0; i--) {\n const interceptor = this.#interceptors[i];\n if (interceptor.after) {\n response = await interceptor.after(response.clone());\n }\n }\n\n return await parse(outputs, response);\n }\n}\n";
1216
1236
 
1217
1237
  // packages/typescript/src/lib/http/interceptors.txt
1218
- var interceptors_default = "export interface Interceptor {\n before?: (config: RequestConfig) => Promise<RequestConfig> | RequestConfig;\n after?: (response: Response) => Promise<Response> | Response;\n}\n\nexport const createHeadersInterceptor = (\n defaultHeaders: () => Record<string, string | undefined>,\n requestHeaders: HeadersInit,\n):Interceptor => {\n return {\n before({init, url}) {\n // Priority Levels\n // 1. Headers Input\n // 2. Request Headers\n // 3. Default Headers\n const headers = defaultHeaders();\n\n for (const [key, value] of new Headers(requestHeaders)) {\n // Only set the header if it doesn't already exist and has a value\n // even though these headers are passed at operation level\n // still they are lower priority compared to the headers input\n if (value !== undefined && !init.headers.has(key)) {\n init.headers.set(key, value);\n }\n }\n\n for (const [key, value] of Object.entries(headers)) {\n // Only set the header if it doesn't already exist and has a value\n if (value !== undefined && !init.headers.has(key)) {\n init.headers.set(key, value);\n }\n }\n\n return {init, url};\n },\n };\n};\n\nexport const createBaseUrlInterceptor = (\n getBaseUrl: () => string,\n): Interceptor => {\n return {\n before({ init, url }) {\n const baseUrl = getBaseUrl();\n if (url.protocol === 'local:') {\n return {\n init,\n url: new URL(url.href.replace('local://', baseUrl))\n };\n }\n return { init, url };\n },\n };\n};\n\nexport const logInterceptor: Interceptor = {\n before({ url, init }) {\n console.log('Request:', { url, init });\n return { url, init };\n },\n after(response) {\n console.log('Response:', response);\n return response;\n },\n};\n\n/**\n * Creates an interceptor that logs detailed information about requests and responses.\n * @param options Configuration options for the logger\n * @returns An interceptor object with before and after handlers\n */\nexport const createDetailedLogInterceptor = (options?: {\n logLevel?: 'debug' | 'info' | 'warn' | 'error';\n includeRequestBody?: boolean;\n includeResponseBody?: boolean;\n}) => {\n const logLevel = options?.logLevel || 'info';\n const includeRequestBody = options?.includeRequestBody || false;\n const includeResponseBody = options?.includeResponseBody || false;\n\n return {\n async before(request: Request) {\n const logData = {\n url: request.url,\n method: request.method,\n contentType: request.headers.get('Content-Type'),\n headers: Object.fromEntries([...request.headers.entries()]),\n };\n\n console[logLevel]('\u{1F680} Outgoing Request:', logData);\n\n if (includeRequestBody) {\n try {\n // Clone the request to avoid consuming the body stream\n const clonedRequest = request.clone();\n if (clonedRequest.headers.get('Content-Type')?.includes('application/json')) {\n const body = await clonedRequest.json().catch(() => null);\n console[logLevel]('Request Body:', body);\n } else {\n const body = await clonedRequest.text().catch(() => null);\n console[logLevel]('Request Body:', body);\n }\n } catch (error) {\n console.error('Could not log request body:', error);\n }\n }\n\n return request;\n },\n\n async after(response: Response) {\n const logData = {\n status: response.status,\n statusText: response.statusText,\n url: response.url,\n headers: Object.fromEntries([...response.headers.entries()]),\n };\n\n console[logLevel]('\u{1F4E5} Incoming Response:', logData);\n\n if (includeResponseBody && response.body) {\n try {\n // Clone the response to avoid consuming the body stream\n const clonedResponse = response.clone();\n if (clonedResponse.headers.get('Content-Type')?.includes('application/json')) {\n const body = await clonedResponse.json().catch(() => null);\n console[logLevel]('Response Body:', body);\n } else {\n const body = await clonedResponse.text().catch(() => null);\n if (body) {\n console[logLevel]('Response Body:', body.substring(0, 500) + (body.length > 500 ? '...' : ''));\n } else {\n console[logLevel]('No response body');\n }\n }\n } catch (error) {\n console.error('Could not log response body:', error);\n }\n }\n\n return response;\n },\n };\n};\n";
1238
+ var interceptors_default = "export interface Interceptor {\n before?: (config: RequestConfig) => Promise<RequestConfig> | RequestConfig;\n after?: (response: Response) => Promise<Response> | Response;\n}\n\nexport const createHeadersInterceptor = (\n headers: Record<string, string | undefined>,\n requestHeaders: HeadersInit,\n):Interceptor => {\n return {\n before({init, url}) {\n // Priority Levels\n // 1. Headers Input\n // 2. Request Headers\n // 3. Default Headers\n\n for (const [key, value] of new Headers(requestHeaders)) {\n // Only set the header if it doesn't already exist and has a value\n // even though these headers are passed at operation level\n // still they are lower priority compared to the headers input\n if (value !== undefined && !init.headers.has(key)) {\n init.headers.set(key, value);\n }\n }\n\n for (const [key, value] of Object.entries(headers)) {\n // Only set the header if it doesn't already exist and has a value\n if (value !== undefined && !init.headers.has(key)) {\n init.headers.set(key, value);\n }\n }\n\n return {init, url};\n },\n };\n};\n\nexport const createBaseUrlInterceptor = (baseUrl: string): Interceptor => {\n return {\n before({ init, url }) {\n if (url.protocol === 'local:') {\n return {\n init,\n url: new URL(url.href.replace('local://', baseUrl))\n };\n }\n return { init, url };\n },\n };\n};\n\nexport const logInterceptor: Interceptor = {\n before({ url, init }) {\n console.log('Request:', { url, init });\n return { url, init };\n },\n after(response) {\n console.log('Response:', response);\n return response;\n },\n};\n\n/**\n * Creates an interceptor that logs detailed information about requests and responses.\n * @param options Configuration options for the logger\n * @returns An interceptor object with before and after handlers\n */\nexport const createDetailedLogInterceptor = (options?: {\n logLevel?: 'debug' | 'info' | 'warn' | 'error';\n includeRequestBody?: boolean;\n includeResponseBody?: boolean;\n}) => {\n const logLevel = options?.logLevel || 'info';\n const includeRequestBody = options?.includeRequestBody || false;\n const includeResponseBody = options?.includeResponseBody || false;\n\n return {\n async before(request: Request) {\n const logData = {\n url: request.url,\n method: request.method,\n contentType: request.headers.get('Content-Type'),\n headers: Object.fromEntries([...request.headers.entries()]),\n };\n\n console[logLevel]('\u{1F680} Outgoing Request:', logData);\n\n if (includeRequestBody) {\n try {\n // Clone the request to avoid consuming the body stream\n const clonedRequest = request.clone();\n if (clonedRequest.headers.get('Content-Type')?.includes('application/json')) {\n const body = await clonedRequest.json().catch(() => null);\n console[logLevel]('Request Body:', body);\n } else {\n const body = await clonedRequest.text().catch(() => null);\n console[logLevel]('Request Body:', body);\n }\n } catch (error) {\n console.error('Could not log request body:', error);\n }\n }\n\n return request;\n },\n\n async after(response: Response) {\n const logData = {\n status: response.status,\n statusText: response.statusText,\n url: response.url,\n headers: Object.fromEntries([...response.headers.entries()]),\n };\n\n console[logLevel]('\u{1F4E5} Incoming Response:', logData);\n\n if (includeResponseBody && response.body) {\n try {\n // Clone the response to avoid consuming the body stream\n const clonedResponse = response.clone();\n if (clonedResponse.headers.get('Content-Type')?.includes('application/json')) {\n const body = await clonedResponse.json().catch(() => null);\n console[logLevel]('Response Body:', body);\n } else {\n const body = await clonedResponse.text().catch(() => null);\n if (body) {\n console[logLevel]('Response Body:', body.substring(0, 500) + (body.length > 500 ? '...' : ''));\n } else {\n console[logLevel]('No response body');\n }\n }\n } catch (error) {\n console.error('Could not log response body:', error);\n }\n }\n\n return response;\n },\n };\n};\n";
1219
1239
 
1220
1240
  // packages/typescript/src/lib/http/parse-response.txt
1221
1241
  var parse_response_default = 'import { parse } from "fast-content-type-parse";\n\nasync function handleChunkedResponse(response: Response, contentType: string) {\n const { type } = parse(contentType);\n\n switch (type) {\n case "application/json": {\n let buffer = "";\n const reader = response.body!.getReader();\n const decoder = new TextDecoder();\n while (true) {\n const { value, done } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value);\n }\n return JSON.parse(buffer);\n }\n case "text/html":\n case "text/plain": {\n let buffer = "";\n const reader = response.body!.getReader();\n const decoder = new TextDecoder();\n while (true) {\n const { value, done } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value);\n }\n return buffer;\n }\n default:\n return response.body;\n }\n}\n\nexport function chunked(response: Response) {\n return response.body!;\n}\n\nexport async function buffered(response: Response) {\n const contentType = response.headers.get("Content-Type");\n if (!contentType) {\n throw new Error("Content-Type header is missing");\n }\n\n if (response.status === 204) {\n return null;\n }\n\n const { type } = parse(contentType);\n switch (type) {\n case "application/json":\n return response.json();\n case "text/plain":\n return response.text();\n case "text/html":\n return response.text();\n case "text/xml":\n case "application/xml":\n return response.text();\n case "application/x-www-form-urlencoded": {\n const text = await response.text();\n return Object.fromEntries(new URLSearchParams(text));\n }\n case "multipart/form-data":\n return response.formData();\n default:\n throw new Error(`Unsupported content type: ${contentType}`);\n }\n}\n';
@@ -3231,8 +3251,9 @@ ${utils_default}`
3231
3251
  await settings.writer(settings.output, configFiles);
3232
3252
  }
3233
3253
  if (settings.readme) {
3234
- await settings.writer(settings.mode === "full" ? settings.output : output, {
3235
- "README.md": toReadme(spec, new TypeScriptSnippet(spec, settings))
3254
+ const path = typeof settings.readme === "string" ? settings.readme : "README.md";
3255
+ await settings.writer(settings.output, {
3256
+ [path]: toReadme(spec, new TypeScriptSnippet(spec, settings))
3236
3257
  });
3237
3258
  }
3238
3259
  await settings.formatCode?.({