apigen-ts 1.3.1 → 1.5.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/dist/_template.ts CHANGED
@@ -54,11 +54,24 @@ export class ApiClient {
54
54
  return new URL(path, base)
55
55
  }
56
56
 
57
+ // same as "${v}" in path template, but keeps "/", "?", "#" inside the path segment
58
+ EncodePathParam(v: unknown) {
59
+ return encodeURIComponent(String(v))
60
+ }
61
+
62
+ // query params with explode: false, eg. Swagger 2 `collectionFormat: csv`
63
+ JoinQueryParam(v: unknown, sep: string) {
64
+ return Array.isArray(v) ? v.join(sep) : v
65
+ }
66
+
57
67
  async Fetch<T>(method: string, path: string, opts: ApigenRequest = {}): Promise<T> {
58
68
  const url = this.PrepareFetchUrl(path)
59
69
 
70
+ // skip omitted values; arrays repeat the key per item (OpenAPI default: form + explode)
60
71
  for (const [k, v] of Object.entries(opts?.search ?? {})) {
61
- url.searchParams.append(k, Array.isArray(v) ? v.join(",") : (v as string))
72
+ for (const x of Array.isArray(v) ? v : [v]) {
73
+ if (x !== undefined && x !== null) url.searchParams.append(k, x as string)
74
+ }
62
75
  }
63
76
 
64
77
  const configHeaders =
@@ -76,7 +89,7 @@ export class ApiClient {
76
89
  headers.delete("content-type")
77
90
  body = ct === "multipart/form-data" ? new FormData() : new URLSearchParams()
78
91
  for (const [k, v] of Object.entries(opts.body as Record<string, string>)) {
79
- body.append(k, v)
92
+ if (v !== undefined && v !== null) body.append(k, v)
80
93
  }
81
94
  }
82
95
 
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { a as apigen, g as getCliConfig } from './main-BiX2OIVV.js';
2
+ import { a as apigen, g as getCliConfig } from './main-Cn0HtcUt.js';
3
3
  import 'fs/promises';
4
4
  import 'path';
5
5
  import 'url';
@@ -4,13 +4,13 @@ import { fileURLToPath } from 'url';
4
4
  import { cli } from 'cleye';
5
5
  import { bundle, BaseResolver, createConfig } from '@redocly/openapi-core';
6
6
  import { filterEmpty, filterNullable } from 'array-utils-ts';
7
- import { get, uniq, upperFirst, isArray, isBoolean, sortBy, isObject, lowerFirst, uniqBy } from 'lodash-es';
7
+ import { get, uniq, upperFirst, isBoolean, isArray, sortBy, isObject, lowerFirst, uniqBy } from 'lodash-es';
8
8
  import { convertObj } from 'swagger2openapi';
9
9
  import ts from 'typescript';
10
10
  import path from 'node:path';
11
11
 
12
12
  var name = "apigen-ts";
13
- var version = "1.3.1";
13
+ var version = "1.5.0";
14
14
 
15
15
  const initCtx = (config) => {
16
16
  return {
@@ -106,7 +106,8 @@ const getCliConfig = () => {
106
106
  };
107
107
 
108
108
  const unref = (ctx, s) => {
109
- if (!s) return void 0;
109
+ if (s === void 0) return void 0;
110
+ if (typeof s !== "object" || s === null) return s;
110
111
  if ("$ref" in s && s.$ref) {
111
112
  const parts = s.$ref.replace("#/", "").split("/");
112
113
  const obj = parts.reduce(
@@ -114,16 +115,24 @@ const unref = (ctx, s) => {
114
115
  (acc, x) => get(acc, x, get(acc, decodeURIComponent(x).replaceAll("~1", "/"))),
115
116
  ctx.doc
116
117
  );
117
- if (obj) return obj;
118
+ if (obj !== void 0) return obj;
118
119
  console.warn(`${ctx.logTag} ref ${s.$ref} not found`);
119
120
  return void 0;
120
121
  }
121
122
  return s;
122
123
  };
124
+ const getContent = (ctx, content) => {
125
+ const result = [];
126
+ for (const [cType, value] of Object.entries(content ?? {})) {
127
+ const mType = unref(ctx, value);
128
+ if (mType) result.push([cType.split(";")[0], mType]);
129
+ }
130
+ return result;
131
+ };
123
132
  const getReqSchema = (ctx, config) => {
124
133
  const req = unref(ctx, config.requestBody);
125
134
  if (!req) return void 0;
126
- const cts = Object.entries(req.content ?? {}).map((x) => [x[0].split(";")[0], x[1].schema]).filter((x) => x[1]);
135
+ const cts = getContent(ctx, req.content).map((x) => [x[0], x[1].schema]).filter((x) => x[1] !== void 0);
127
136
  if (cts.length === 0) return void 0;
128
137
  const pretenders = [
129
138
  "application/json",
@@ -138,12 +147,15 @@ const getReqSchema = (ctx, config) => {
138
147
  cts.map((x) => x[0]);
139
148
  return void 0;
140
149
  };
141
- const getRepSchema = (ctx, config) => {
150
+ const getRepContent = (ctx, config) => {
142
151
  const successCodes = Object.keys(config.responses ?? {}).filter((x) => x.startsWith("2")).filter((x) => get(config, ["responses", x]));
143
152
  const firstSuccess = unref(ctx, get(config, ["responses", successCodes[0]], {}));
144
- if (!firstSuccess || !("content" in firstSuccess)) return void 0;
145
- const cts = Object.entries(firstSuccess.content ?? {}).filter((x) => x[1].schema);
146
- if (cts.length === 0) return void 0;
153
+ if (!firstSuccess || !("content" in firstSuccess)) return [];
154
+ return getContent(ctx, firstSuccess.content);
155
+ };
156
+ const getRepSchema = (ctx, config) => {
157
+ const cts = getRepContent(ctx, config).filter((x) => x[1].schema !== void 0);
158
+ if (!cts.length) return void 0;
147
159
  const ctJson = cts.find((x) => x[0].startsWith("application/json"));
148
160
  if (ctJson) return ctJson[1].schema;
149
161
  const ctText = cts.find((x) => x[0].startsWith("text/"));
@@ -213,10 +225,22 @@ const Keywords = /* @__PURE__ */ new Set([
213
225
  "Exclude",
214
226
  "Extract",
215
227
  // ts keywords
228
+ "Array",
216
229
  "Date",
217
- "object",
218
- "Response"
219
- // ts type names
230
+ "File",
231
+ "FormData",
232
+ "Headers",
233
+ "JSON",
234
+ "Object",
235
+ "Promise",
236
+ "RequestInit",
237
+ "Response",
238
+ "URL",
239
+ "URLSearchParams",
240
+ "fetch",
241
+ "globalThis",
242
+ "object"
243
+ // runtime names
220
244
  ]);
221
245
  const normalizeIdentifier = (val, asVar = false) => {
222
246
  let name = val.replace("#/components/schemas/", "").replaceAll("'", "").replace(/[^a-zA-Z0-9]/g, "_");
@@ -225,7 +249,7 @@ const normalizeIdentifier = (val, asVar = false) => {
225
249
  return name;
226
250
  };
227
251
  const makeInlineEnum = (s) => {
228
- if (!s.enum) return void 0;
252
+ if (isBoolean(s) || !s.enum) return void 0;
229
253
  const values = filterEmpty(s.enum);
230
254
  if (!values.length) return void 0;
231
255
  if (!s.type) {
@@ -258,8 +282,16 @@ const makeInlineEnum = (s) => {
258
282
  console.warn(`enum with unknown type "${s.type}" in`, s);
259
283
  return void 0;
260
284
  };
285
+ const makeConst = (s) => {
286
+ if (isBoolean(s) || !("const" in s)) return void 0;
287
+ const value = s.const;
288
+ if (!["string", "number", "boolean"].includes(typeof value)) return void 0;
289
+ const t = makeInlineEnum({ ...s, enum: [value] });
290
+ return t && ts.isUnionTypeNode(t) ? t.types[0] : t;
291
+ };
261
292
  const makeObject = (ctx, s) => {
262
- if (s.type !== "object") throw new Error(`makeObject: not an object ${JSON.stringify(s)}`);
293
+ if (isBoolean(s) || s.type !== "object")
294
+ throw new Error(`makeObject: not an object ${JSON.stringify(s)}`);
263
295
  if (s.additionalProperties && !isBoolean(s.additionalProperties)) {
264
296
  return f$2.createTypeReferenceNode("Record", [
265
297
  f$2.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
@@ -278,19 +310,24 @@ const makeType = (ctx, s) => {
278
310
  const mk = makeType.bind(null, ctx);
279
311
  if (s === void 0) return f$2.createKeywordTypeNode(ts.SyntaxKind.VoidKeyword);
280
312
  if (s === null) return f$2.createLiteralTypeNode(f$2.createNull());
313
+ if (isBoolean(s)) {
314
+ return f$2.createKeywordTypeNode(s ? ts.SyntaxKind.UnknownKeyword : ts.SyntaxKind.NeverKeyword);
315
+ }
281
316
  if ("$ref" in s && s.$ref) {
282
317
  const parts = s.$ref.replace("#/", "").split("/");
283
318
  if (parts.length === 3 && parts[0] === "components" && parts[1] === "schemas") {
284
319
  return f$2.createTypeReferenceNode(normalizeIdentifier(parts[2], true));
285
320
  }
286
321
  const t = unref(ctx, s);
287
- if (!t) throw new Error(`makeTypeRef: ref not found ${JSON.stringify(s)}`);
322
+ if (t === void 0) throw new Error(`makeTypeRef: ref not found ${JSON.stringify(s)}`);
288
323
  return makeType(ctx, t);
289
324
  }
290
325
  if ("oneOf" in s && s.oneOf) return f$2.createUnionTypeNode(s.oneOf.map(mk));
291
326
  if ("anyOf" in s && s.anyOf) return f$2.createUnionTypeNode(s.anyOf.map(mk));
292
327
  if ("allOf" in s && s.allOf) return f$2.createIntersectionTypeNode(s.allOf.map(mk));
293
328
  if ("type" in s && s.type === "integer") s.type = "number";
329
+ const constant = makeConst(s);
330
+ if (constant) return constant;
294
331
  if ("enum" in s && s.enum && !Array.isArray(s.type)) {
295
332
  const isArray2 = s.type === "array";
296
333
  const t = makeInlineEnum(isArray2 ? { ...s, type: s.items?.type } : s);
@@ -314,7 +351,6 @@ const makeType = (ctx, s) => {
314
351
  if (s.type === "object") t = makeObject(ctx, s);
315
352
  else if (s.type === "boolean") t = f$2.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword);
316
353
  else if (s.type === "number") t = f$2.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword);
317
- else if (isConstantString(s)) t = f$2.createLiteralTypeNode(f$2.createStringLiteral(s.const));
318
354
  else if (s.type === "string") t = f$2.createKeywordTypeNode(ts.SyntaxKind.StringKeyword);
319
355
  else if (s.type === "null") t = f$2.createLiteralTypeNode(f$2.createNull());
320
356
  else if (isArray(s.type)) t = makeLiteralUnion(ctx, s.type);
@@ -337,16 +373,14 @@ const makeType = (ctx, s) => {
337
373
  return f$2.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword);
338
374
  };
339
375
  const isStringEnum = (s) => {
376
+ if (isBoolean(s)) return false;
340
377
  if ("enum" in s && s.enum) {
341
378
  return s.enum.every((x) => typeof x === "string");
342
379
  }
343
380
  return false;
344
381
  };
345
382
  const isPrefixItems = (s) => {
346
- return s.prefixItems !== void 0;
347
- };
348
- const isConstantString = (s) => {
349
- return s.type === "string" && s.const !== void 0;
383
+ return !isBoolean(s) && s.prefixItems !== void 0;
350
384
  };
351
385
  const makeTypeAlias = (ctx, name, s) => {
352
386
  if (isStringEnum(s) && !ctx.inlineEnums) {
@@ -372,7 +406,17 @@ const makeTypeAlias = (ctx, name, s) => {
372
406
  };
373
407
 
374
408
  const f$1 = ts.factory;
375
- const HttpMethods = ["get", "post", "put", "patch", "delete", "head", "options", "trace"];
409
+ const HttpMethods = [
410
+ "get",
411
+ "post",
412
+ "put",
413
+ "patch",
414
+ "delete",
415
+ "head",
416
+ "options",
417
+ "trace",
418
+ "query"
419
+ ];
376
420
  const normalizeOpName = (val) => {
377
421
  const articles = /* @__PURE__ */ new Set(["a", "an", "the"]);
378
422
  const tmp = val.replace(/'/, "").replace(/[^a-zA-Z0-9]/g, "_").split("_").filter((x) => x !== "" && !articles.has(x)).map((x) => upperFirst(x));
@@ -411,21 +455,39 @@ const prepareUrl = (url, rename) => {
411
455
  const [name, ...rest] = x.split("}");
412
456
  const right = rest.join("}");
413
457
  return f$1.createTemplateSpan(
414
- f$1.createIdentifier(name),
415
- // no normalization required
458
+ f$1.createCallExpression(f$1.createIdentifier("this.EncodePathParam"), void 0, [
459
+ f$1.createIdentifier(name)
460
+ // no normalization required
461
+ ]),
416
462
  i === parts.length - 2 ? f$1.createTemplateTail(right) : f$1.createTemplateMiddle(right)
417
463
  );
418
464
  })
419
465
  );
420
466
  };
421
- const prepareOp = (ctx, cfg, opName) => {
467
+ const prepareSearch = (params) => {
468
+ const separators = { form: ",", spaceDelimited: " ", pipeDelimited: "|" };
469
+ const joined = params.flatMap((x) => {
470
+ const style = x.style ?? "form";
471
+ if ((x.explode ?? style === "form") || !(style in separators)) return [];
472
+ const key = /^[a-zA-Z_$][\w$]*$/.test(x.name) ? f$1.createPropertyAccessExpression(f$1.createIdentifier("search"), x.name) : f$1.createElementAccessExpression(f$1.createIdentifier("search"), f$1.createStringLiteral(x.name));
473
+ const value = f$1.createCallExpression(f$1.createIdentifier("this.JoinQueryParam"), void 0, [
474
+ key,
475
+ f$1.createStringLiteral(separators[style])
476
+ ]);
477
+ return [f$1.createPropertyAssignment(f$1.createStringLiteral(x.name), value)];
478
+ });
479
+ if (!joined.length) return f$1.createShorthandPropertyAssignment("search");
480
+ const search = [f$1.createSpreadAssignment(f$1.createIdentifier("search")), ...joined];
481
+ return f$1.createPropertyAssignment("search", f$1.createObjectLiteralExpression(search));
482
+ };
483
+ const prepareOp = (ctx, cfg, opName, httpMethod) => {
422
484
  cfg.parameters = cfg.parameters ?? [];
423
485
  const reqSchema = getReqSchema(ctx, cfg);
424
486
  const repSchema = getRepSchema(ctx, cfg);
425
487
  const allParams = filterNullable(cfg.parameters.map((x) => unref(ctx, x)));
426
488
  const params = uniqBy(allParams.filter((x) => x.in === "path"), "name");
427
489
  if (reqSchema) params.push({ name: "body", schema: reqSchema[1] });
428
- const search = allParams.filter((x) => x.in === "query");
490
+ const search = uniqBy(allParams.filter((x) => x.in === "query"), "name");
429
491
  allParams.filter((x) => x.in === "header");
430
492
  for (const [name, v] of Object.entries({ search })) {
431
493
  if (!v.length) continue;
@@ -452,7 +514,7 @@ const prepareOp = (ctx, cfg, opName) => {
452
514
  }
453
515
  const cbArgs = filterNullable([
454
516
  ctx.fetchOptions ? f$1.createSpreadAssignment(f$1.createIdentifier("opts")) : void 0,
455
- search.length ? f$1.createShorthandPropertyAssignment("search") : void 0,
517
+ search.length ? prepareSearch(search) : void 0,
456
518
  reqSchema && f$1.createShorthandPropertyAssignment("body"),
457
519
  reqSchema && reqSchema[0] !== "application/json" ? f$1.createPropertyAssignment(
458
520
  "headers",
@@ -478,7 +540,7 @@ const prepareOp = (ctx, cfg, opName) => {
478
540
  f$1.createIdentifier("this.Fetch"),
479
541
  [makeType(ctx, repSchema)],
480
542
  [
481
- f$1.createStringLiteral(cfg.method),
543
+ f$1.createStringLiteral(httpMethod),
482
544
  // method
483
545
  prepareUrl(cfg.path, urlReplacements),
484
546
  // path
@@ -500,43 +562,49 @@ const prepareNs = (ctx, name, handlers) => {
500
562
  f$1.createObjectLiteralExpression(handlers)
501
563
  );
502
564
  };
565
+ const addOp = (ctx, routes, pathConfig, cfg, httpMethod) => {
566
+ ctx.logTag = `${httpMethod.padEnd(6, " ")} ${cfg.path}`;
567
+ if (pathConfig.parameters) {
568
+ cfg.parameters = [...cfg.parameters ?? [], ...pathConfig.parameters];
569
+ }
570
+ const opTags = cfg.tags ?? [];
571
+ if (ctx.includeTags?.length && !opTags.some((t) => ctx.includeTags.includes(t))) return;
572
+ if (ctx.excludeTags?.length && opTags.some((t) => ctx.excludeTags.includes(t))) return;
573
+ const [ns, op] = getOpName(ctx, cfg);
574
+ if (!routes[ns]) routes[ns] = [];
575
+ const joined = [ns, op].join(".");
576
+ if (ctx.usedNames.has(joined)) {
577
+ return;
578
+ } else {
579
+ ctx.usedNames.add(joined);
580
+ }
581
+ try {
582
+ routes[ns].push(prepareOp(ctx, cfg, op, httpMethod));
583
+ } catch (e) {
584
+ console.error(`${ctx.logTag} - ${e}`, cfg);
585
+ throw e;
586
+ }
587
+ };
503
588
  const prepareRoutes = async (ctx) => {
504
589
  const routes = {};
505
- for (const [path, pathConfig] of Object.entries(ctx.doc.paths ?? {})) {
590
+ for (const [path, value] of Object.entries(ctx.doc.paths ?? {})) {
506
591
  ctx.logTag = `${"[ALL]".toUpperCase().padEnd(6, " ")} ${path}`;
507
- if (!isObject(pathConfig)) continue;
592
+ if (!isObject(value)) continue;
508
593
  if (ctx.filterPaths) {
509
594
  const match = ctx.filterPaths instanceof RegExp ? ctx.filterPaths.test(path) : ctx.filterPaths(path);
510
595
  if (!match) continue;
511
596
  }
512
- if ("$ref" in pathConfig) {
513
- console.warn(`${ctx.logTag} $ref should be resolved before (skipping)`);
514
- continue;
515
- }
597
+ const pathConfig = unref(ctx, value);
598
+ if (!pathConfig) continue;
516
599
  for (const method of HttpMethods) {
517
- ctx.logTag = `${method.toUpperCase().padEnd(6, " ")} ${path}`;
518
600
  const config = pathConfig[method];
519
601
  if (!config) continue;
520
- if (pathConfig.parameters) {
521
- config.parameters = [...config.parameters ?? [], ...pathConfig.parameters];
522
- }
523
- const opTags = config.tags ?? [];
524
- if (ctx.includeTags?.length && !opTags.some((t) => ctx.includeTags.includes(t))) continue;
525
- if (ctx.excludeTags?.length && opTags.some((t) => ctx.excludeTags.includes(t))) continue;
526
- const [ns, op] = getOpName(ctx, { ...config, method, path });
527
- if (!routes[ns]) routes[ns] = [];
528
- const joined = [ns, op].join(".");
529
- if (ctx.usedNames.has(joined)) {
530
- continue;
531
- } else {
532
- ctx.usedNames.add(joined);
533
- }
534
- try {
535
- routes[ns].push(prepareOp(ctx, { ...config, method, path }, op));
536
- } catch (e) {
537
- console.error(`${ctx.logTag} - ${e}`, config);
538
- throw e;
539
- }
602
+ addOp(ctx, routes, pathConfig, { ...config, method, path }, method.toUpperCase());
603
+ }
604
+ for (const [method, value2] of Object.entries(pathConfig.additionalOperations ?? {})) {
605
+ const config = unref(ctx, value2);
606
+ if (!config) continue;
607
+ addOp(ctx, routes, pathConfig, { ...config, method, path }, method);
540
608
  }
541
609
  }
542
610
  return routes;
package/dist/main.cjs CHANGED
@@ -29,7 +29,8 @@ const initCtx = (config) => {
29
29
  };
30
30
 
31
31
  const unref = (ctx, s) => {
32
- if (!s) return void 0;
32
+ if (s === void 0) return void 0;
33
+ if (typeof s !== "object" || s === null) return s;
33
34
  if ("$ref" in s && s.$ref) {
34
35
  const parts = s.$ref.replace("#/", "").split("/");
35
36
  const obj = parts.reduce(
@@ -37,16 +38,24 @@ const unref = (ctx, s) => {
37
38
  (acc, x) => lodashEs.get(acc, x, lodashEs.get(acc, decodeURIComponent(x).replaceAll("~1", "/"))),
38
39
  ctx.doc
39
40
  );
40
- if (obj) return obj;
41
+ if (obj !== void 0) return obj;
41
42
  console.warn(`${ctx.logTag} ref ${s.$ref} not found`);
42
43
  return void 0;
43
44
  }
44
45
  return s;
45
46
  };
47
+ const getContent = (ctx, content) => {
48
+ const result = [];
49
+ for (const [cType, value] of Object.entries(content ?? {})) {
50
+ const mType = unref(ctx, value);
51
+ if (mType) result.push([cType.split(";")[0], mType]);
52
+ }
53
+ return result;
54
+ };
46
55
  const getReqSchema = (ctx, config) => {
47
56
  const req = unref(ctx, config.requestBody);
48
57
  if (!req) return void 0;
49
- const cts = Object.entries(req.content ?? {}).map((x) => [x[0].split(";")[0], x[1].schema]).filter((x) => x[1]);
58
+ const cts = getContent(ctx, req.content).map((x) => [x[0], x[1].schema]).filter((x) => x[1] !== void 0);
50
59
  if (cts.length === 0) return void 0;
51
60
  const pretenders = [
52
61
  "application/json",
@@ -61,12 +70,15 @@ const getReqSchema = (ctx, config) => {
61
70
  cts.map((x) => x[0]);
62
71
  return void 0;
63
72
  };
64
- const getRepSchema = (ctx, config) => {
73
+ const getRepContent = (ctx, config) => {
65
74
  const successCodes = Object.keys(config.responses ?? {}).filter((x) => x.startsWith("2")).filter((x) => lodashEs.get(config, ["responses", x]));
66
75
  const firstSuccess = unref(ctx, lodashEs.get(config, ["responses", successCodes[0]], {}));
67
- if (!firstSuccess || !("content" in firstSuccess)) return void 0;
68
- const cts = Object.entries(firstSuccess.content ?? {}).filter((x) => x[1].schema);
69
- if (cts.length === 0) return void 0;
76
+ if (!firstSuccess || !("content" in firstSuccess)) return [];
77
+ return getContent(ctx, firstSuccess.content);
78
+ };
79
+ const getRepSchema = (ctx, config) => {
80
+ const cts = getRepContent(ctx, config).filter((x) => x[1].schema !== void 0);
81
+ if (!cts.length) return void 0;
70
82
  const ctJson = cts.find((x) => x[0].startsWith("application/json"));
71
83
  if (ctJson) return ctJson[1].schema;
72
84
  const ctText = cts.find((x) => x[0].startsWith("text/"));
@@ -136,10 +148,22 @@ const Keywords = /* @__PURE__ */ new Set([
136
148
  "Exclude",
137
149
  "Extract",
138
150
  // ts keywords
151
+ "Array",
139
152
  "Date",
140
- "object",
141
- "Response"
142
- // ts type names
153
+ "File",
154
+ "FormData",
155
+ "Headers",
156
+ "JSON",
157
+ "Object",
158
+ "Promise",
159
+ "RequestInit",
160
+ "Response",
161
+ "URL",
162
+ "URLSearchParams",
163
+ "fetch",
164
+ "globalThis",
165
+ "object"
166
+ // runtime names
143
167
  ]);
144
168
  const normalizeIdentifier = (val, asVar = false) => {
145
169
  let name = val.replace("#/components/schemas/", "").replaceAll("'", "").replace(/[^a-zA-Z0-9]/g, "_");
@@ -148,7 +172,7 @@ const normalizeIdentifier = (val, asVar = false) => {
148
172
  return name;
149
173
  };
150
174
  const makeInlineEnum = (s) => {
151
- if (!s.enum) return void 0;
175
+ if (lodashEs.isBoolean(s) || !s.enum) return void 0;
152
176
  const values = arrayUtilsTs.filterEmpty(s.enum);
153
177
  if (!values.length) return void 0;
154
178
  if (!s.type) {
@@ -181,8 +205,16 @@ const makeInlineEnum = (s) => {
181
205
  console.warn(`enum with unknown type "${s.type}" in`, s);
182
206
  return void 0;
183
207
  };
208
+ const makeConst = (s) => {
209
+ if (lodashEs.isBoolean(s) || !("const" in s)) return void 0;
210
+ const value = s.const;
211
+ if (!["string", "number", "boolean"].includes(typeof value)) return void 0;
212
+ const t = makeInlineEnum({ ...s, enum: [value] });
213
+ return t && ts.isUnionTypeNode(t) ? t.types[0] : t;
214
+ };
184
215
  const makeObject = (ctx, s) => {
185
- if (s.type !== "object") throw new Error(`makeObject: not an object ${JSON.stringify(s)}`);
216
+ if (lodashEs.isBoolean(s) || s.type !== "object")
217
+ throw new Error(`makeObject: not an object ${JSON.stringify(s)}`);
186
218
  if (s.additionalProperties && !lodashEs.isBoolean(s.additionalProperties)) {
187
219
  return f$2.createTypeReferenceNode("Record", [
188
220
  f$2.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
@@ -201,19 +233,24 @@ const makeType = (ctx, s) => {
201
233
  const mk = makeType.bind(null, ctx);
202
234
  if (s === void 0) return f$2.createKeywordTypeNode(ts.SyntaxKind.VoidKeyword);
203
235
  if (s === null) return f$2.createLiteralTypeNode(f$2.createNull());
236
+ if (lodashEs.isBoolean(s)) {
237
+ return f$2.createKeywordTypeNode(s ? ts.SyntaxKind.UnknownKeyword : ts.SyntaxKind.NeverKeyword);
238
+ }
204
239
  if ("$ref" in s && s.$ref) {
205
240
  const parts = s.$ref.replace("#/", "").split("/");
206
241
  if (parts.length === 3 && parts[0] === "components" && parts[1] === "schemas") {
207
242
  return f$2.createTypeReferenceNode(normalizeIdentifier(parts[2], true));
208
243
  }
209
244
  const t = unref(ctx, s);
210
- if (!t) throw new Error(`makeTypeRef: ref not found ${JSON.stringify(s)}`);
245
+ if (t === void 0) throw new Error(`makeTypeRef: ref not found ${JSON.stringify(s)}`);
211
246
  return makeType(ctx, t);
212
247
  }
213
248
  if ("oneOf" in s && s.oneOf) return f$2.createUnionTypeNode(s.oneOf.map(mk));
214
249
  if ("anyOf" in s && s.anyOf) return f$2.createUnionTypeNode(s.anyOf.map(mk));
215
250
  if ("allOf" in s && s.allOf) return f$2.createIntersectionTypeNode(s.allOf.map(mk));
216
251
  if ("type" in s && s.type === "integer") s.type = "number";
252
+ const constant = makeConst(s);
253
+ if (constant) return constant;
217
254
  if ("enum" in s && s.enum && !Array.isArray(s.type)) {
218
255
  const isArray2 = s.type === "array";
219
256
  const t = makeInlineEnum(isArray2 ? { ...s, type: s.items?.type } : s);
@@ -237,7 +274,6 @@ const makeType = (ctx, s) => {
237
274
  if (s.type === "object") t = makeObject(ctx, s);
238
275
  else if (s.type === "boolean") t = f$2.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword);
239
276
  else if (s.type === "number") t = f$2.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword);
240
- else if (isConstantString(s)) t = f$2.createLiteralTypeNode(f$2.createStringLiteral(s.const));
241
277
  else if (s.type === "string") t = f$2.createKeywordTypeNode(ts.SyntaxKind.StringKeyword);
242
278
  else if (s.type === "null") t = f$2.createLiteralTypeNode(f$2.createNull());
243
279
  else if (lodashEs.isArray(s.type)) t = makeLiteralUnion(ctx, s.type);
@@ -260,16 +296,14 @@ const makeType = (ctx, s) => {
260
296
  return f$2.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword);
261
297
  };
262
298
  const isStringEnum = (s) => {
299
+ if (lodashEs.isBoolean(s)) return false;
263
300
  if ("enum" in s && s.enum) {
264
301
  return s.enum.every((x) => typeof x === "string");
265
302
  }
266
303
  return false;
267
304
  };
268
305
  const isPrefixItems = (s) => {
269
- return s.prefixItems !== void 0;
270
- };
271
- const isConstantString = (s) => {
272
- return s.type === "string" && s.const !== void 0;
306
+ return !lodashEs.isBoolean(s) && s.prefixItems !== void 0;
273
307
  };
274
308
  const makeTypeAlias = (ctx, name, s) => {
275
309
  if (isStringEnum(s) && !ctx.inlineEnums) {
@@ -295,7 +329,17 @@ const makeTypeAlias = (ctx, name, s) => {
295
329
  };
296
330
 
297
331
  const f$1 = ts.factory;
298
- const HttpMethods = ["get", "post", "put", "patch", "delete", "head", "options", "trace"];
332
+ const HttpMethods = [
333
+ "get",
334
+ "post",
335
+ "put",
336
+ "patch",
337
+ "delete",
338
+ "head",
339
+ "options",
340
+ "trace",
341
+ "query"
342
+ ];
299
343
  const normalizeOpName = (val) => {
300
344
  const articles = /* @__PURE__ */ new Set(["a", "an", "the"]);
301
345
  const tmp = val.replace(/'/, "").replace(/[^a-zA-Z0-9]/g, "_").split("_").filter((x) => x !== "" && !articles.has(x)).map((x) => lodashEs.upperFirst(x));
@@ -334,21 +378,39 @@ const prepareUrl = (url, rename) => {
334
378
  const [name, ...rest] = x.split("}");
335
379
  const right = rest.join("}");
336
380
  return f$1.createTemplateSpan(
337
- f$1.createIdentifier(name),
338
- // no normalization required
381
+ f$1.createCallExpression(f$1.createIdentifier("this.EncodePathParam"), void 0, [
382
+ f$1.createIdentifier(name)
383
+ // no normalization required
384
+ ]),
339
385
  i === parts.length - 2 ? f$1.createTemplateTail(right) : f$1.createTemplateMiddle(right)
340
386
  );
341
387
  })
342
388
  );
343
389
  };
344
- const prepareOp = (ctx, cfg, opName) => {
390
+ const prepareSearch = (params) => {
391
+ const separators = { form: ",", spaceDelimited: " ", pipeDelimited: "|" };
392
+ const joined = params.flatMap((x) => {
393
+ const style = x.style ?? "form";
394
+ if ((x.explode ?? style === "form") || !(style in separators)) return [];
395
+ const key = /^[a-zA-Z_$][\w$]*$/.test(x.name) ? f$1.createPropertyAccessExpression(f$1.createIdentifier("search"), x.name) : f$1.createElementAccessExpression(f$1.createIdentifier("search"), f$1.createStringLiteral(x.name));
396
+ const value = f$1.createCallExpression(f$1.createIdentifier("this.JoinQueryParam"), void 0, [
397
+ key,
398
+ f$1.createStringLiteral(separators[style])
399
+ ]);
400
+ return [f$1.createPropertyAssignment(f$1.createStringLiteral(x.name), value)];
401
+ });
402
+ if (!joined.length) return f$1.createShorthandPropertyAssignment("search");
403
+ const search = [f$1.createSpreadAssignment(f$1.createIdentifier("search")), ...joined];
404
+ return f$1.createPropertyAssignment("search", f$1.createObjectLiteralExpression(search));
405
+ };
406
+ const prepareOp = (ctx, cfg, opName, httpMethod) => {
345
407
  cfg.parameters = cfg.parameters ?? [];
346
408
  const reqSchema = getReqSchema(ctx, cfg);
347
409
  const repSchema = getRepSchema(ctx, cfg);
348
410
  const allParams = arrayUtilsTs.filterNullable(cfg.parameters.map((x) => unref(ctx, x)));
349
411
  const params = lodashEs.uniqBy(allParams.filter((x) => x.in === "path"), "name");
350
412
  if (reqSchema) params.push({ name: "body", schema: reqSchema[1] });
351
- const search = allParams.filter((x) => x.in === "query");
413
+ const search = lodashEs.uniqBy(allParams.filter((x) => x.in === "query"), "name");
352
414
  allParams.filter((x) => x.in === "header");
353
415
  for (const [name, v] of Object.entries({ search })) {
354
416
  if (!v.length) continue;
@@ -375,7 +437,7 @@ const prepareOp = (ctx, cfg, opName) => {
375
437
  }
376
438
  const cbArgs = arrayUtilsTs.filterNullable([
377
439
  ctx.fetchOptions ? f$1.createSpreadAssignment(f$1.createIdentifier("opts")) : void 0,
378
- search.length ? f$1.createShorthandPropertyAssignment("search") : void 0,
440
+ search.length ? prepareSearch(search) : void 0,
379
441
  reqSchema && f$1.createShorthandPropertyAssignment("body"),
380
442
  reqSchema && reqSchema[0] !== "application/json" ? f$1.createPropertyAssignment(
381
443
  "headers",
@@ -401,7 +463,7 @@ const prepareOp = (ctx, cfg, opName) => {
401
463
  f$1.createIdentifier("this.Fetch"),
402
464
  [makeType(ctx, repSchema)],
403
465
  [
404
- f$1.createStringLiteral(cfg.method),
466
+ f$1.createStringLiteral(httpMethod),
405
467
  // method
406
468
  prepareUrl(cfg.path, urlReplacements),
407
469
  // path
@@ -423,43 +485,49 @@ const prepareNs = (ctx, name, handlers) => {
423
485
  f$1.createObjectLiteralExpression(handlers)
424
486
  );
425
487
  };
488
+ const addOp = (ctx, routes, pathConfig, cfg, httpMethod) => {
489
+ ctx.logTag = `${httpMethod.padEnd(6, " ")} ${cfg.path}`;
490
+ if (pathConfig.parameters) {
491
+ cfg.parameters = [...cfg.parameters ?? [], ...pathConfig.parameters];
492
+ }
493
+ const opTags = cfg.tags ?? [];
494
+ if (ctx.includeTags?.length && !opTags.some((t) => ctx.includeTags.includes(t))) return;
495
+ if (ctx.excludeTags?.length && opTags.some((t) => ctx.excludeTags.includes(t))) return;
496
+ const [ns, op] = getOpName(ctx, cfg);
497
+ if (!routes[ns]) routes[ns] = [];
498
+ const joined = [ns, op].join(".");
499
+ if (ctx.usedNames.has(joined)) {
500
+ return;
501
+ } else {
502
+ ctx.usedNames.add(joined);
503
+ }
504
+ try {
505
+ routes[ns].push(prepareOp(ctx, cfg, op, httpMethod));
506
+ } catch (e) {
507
+ console.error(`${ctx.logTag} - ${e}`, cfg);
508
+ throw e;
509
+ }
510
+ };
426
511
  const prepareRoutes = async (ctx) => {
427
512
  const routes = {};
428
- for (const [path, pathConfig] of Object.entries(ctx.doc.paths ?? {})) {
513
+ for (const [path, value] of Object.entries(ctx.doc.paths ?? {})) {
429
514
  ctx.logTag = `${"[ALL]".toUpperCase().padEnd(6, " ")} ${path}`;
430
- if (!lodashEs.isObject(pathConfig)) continue;
515
+ if (!lodashEs.isObject(value)) continue;
431
516
  if (ctx.filterPaths) {
432
517
  const match = ctx.filterPaths instanceof RegExp ? ctx.filterPaths.test(path) : ctx.filterPaths(path);
433
518
  if (!match) continue;
434
519
  }
435
- if ("$ref" in pathConfig) {
436
- console.warn(`${ctx.logTag} $ref should be resolved before (skipping)`);
437
- continue;
438
- }
520
+ const pathConfig = unref(ctx, value);
521
+ if (!pathConfig) continue;
439
522
  for (const method of HttpMethods) {
440
- ctx.logTag = `${method.toUpperCase().padEnd(6, " ")} ${path}`;
441
523
  const config = pathConfig[method];
442
524
  if (!config) continue;
443
- if (pathConfig.parameters) {
444
- config.parameters = [...config.parameters ?? [], ...pathConfig.parameters];
445
- }
446
- const opTags = config.tags ?? [];
447
- if (ctx.includeTags?.length && !opTags.some((t) => ctx.includeTags.includes(t))) continue;
448
- if (ctx.excludeTags?.length && opTags.some((t) => ctx.excludeTags.includes(t))) continue;
449
- const [ns, op] = getOpName(ctx, { ...config, method, path });
450
- if (!routes[ns]) routes[ns] = [];
451
- const joined = [ns, op].join(".");
452
- if (ctx.usedNames.has(joined)) {
453
- continue;
454
- } else {
455
- ctx.usedNames.add(joined);
456
- }
457
- try {
458
- routes[ns].push(prepareOp(ctx, { ...config, method, path }, op));
459
- } catch (e) {
460
- console.error(`${ctx.logTag} - ${e}`, config);
461
- throw e;
462
- }
525
+ addOp(ctx, routes, pathConfig, { ...config, method, path }, method.toUpperCase());
526
+ }
527
+ for (const [method, value2] of Object.entries(pathConfig.additionalOperations ?? {})) {
528
+ const config = unref(ctx, value2);
529
+ if (!config) continue;
530
+ addOp(ctx, routes, pathConfig, { ...config, method, path }, method);
463
531
  }
464
532
  }
465
533
  return routes;
package/dist/main.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import 'fs/promises';
2
2
  import 'path';
3
3
  import 'url';
4
- export { a as apigen } from './main-BiX2OIVV.js';
4
+ export { a as apigen } from './main-Cn0HtcUt.js';
5
5
  import 'cleye';
6
6
  import '@redocly/openapi-core';
7
7
  import 'array-utils-ts';
package/dist/main.mjs CHANGED
@@ -4,7 +4,7 @@ import { fileURLToPath } from 'url';
4
4
  import 'cleye';
5
5
  import { bundle, BaseResolver, createConfig } from '@redocly/openapi-core';
6
6
  import { filterEmpty, filterNullable } from 'array-utils-ts';
7
- import { get, uniq, upperFirst, isArray, isBoolean, sortBy, isObject, lowerFirst, uniqBy } from 'lodash-es';
7
+ import { get, uniq, upperFirst, isBoolean, isArray, sortBy, isObject, lowerFirst, uniqBy } from 'lodash-es';
8
8
  import { convertObj } from 'swagger2openapi';
9
9
  import ts from 'typescript';
10
10
  import path from 'node:path';
@@ -26,7 +26,8 @@ const initCtx = (config) => {
26
26
  };
27
27
 
28
28
  const unref = (ctx, s) => {
29
- if (!s) return void 0;
29
+ if (s === void 0) return void 0;
30
+ if (typeof s !== "object" || s === null) return s;
30
31
  if ("$ref" in s && s.$ref) {
31
32
  const parts = s.$ref.replace("#/", "").split("/");
32
33
  const obj = parts.reduce(
@@ -34,16 +35,24 @@ const unref = (ctx, s) => {
34
35
  (acc, x) => get(acc, x, get(acc, decodeURIComponent(x).replaceAll("~1", "/"))),
35
36
  ctx.doc
36
37
  );
37
- if (obj) return obj;
38
+ if (obj !== void 0) return obj;
38
39
  console.warn(`${ctx.logTag} ref ${s.$ref} not found`);
39
40
  return void 0;
40
41
  }
41
42
  return s;
42
43
  };
44
+ const getContent = (ctx, content) => {
45
+ const result = [];
46
+ for (const [cType, value] of Object.entries(content ?? {})) {
47
+ const mType = unref(ctx, value);
48
+ if (mType) result.push([cType.split(";")[0], mType]);
49
+ }
50
+ return result;
51
+ };
43
52
  const getReqSchema = (ctx, config) => {
44
53
  const req = unref(ctx, config.requestBody);
45
54
  if (!req) return void 0;
46
- const cts = Object.entries(req.content ?? {}).map((x) => [x[0].split(";")[0], x[1].schema]).filter((x) => x[1]);
55
+ const cts = getContent(ctx, req.content).map((x) => [x[0], x[1].schema]).filter((x) => x[1] !== void 0);
47
56
  if (cts.length === 0) return void 0;
48
57
  const pretenders = [
49
58
  "application/json",
@@ -58,12 +67,15 @@ const getReqSchema = (ctx, config) => {
58
67
  cts.map((x) => x[0]);
59
68
  return void 0;
60
69
  };
61
- const getRepSchema = (ctx, config) => {
70
+ const getRepContent = (ctx, config) => {
62
71
  const successCodes = Object.keys(config.responses ?? {}).filter((x) => x.startsWith("2")).filter((x) => get(config, ["responses", x]));
63
72
  const firstSuccess = unref(ctx, get(config, ["responses", successCodes[0]], {}));
64
- if (!firstSuccess || !("content" in firstSuccess)) return void 0;
65
- const cts = Object.entries(firstSuccess.content ?? {}).filter((x) => x[1].schema);
66
- if (cts.length === 0) return void 0;
73
+ if (!firstSuccess || !("content" in firstSuccess)) return [];
74
+ return getContent(ctx, firstSuccess.content);
75
+ };
76
+ const getRepSchema = (ctx, config) => {
77
+ const cts = getRepContent(ctx, config).filter((x) => x[1].schema !== void 0);
78
+ if (!cts.length) return void 0;
67
79
  const ctJson = cts.find((x) => x[0].startsWith("application/json"));
68
80
  if (ctJson) return ctJson[1].schema;
69
81
  const ctText = cts.find((x) => x[0].startsWith("text/"));
@@ -133,10 +145,22 @@ const Keywords = /* @__PURE__ */ new Set([
133
145
  "Exclude",
134
146
  "Extract",
135
147
  // ts keywords
148
+ "Array",
136
149
  "Date",
137
- "object",
138
- "Response"
139
- // ts type names
150
+ "File",
151
+ "FormData",
152
+ "Headers",
153
+ "JSON",
154
+ "Object",
155
+ "Promise",
156
+ "RequestInit",
157
+ "Response",
158
+ "URL",
159
+ "URLSearchParams",
160
+ "fetch",
161
+ "globalThis",
162
+ "object"
163
+ // runtime names
140
164
  ]);
141
165
  const normalizeIdentifier = (val, asVar = false) => {
142
166
  let name = val.replace("#/components/schemas/", "").replaceAll("'", "").replace(/[^a-zA-Z0-9]/g, "_");
@@ -145,7 +169,7 @@ const normalizeIdentifier = (val, asVar = false) => {
145
169
  return name;
146
170
  };
147
171
  const makeInlineEnum = (s) => {
148
- if (!s.enum) return void 0;
172
+ if (isBoolean(s) || !s.enum) return void 0;
149
173
  const values = filterEmpty(s.enum);
150
174
  if (!values.length) return void 0;
151
175
  if (!s.type) {
@@ -178,8 +202,16 @@ const makeInlineEnum = (s) => {
178
202
  console.warn(`enum with unknown type "${s.type}" in`, s);
179
203
  return void 0;
180
204
  };
205
+ const makeConst = (s) => {
206
+ if (isBoolean(s) || !("const" in s)) return void 0;
207
+ const value = s.const;
208
+ if (!["string", "number", "boolean"].includes(typeof value)) return void 0;
209
+ const t = makeInlineEnum({ ...s, enum: [value] });
210
+ return t && ts.isUnionTypeNode(t) ? t.types[0] : t;
211
+ };
181
212
  const makeObject = (ctx, s) => {
182
- if (s.type !== "object") throw new Error(`makeObject: not an object ${JSON.stringify(s)}`);
213
+ if (isBoolean(s) || s.type !== "object")
214
+ throw new Error(`makeObject: not an object ${JSON.stringify(s)}`);
183
215
  if (s.additionalProperties && !isBoolean(s.additionalProperties)) {
184
216
  return f$2.createTypeReferenceNode("Record", [
185
217
  f$2.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
@@ -198,19 +230,24 @@ const makeType = (ctx, s) => {
198
230
  const mk = makeType.bind(null, ctx);
199
231
  if (s === void 0) return f$2.createKeywordTypeNode(ts.SyntaxKind.VoidKeyword);
200
232
  if (s === null) return f$2.createLiteralTypeNode(f$2.createNull());
233
+ if (isBoolean(s)) {
234
+ return f$2.createKeywordTypeNode(s ? ts.SyntaxKind.UnknownKeyword : ts.SyntaxKind.NeverKeyword);
235
+ }
201
236
  if ("$ref" in s && s.$ref) {
202
237
  const parts = s.$ref.replace("#/", "").split("/");
203
238
  if (parts.length === 3 && parts[0] === "components" && parts[1] === "schemas") {
204
239
  return f$2.createTypeReferenceNode(normalizeIdentifier(parts[2], true));
205
240
  }
206
241
  const t = unref(ctx, s);
207
- if (!t) throw new Error(`makeTypeRef: ref not found ${JSON.stringify(s)}`);
242
+ if (t === void 0) throw new Error(`makeTypeRef: ref not found ${JSON.stringify(s)}`);
208
243
  return makeType(ctx, t);
209
244
  }
210
245
  if ("oneOf" in s && s.oneOf) return f$2.createUnionTypeNode(s.oneOf.map(mk));
211
246
  if ("anyOf" in s && s.anyOf) return f$2.createUnionTypeNode(s.anyOf.map(mk));
212
247
  if ("allOf" in s && s.allOf) return f$2.createIntersectionTypeNode(s.allOf.map(mk));
213
248
  if ("type" in s && s.type === "integer") s.type = "number";
249
+ const constant = makeConst(s);
250
+ if (constant) return constant;
214
251
  if ("enum" in s && s.enum && !Array.isArray(s.type)) {
215
252
  const isArray2 = s.type === "array";
216
253
  const t = makeInlineEnum(isArray2 ? { ...s, type: s.items?.type } : s);
@@ -234,7 +271,6 @@ const makeType = (ctx, s) => {
234
271
  if (s.type === "object") t = makeObject(ctx, s);
235
272
  else if (s.type === "boolean") t = f$2.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword);
236
273
  else if (s.type === "number") t = f$2.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword);
237
- else if (isConstantString(s)) t = f$2.createLiteralTypeNode(f$2.createStringLiteral(s.const));
238
274
  else if (s.type === "string") t = f$2.createKeywordTypeNode(ts.SyntaxKind.StringKeyword);
239
275
  else if (s.type === "null") t = f$2.createLiteralTypeNode(f$2.createNull());
240
276
  else if (isArray(s.type)) t = makeLiteralUnion(ctx, s.type);
@@ -257,16 +293,14 @@ const makeType = (ctx, s) => {
257
293
  return f$2.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword);
258
294
  };
259
295
  const isStringEnum = (s) => {
296
+ if (isBoolean(s)) return false;
260
297
  if ("enum" in s && s.enum) {
261
298
  return s.enum.every((x) => typeof x === "string");
262
299
  }
263
300
  return false;
264
301
  };
265
302
  const isPrefixItems = (s) => {
266
- return s.prefixItems !== void 0;
267
- };
268
- const isConstantString = (s) => {
269
- return s.type === "string" && s.const !== void 0;
303
+ return !isBoolean(s) && s.prefixItems !== void 0;
270
304
  };
271
305
  const makeTypeAlias = (ctx, name, s) => {
272
306
  if (isStringEnum(s) && !ctx.inlineEnums) {
@@ -292,7 +326,17 @@ const makeTypeAlias = (ctx, name, s) => {
292
326
  };
293
327
 
294
328
  const f$1 = ts.factory;
295
- const HttpMethods = ["get", "post", "put", "patch", "delete", "head", "options", "trace"];
329
+ const HttpMethods = [
330
+ "get",
331
+ "post",
332
+ "put",
333
+ "patch",
334
+ "delete",
335
+ "head",
336
+ "options",
337
+ "trace",
338
+ "query"
339
+ ];
296
340
  const normalizeOpName = (val) => {
297
341
  const articles = /* @__PURE__ */ new Set(["a", "an", "the"]);
298
342
  const tmp = val.replace(/'/, "").replace(/[^a-zA-Z0-9]/g, "_").split("_").filter((x) => x !== "" && !articles.has(x)).map((x) => upperFirst(x));
@@ -331,21 +375,39 @@ const prepareUrl = (url, rename) => {
331
375
  const [name, ...rest] = x.split("}");
332
376
  const right = rest.join("}");
333
377
  return f$1.createTemplateSpan(
334
- f$1.createIdentifier(name),
335
- // no normalization required
378
+ f$1.createCallExpression(f$1.createIdentifier("this.EncodePathParam"), void 0, [
379
+ f$1.createIdentifier(name)
380
+ // no normalization required
381
+ ]),
336
382
  i === parts.length - 2 ? f$1.createTemplateTail(right) : f$1.createTemplateMiddle(right)
337
383
  );
338
384
  })
339
385
  );
340
386
  };
341
- const prepareOp = (ctx, cfg, opName) => {
387
+ const prepareSearch = (params) => {
388
+ const separators = { form: ",", spaceDelimited: " ", pipeDelimited: "|" };
389
+ const joined = params.flatMap((x) => {
390
+ const style = x.style ?? "form";
391
+ if ((x.explode ?? style === "form") || !(style in separators)) return [];
392
+ const key = /^[a-zA-Z_$][\w$]*$/.test(x.name) ? f$1.createPropertyAccessExpression(f$1.createIdentifier("search"), x.name) : f$1.createElementAccessExpression(f$1.createIdentifier("search"), f$1.createStringLiteral(x.name));
393
+ const value = f$1.createCallExpression(f$1.createIdentifier("this.JoinQueryParam"), void 0, [
394
+ key,
395
+ f$1.createStringLiteral(separators[style])
396
+ ]);
397
+ return [f$1.createPropertyAssignment(f$1.createStringLiteral(x.name), value)];
398
+ });
399
+ if (!joined.length) return f$1.createShorthandPropertyAssignment("search");
400
+ const search = [f$1.createSpreadAssignment(f$1.createIdentifier("search")), ...joined];
401
+ return f$1.createPropertyAssignment("search", f$1.createObjectLiteralExpression(search));
402
+ };
403
+ const prepareOp = (ctx, cfg, opName, httpMethod) => {
342
404
  cfg.parameters = cfg.parameters ?? [];
343
405
  const reqSchema = getReqSchema(ctx, cfg);
344
406
  const repSchema = getRepSchema(ctx, cfg);
345
407
  const allParams = filterNullable(cfg.parameters.map((x) => unref(ctx, x)));
346
408
  const params = uniqBy(allParams.filter((x) => x.in === "path"), "name");
347
409
  if (reqSchema) params.push({ name: "body", schema: reqSchema[1] });
348
- const search = allParams.filter((x) => x.in === "query");
410
+ const search = uniqBy(allParams.filter((x) => x.in === "query"), "name");
349
411
  allParams.filter((x) => x.in === "header");
350
412
  for (const [name, v] of Object.entries({ search })) {
351
413
  if (!v.length) continue;
@@ -372,7 +434,7 @@ const prepareOp = (ctx, cfg, opName) => {
372
434
  }
373
435
  const cbArgs = filterNullable([
374
436
  ctx.fetchOptions ? f$1.createSpreadAssignment(f$1.createIdentifier("opts")) : void 0,
375
- search.length ? f$1.createShorthandPropertyAssignment("search") : void 0,
437
+ search.length ? prepareSearch(search) : void 0,
376
438
  reqSchema && f$1.createShorthandPropertyAssignment("body"),
377
439
  reqSchema && reqSchema[0] !== "application/json" ? f$1.createPropertyAssignment(
378
440
  "headers",
@@ -398,7 +460,7 @@ const prepareOp = (ctx, cfg, opName) => {
398
460
  f$1.createIdentifier("this.Fetch"),
399
461
  [makeType(ctx, repSchema)],
400
462
  [
401
- f$1.createStringLiteral(cfg.method),
463
+ f$1.createStringLiteral(httpMethod),
402
464
  // method
403
465
  prepareUrl(cfg.path, urlReplacements),
404
466
  // path
@@ -420,43 +482,49 @@ const prepareNs = (ctx, name, handlers) => {
420
482
  f$1.createObjectLiteralExpression(handlers)
421
483
  );
422
484
  };
485
+ const addOp = (ctx, routes, pathConfig, cfg, httpMethod) => {
486
+ ctx.logTag = `${httpMethod.padEnd(6, " ")} ${cfg.path}`;
487
+ if (pathConfig.parameters) {
488
+ cfg.parameters = [...cfg.parameters ?? [], ...pathConfig.parameters];
489
+ }
490
+ const opTags = cfg.tags ?? [];
491
+ if (ctx.includeTags?.length && !opTags.some((t) => ctx.includeTags.includes(t))) return;
492
+ if (ctx.excludeTags?.length && opTags.some((t) => ctx.excludeTags.includes(t))) return;
493
+ const [ns, op] = getOpName(ctx, cfg);
494
+ if (!routes[ns]) routes[ns] = [];
495
+ const joined = [ns, op].join(".");
496
+ if (ctx.usedNames.has(joined)) {
497
+ return;
498
+ } else {
499
+ ctx.usedNames.add(joined);
500
+ }
501
+ try {
502
+ routes[ns].push(prepareOp(ctx, cfg, op, httpMethod));
503
+ } catch (e) {
504
+ console.error(`${ctx.logTag} - ${e}`, cfg);
505
+ throw e;
506
+ }
507
+ };
423
508
  const prepareRoutes = async (ctx) => {
424
509
  const routes = {};
425
- for (const [path, pathConfig] of Object.entries(ctx.doc.paths ?? {})) {
510
+ for (const [path, value] of Object.entries(ctx.doc.paths ?? {})) {
426
511
  ctx.logTag = `${"[ALL]".toUpperCase().padEnd(6, " ")} ${path}`;
427
- if (!isObject(pathConfig)) continue;
512
+ if (!isObject(value)) continue;
428
513
  if (ctx.filterPaths) {
429
514
  const match = ctx.filterPaths instanceof RegExp ? ctx.filterPaths.test(path) : ctx.filterPaths(path);
430
515
  if (!match) continue;
431
516
  }
432
- if ("$ref" in pathConfig) {
433
- console.warn(`${ctx.logTag} $ref should be resolved before (skipping)`);
434
- continue;
435
- }
517
+ const pathConfig = unref(ctx, value);
518
+ if (!pathConfig) continue;
436
519
  for (const method of HttpMethods) {
437
- ctx.logTag = `${method.toUpperCase().padEnd(6, " ")} ${path}`;
438
520
  const config = pathConfig[method];
439
521
  if (!config) continue;
440
- if (pathConfig.parameters) {
441
- config.parameters = [...config.parameters ?? [], ...pathConfig.parameters];
442
- }
443
- const opTags = config.tags ?? [];
444
- if (ctx.includeTags?.length && !opTags.some((t) => ctx.includeTags.includes(t))) continue;
445
- if (ctx.excludeTags?.length && opTags.some((t) => ctx.excludeTags.includes(t))) continue;
446
- const [ns, op] = getOpName(ctx, { ...config, method, path });
447
- if (!routes[ns]) routes[ns] = [];
448
- const joined = [ns, op].join(".");
449
- if (ctx.usedNames.has(joined)) {
450
- continue;
451
- } else {
452
- ctx.usedNames.add(joined);
453
- }
454
- try {
455
- routes[ns].push(prepareOp(ctx, { ...config, method, path }, op));
456
- } catch (e) {
457
- console.error(`${ctx.logTag} - ${e}`, config);
458
- throw e;
459
- }
522
+ addOp(ctx, routes, pathConfig, { ...config, method, path }, method.toUpperCase());
523
+ }
524
+ for (const [method, value2] of Object.entries(pathConfig.additionalOperations ?? {})) {
525
+ const config = unref(ctx, value2);
526
+ if (!config) continue;
527
+ addOp(ctx, routes, pathConfig, { ...config, method, path }, method);
460
528
  }
461
529
  }
462
530
  return routes;
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "apigen-ts",
4
- "version": "1.3.1",
4
+ "version": "1.5.0",
5
5
  "license": "MIT",
6
6
  "author": "vladkens <v.pronsky@gmail.com>",
7
- "repository": "vladkens/apigen-ts",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "vladkens/apigen-ts"
10
+ },
8
11
  "description": "Simple typed OpenAPI client generator",
9
12
  "keywords": [
10
13
  "openapi",
@@ -14,23 +17,26 @@
14
17
  "generator",
15
18
  "codegen"
16
19
  ],
20
+ "engines": {
21
+ "node": ">=22"
22
+ },
17
23
  "dependencies": {
18
- "@redocly/openapi-core": "2.31.5",
24
+ "@redocly/openapi-core": "2.76.0",
19
25
  "@types/lodash-es": "4.17.12",
20
26
  "@types/swagger2openapi": "7.0.4",
21
27
  "array-utils-ts": "1.1.0",
22
- "cleye": "2.6.0",
28
+ "cleye": "2.7.0",
23
29
  "lodash-es": "4.18.1",
24
30
  "swagger2openapi": "7.0.8"
25
31
  },
26
32
  "devDependencies": {
27
- "@types/node": "25.9.1",
33
+ "@types/node": "26.6.6",
28
34
  "fetch-mock": "12.6.0",
29
- "npm-check-updates": "22.2.1",
30
- "pkgroll": "2.27.1",
31
- "prettier": "3.8.3",
35
+ "npm-check-updates": "23.1.1",
36
+ "pkgroll": "2.29.0",
37
+ "prettier": "3.9.10",
32
38
  "prettier-plugin-organize-imports": "4.3.0",
33
- "tsx": "4.22.4",
39
+ "tsx": "4.23.15",
34
40
  "typescript": "6.0.3"
35
41
  },
36
42
  "peerDependencies": {
package/readme.md CHANGED
@@ -3,7 +3,7 @@
3
3
  <div align="center">
4
4
 
5
5
  [<img src="https://badges.ws/npm/v/apigen-ts" alt="version" />](https://npmjs.org/package/apigen-ts)
6
- [<img src="https://packagephobia.com/badge?p=array-utils-ts" alt="size" />](https://packagephobia.now.sh/result?p=apigen-ts)
6
+ [<img src="https://badges.ws/packagephobia/publish/apigen-ts" alt="size" />](https://packagephobia.com/result?p=apigen-ts)
7
7
  [<img src="https://badges.ws/npm/dm/apigen-ts" alt="downloads" />](https://npmjs.org/package/apigen-ts)
8
8
  [<img src="https://badges.ws/github/license/vladkens/apigen-ts" alt="license" />](https://github.com/vladkens/apigen-ts/blob/main/LICENSE)
9
9
  [<img src="https://badges.ws/badge/-/buy%20me%20a%20coffee/ff813f?icon=buymeacoffee&label" alt="donate" />](https://buymeacoffee.com/vladkens)
@@ -18,7 +18,7 @@ Turn your OpenAPI spec into a typed TypeScript client with one command.
18
18
 
19
19
  - **One file.** Outputs a single `api-client.ts` — no scattered modules, no runtime deps in generated code.
20
20
  - **Fully typed.** Every method returns the exact response type from your schema. No casting, no `any`.
21
- - **Pure Node.js.** No Java, no Docker. Works with `npx` in any project.
21
+ - **Pure Node.js.** No Java, no Docker. Works with `pnpm` in any project.
22
22
  - **Fetch-based.** Uses native `fetch`. Override it with your own function for auth, retries, or logging.
23
23
  - **All OpenAPI versions.** Supports v2 (Swagger), v3, and v3.1 — auto-upgrades v2 on the fly.
24
24
  - **Extras built in.** Automatic date parsing, string literal unions instead of enums, Prettier formatting.
@@ -29,7 +29,7 @@ Unlike `openapi-typescript`, it generates a ready-to-call client — not just ty
29
29
  ## Install
30
30
 
31
31
  ```sh
32
- npm i -D apigen-ts
32
+ pnpm add -D apigen-ts
33
33
  ```
34
34
 
35
35
  ## Usage
@@ -38,16 +38,16 @@ npm i -D apigen-ts
38
38
 
39
39
  ```sh
40
40
  # From a local file
41
- npx apigen-ts ./openapi.json ./api-client.ts
41
+ pnpm apigen-ts ./openapi.json ./api-client.ts
42
42
 
43
43
  # From a URL
44
- npx apigen-ts https://petstore3.swagger.io/api/v3/openapi.json ./api-client.ts
44
+ pnpm apigen-ts https://petstore3.swagger.io/api/v3/openapi.json ./api-client.ts
45
45
 
46
46
  # From a protected URL
47
- npx apigen-ts https://secret-api.example.com ./api-client.ts -H "x-api-key: secret-key"
47
+ pnpm apigen-ts https://secret-api.example.com ./api-client.ts -H "x-api-key: secret-key"
48
48
  ```
49
49
 
50
- Run `npx apigen-ts --help` for all options. See [generated examples](./examples/).
50
+ Run `pnpm apigen-ts --help` for all options. See [generated examples](./examples/).
51
51
 
52
52
  ### 2. Import
53
53
 
@@ -87,7 +87,7 @@ await api.protectedRoute.get() // authenticated
87
87
  ### Automatic date parsing
88
88
 
89
89
  ```sh
90
- npx apigen-ts ./openapi.json ./api-client.ts --parse-dates
90
+ pnpm apigen-ts ./openapi.json ./api-client.ts --parse-dates
91
91
  ```
92
92
 
93
93
  ```ts
@@ -100,7 +100,7 @@ const createdAt: Date = pet.createdAt // parsed from format=date-time string
100
100
  Pass `--inline-enums` to generate string literal unions — useful for Node.js [type stripping](https://nodejs.org/api/typescript.html#type-stripping):
101
101
 
102
102
  ```sh
103
- npx apigen-ts ./openapi.json ./api-client.ts --inline-enums
103
+ pnpm apigen-ts ./openapi.json ./api-client.ts --inline-enums
104
104
  ```
105
105
 
106
106
  ```ts
@@ -119,17 +119,17 @@ enum MyEnum {
119
119
  Include only the endpoints you need — useful with large schemas (e.g. Cloudflare's 8 MB monolith):
120
120
 
121
121
  ```sh
122
- npx apigen-ts ./openapi.json ./api-client.ts --filter-paths '^/accounts'
122
+ pnpm apigen-ts ./openapi.json ./api-client.ts --filter-paths '^/accounts'
123
123
  ```
124
124
 
125
125
  ### Filter by tag
126
126
 
127
127
  ```sh
128
128
  # include only endpoints tagged "pets" or "store"
129
- npx apigen-ts ./openapi.json ./api-client.ts --include-tags pets,store
129
+ pnpm apigen-ts ./openapi.json ./api-client.ts --include-tags pets,store
130
130
 
131
131
  # exclude endpoints tagged "internal"
132
- npx apigen-ts ./openapi.json ./api-client.ts --exclude-tags internal
132
+ pnpm apigen-ts ./openapi.json ./api-client.ts --exclude-tags internal
133
133
  ```
134
134
 
135
135
  When both flags are set, `--exclude-tags` wins.
@@ -139,7 +139,7 @@ When both flags are set, `--exclude-tags` wins.
139
139
  Pass `--fetch-options` to add an optional last argument to every generated method, accepting any [`RequestInit`](https://developer.mozilla.org/en-US/docs/Web/API/RequestInit) field:
140
140
 
141
141
  ```sh
142
- npx apigen-ts ./openapi.json ./api-client.ts --fetch-options
142
+ pnpm apigen-ts ./openapi.json ./api-client.ts --fetch-options
143
143
  ```
144
144
 
145
145
  ```ts