oapiex 0.1.2 → 0.2.1

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/index.cjs CHANGED
@@ -36,10 +36,11 @@ node_path = __toESM(node_path);
36
36
  let node_fs_promises = require("node:fs/promises");
37
37
  node_fs_promises = __toESM(node_fs_promises);
38
38
  let _h3ravel_musket = require("@h3ravel/musket");
39
- let url = require("url");
40
39
  let prettier = require("prettier");
41
40
  prettier = __toESM(prettier);
42
41
  let node_url = require("node:url");
42
+ let node_fs = require("node:fs");
43
+ let url = require("url");
43
44
 
44
45
  //#region src/Manager.ts
45
46
  const supportedBrowsers = [
@@ -166,6 +167,7 @@ const browser = async (source, config = globalConfig, initial = false) => {
166
167
  }
167
168
  } else if (config.browser === "puppeteer") {
168
169
  const activeSession = getBrowserSession();
170
+ const browserTimeout = Math.max(config.requestTimeout, 3e4);
169
171
  let browserInstance = activeSession?.browser === "puppeteer" ? activeSession.puppeteerBrowser : void 0;
170
172
  let shouldClose = false;
171
173
  let page;
@@ -193,14 +195,12 @@ const browser = async (source, config = globalConfig, initial = false) => {
193
195
  try {
194
196
  await page.goto(source, {
195
197
  waitUntil: "domcontentloaded",
196
- timeout: config.requestTimeout
198
+ timeout: browserTimeout
197
199
  });
198
200
  } catch (error) {
199
201
  if (!page || !await hasExtractableReadmeContent(page)) throw error;
200
202
  }
201
- await waitForExtractableReadmeContent(page, config.requestTimeout, initial);
202
- await waitForOperationHydration(page, config.requestTimeout);
203
- let html = await page.content();
203
+ let html = await extractStablePageHtml(page, browserTimeout, initial);
204
204
  if (!html) throw new Error(`Unable to extract HTML from remote source: ${source}`);
205
205
  if (!html.includes("id=\"ssr-props\"")) {
206
206
  const { data: rawHtml } = await axios.default.get(source, {
@@ -248,6 +248,37 @@ const waitForOperationHydration = async (page, timeout) => {
248
248
  });
249
249
  } catch {}
250
250
  };
251
+ const extractStablePageHtml = async (page, timeout, initial = false, maxAttempts = 3) => {
252
+ let lastError;
253
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) try {
254
+ await waitForExtractableReadmeContent(page, timeout, initial);
255
+ await waitForOperationHydration(page, timeout);
256
+ return await page.content();
257
+ } catch (error) {
258
+ lastError = error;
259
+ if (!isExecutionContextNavigationError(error) || attempt === maxAttempts) throw error;
260
+ await waitForNavigationSettle(page, timeout);
261
+ }
262
+ throw lastError instanceof Error ? lastError : /* @__PURE__ */ new Error("Unable to extract stable HTML from remote source");
263
+ };
264
+ const waitForNavigationSettle = async (page, timeout) => {
265
+ try {
266
+ await page.waitForFunction(() => {
267
+ return document.readyState === "interactive" || document.readyState === "complete";
268
+ }, { timeout: Math.min(timeout, 5e3) });
269
+ } catch {}
270
+ try {
271
+ await page.waitForNetworkIdle?.({
272
+ idleTime: 500,
273
+ timeout: Math.min(timeout, 5e3)
274
+ });
275
+ } catch {}
276
+ };
277
+ const isExecutionContextNavigationError = (error) => {
278
+ if (!(error instanceof Error)) return false;
279
+ const message = error.message.toLowerCase();
280
+ return message.includes("execution context was destroyed") || message.includes("cannot find context with specified id") || message.includes("most likely because of a navigation");
281
+ };
251
282
  const hasExtractableReadmeContent = async (page) => {
252
283
  return Boolean(await page.$("[data-testid=\"http-method\"], article#content, script#ssr-props"));
253
284
  };
@@ -263,41 +294,94 @@ const extractSsrPropsScript = (html) => {
263
294
  return html.match(/<script id="ssr-props"[^>]*>[\s\S]*?<\/script>/i)?.[0] ?? null;
264
295
  };
265
296
 
266
- //#endregion
267
- //#region src/Core.ts
268
- const isRecord = (value) => {
269
- return typeof value === "object" && value !== null && !Array.isArray(value);
270
- };
271
-
272
297
  //#endregion
273
298
  //#region src/JsonRepair.ts
274
- const parsePossiblyTruncatedJson = (value) => {
275
- const trimmed = value.trim();
276
- if (!/^(?:\{|\[)/.test(trimmed)) return null;
277
- try {
278
- return JSON.parse(trimmed);
279
- } catch {
280
- const repaired = repairCommonJsonIssues(trimmed);
299
+ var JsonRepair = class JsonRepair {
300
+ static parsePossiblyTruncated = (value) => {
301
+ const repairer = new JsonRepair();
302
+ const trimmed = value.trim();
303
+ if (!/^(?:\{|\[)/.test(trimmed)) return null;
281
304
  try {
282
- return JSON.parse(repaired);
305
+ return JSON.parse(trimmed);
283
306
  } catch {
284
- return null;
307
+ const repaired = repairer.repairCommonJsonIssues(trimmed);
308
+ try {
309
+ return JSON.parse(repaired);
310
+ } catch {
311
+ return null;
312
+ }
285
313
  }
286
- }
287
- };
288
- const repairCommonJsonIssues = (value) => {
289
- const withMissingCommasInserted = insertMissingCommas(value);
290
- return `${withMissingCommasInserted}${buildMissingJsonClosers(withMissingCommasInserted)}`;
291
- };
292
- const insertMissingCommas = (value) => {
293
- let result = "";
294
- let inString = false;
295
- let isEscaped = false;
296
- let previousSignificantCharacter = "";
297
- for (let index = 0; index < value.length; index += 1) {
298
- const character = value[index];
299
- if (inString) {
314
+ };
315
+ repairCommonJsonIssues = (value) => {
316
+ const withUnexpectedTokensRemoved = this.removeUnexpectedObjectTokens(value);
317
+ const withMissingCommasInserted = this.insertMissingCommas(withUnexpectedTokensRemoved);
318
+ return `${withMissingCommasInserted}${this.buildMissingJsonClosers(withMissingCommasInserted)}`;
319
+ };
320
+ removeUnexpectedObjectTokens = (value) => {
321
+ return value.replace(/([[{,]\s*)([A-Za-z_$][\w$-]*)(?=\s*"(?:\\.|[^"\\])*"\s*:)/g, "$1");
322
+ };
323
+ insertMissingCommas = (value) => {
324
+ let result = "";
325
+ let inString = false;
326
+ let isEscaped = false;
327
+ let previousSignificantCharacter = "";
328
+ for (let index = 0; index < value.length; index += 1) {
329
+ const character = value[index];
330
+ if (inString) {
331
+ result += character;
332
+ if (isEscaped) {
333
+ isEscaped = false;
334
+ continue;
335
+ }
336
+ if (character === "\\") {
337
+ isEscaped = true;
338
+ continue;
339
+ }
340
+ if (character === "\"") {
341
+ inString = false;
342
+ previousSignificantCharacter = "\"";
343
+ }
344
+ continue;
345
+ }
346
+ if (character === "\"") {
347
+ const remainder = value.slice(index);
348
+ if (/^"(?:\\.|[^"\\])*"\s*:/.test(remainder) && this.shouldInsertCommaBeforeKey(previousSignificantCharacter, result)) result += ",";
349
+ result += character;
350
+ inString = true;
351
+ continue;
352
+ }
300
353
  result += character;
354
+ if (!/\s/.test(character)) previousSignificantCharacter = character;
355
+ }
356
+ return result;
357
+ };
358
+ shouldInsertCommaBeforeKey = (previousSignificantCharacter, currentOutput) => {
359
+ if (!previousSignificantCharacter) return false;
360
+ if (![
361
+ "\"",
362
+ "}",
363
+ "]",
364
+ "e",
365
+ "l",
366
+ "0",
367
+ "1",
368
+ "2",
369
+ "3",
370
+ "4",
371
+ "5",
372
+ "6",
373
+ "7",
374
+ "8",
375
+ "9"
376
+ ].includes(previousSignificantCharacter)) return false;
377
+ const trimmedOutput = currentOutput.trimEnd();
378
+ return !trimmedOutput.endsWith(",") && !trimmedOutput.endsWith("{");
379
+ };
380
+ buildMissingJsonClosers = (value) => {
381
+ const stack = [];
382
+ let inString = false;
383
+ let isEscaped = false;
384
+ for (const character of value) {
301
385
  if (isEscaped) {
302
386
  isEscaped = false;
303
387
  continue;
@@ -307,74 +391,28 @@ const insertMissingCommas = (value) => {
307
391
  continue;
308
392
  }
309
393
  if (character === "\"") {
310
- inString = false;
311
- previousSignificantCharacter = "\"";
394
+ inString = !inString;
395
+ continue;
312
396
  }
313
- continue;
314
- }
315
- if (character === "\"") {
316
- const remainder = value.slice(index);
317
- if (/^"(?:\\.|[^"\\])*"\s*:/.test(remainder) && shouldInsertCommaBeforeKey(previousSignificantCharacter, result)) result += ",";
318
- result += character;
319
- inString = true;
320
- continue;
397
+ if (inString) continue;
398
+ if (character === "{") {
399
+ stack.push("}");
400
+ continue;
401
+ }
402
+ if (character === "[") {
403
+ stack.push("]");
404
+ continue;
405
+ }
406
+ if ((character === "}" || character === "]") && stack[stack.length - 1] === character) stack.pop();
321
407
  }
322
- result += character;
323
- if (!/\s/.test(character)) previousSignificantCharacter = character;
324
- }
325
- return result;
408
+ return stack.reverse().join("");
409
+ };
326
410
  };
327
- const shouldInsertCommaBeforeKey = (previousSignificantCharacter, currentOutput) => {
328
- if (!previousSignificantCharacter) return false;
329
- if (![
330
- "\"",
331
- "}",
332
- "]",
333
- "e",
334
- "l",
335
- "0",
336
- "1",
337
- "2",
338
- "3",
339
- "4",
340
- "5",
341
- "6",
342
- "7",
343
- "8",
344
- "9"
345
- ].includes(previousSignificantCharacter)) return false;
346
- const trimmedOutput = currentOutput.trimEnd();
347
- return !trimmedOutput.endsWith(",") && !trimmedOutput.endsWith("{");
348
- };
349
- const buildMissingJsonClosers = (value) => {
350
- const stack = [];
351
- let inString = false;
352
- let isEscaped = false;
353
- for (const character of value) {
354
- if (isEscaped) {
355
- isEscaped = false;
356
- continue;
357
- }
358
- if (character === "\\") {
359
- isEscaped = true;
360
- continue;
361
- }
362
- if (character === "\"") {
363
- inString = !inString;
364
- continue;
365
- }
366
- if (inString) continue;
367
- if (character === "{") {
368
- stack.push("}");
369
- continue;
370
- }
371
- if (character === "[") {
372
- stack.push("]");
373
- continue;
374
- }
375
- if ((character === "}" || character === "]") && stack[stack.length - 1] === character) stack.pop();
376
- }
377
- return stack.reverse().join("");
411
+
412
+ //#endregion
413
+ //#region src/Core.ts
414
+ const isRecord = (value) => {
415
+ return typeof value === "object" && value !== null && !Array.isArray(value);
378
416
  };
379
417
 
380
418
  //#endregion
@@ -717,11 +755,16 @@ const extractParameterDescription = (param) => {
717
755
  const normalizeResponseBody = (body, contentType) => {
718
756
  const trimmed = body.trim();
719
757
  if (contentType?.toLowerCase().includes("json") || /^(?:\{|\[)/.test(trimmed)) {
720
- const parsedBody = parsePossiblyTruncatedJson(trimmed);
758
+ const parsedBody = JsonRepair.parsePossiblyTruncated(trimmed);
721
759
  if (parsedBody !== null) return {
722
760
  format: "json",
723
761
  body: parsedBody
724
762
  };
763
+ const looseParsedBody = parseLooseStructuredValue(trimmed);
764
+ if (looseParsedBody !== null) return {
765
+ format: "json",
766
+ body: looseParsedBody
767
+ };
725
768
  return {
726
769
  format: "text",
727
770
  body
@@ -869,15 +912,114 @@ const extractStringLiteralValue = (source, startIndex) => {
869
912
  return null;
870
913
  };
871
914
  const parseLooseStructuredValue = (value) => {
872
- const trimmed = value.trim();
915
+ const trimmed = preprocessLooseStructuredValue(value).trim();
873
916
  if (!/^[[{]/.test(trimmed)) return null;
874
- const normalized = trimmed.replace(/([{,]\s*)([A-Za-z_$][\w$-]*)(\s*:)/g, "$1\"$2\"$3").replace(/'([^'\\]*(?:\\.[^'\\]*)*)'/g, (_match, inner) => JSON.stringify(inner.replace(/\\'/g, "'"))).replace(/,\s*([}\]])/g, "$1");
917
+ const normalized = replaceSingleQuotedStringsOutsideDoubleQuotes(trimmed.replace(/([{,]\s*)([A-Za-z_$][\w$-]*)(\s*:)/g, "$1\"$2\"$3").replace(/,\s*([}\]])/g, "$1"));
875
918
  try {
876
919
  return JSON.parse(normalized);
877
920
  } catch {
878
921
  return null;
879
922
  }
880
923
  };
924
+ const replaceSingleQuotedStringsOutsideDoubleQuotes = (value) => {
925
+ let result = "";
926
+ let inDoubleString = false;
927
+ let inSingleString = false;
928
+ let isEscaped = false;
929
+ let singleQuotedContent = "";
930
+ for (const character of value) {
931
+ if (inSingleString) {
932
+ if (isEscaped) {
933
+ singleQuotedContent += character;
934
+ isEscaped = false;
935
+ continue;
936
+ }
937
+ if (character === "\\") {
938
+ isEscaped = true;
939
+ continue;
940
+ }
941
+ if (character === "'") {
942
+ result += JSON.stringify(singleQuotedContent);
943
+ singleQuotedContent = "";
944
+ inSingleString = false;
945
+ continue;
946
+ }
947
+ singleQuotedContent += character;
948
+ continue;
949
+ }
950
+ if (inDoubleString) {
951
+ result += character;
952
+ if (isEscaped) {
953
+ isEscaped = false;
954
+ continue;
955
+ }
956
+ if (character === "\\") {
957
+ isEscaped = true;
958
+ continue;
959
+ }
960
+ if (character === "\"") inDoubleString = false;
961
+ continue;
962
+ }
963
+ if (character === "'") {
964
+ inSingleString = true;
965
+ singleQuotedContent = "";
966
+ continue;
967
+ }
968
+ if (character === "\"") inDoubleString = true;
969
+ result += character;
970
+ }
971
+ return result;
972
+ };
973
+ const preprocessLooseStructuredValue = (value) => {
974
+ let normalized = stripLineCommentsOutsideStrings(value);
975
+ normalized = removeLooseBareTokensBeforeKeys(normalized);
976
+ normalized = repairAnonymousDataEnvelope(normalized);
977
+ return normalized;
978
+ };
979
+ const stripLineCommentsOutsideStrings = (value) => {
980
+ let result = "";
981
+ let inString = false;
982
+ let isEscaped = false;
983
+ for (let index = 0; index < value.length; index += 1) {
984
+ const character = value[index];
985
+ const nextCharacter = value[index + 1];
986
+ if (inString) {
987
+ result += character;
988
+ if (isEscaped) {
989
+ isEscaped = false;
990
+ continue;
991
+ }
992
+ if (character === "\\") {
993
+ isEscaped = true;
994
+ continue;
995
+ }
996
+ if (character === "\"") inString = false;
997
+ continue;
998
+ }
999
+ if (character === "\"") {
1000
+ inString = true;
1001
+ result += character;
1002
+ continue;
1003
+ }
1004
+ if (character === "/" && nextCharacter === "/") {
1005
+ while (index < value.length && value[index] !== "\n") index += 1;
1006
+ if (index < value.length) result += value[index];
1007
+ continue;
1008
+ }
1009
+ result += character;
1010
+ }
1011
+ return result;
1012
+ };
1013
+ const removeLooseBareTokensBeforeKeys = (value) => {
1014
+ return value.replace(/([[{]\s*)([A-Za-z_$][\w$-]*)(\s+)(?=")/g, "$1");
1015
+ };
1016
+ const repairAnonymousDataEnvelope = (value) => {
1017
+ const trimmed = value.trimStart();
1018
+ if (!trimmed.startsWith("{") || /"data"\s*:\s*\[/.test(trimmed)) return value;
1019
+ if (!/^\{\s*"[^"]+"\s*:/.test(trimmed)) return value;
1020
+ if (!/\]\s*,\s*"meta"\s*:/.test(trimmed)) return value;
1021
+ return `${value.slice(0, value.indexOf("{"))}{"data": [{${trimmed.slice(1)}`;
1022
+ };
881
1023
  const escapeSelector = (value) => {
882
1024
  return value.replace(/([#.:[\],=])/g, "\\$1");
883
1025
  };
@@ -965,264 +1107,2306 @@ var Application = class {
965
1107
  };
966
1108
 
967
1109
  //#endregion
968
- //#region src/Commands/InitCommand.ts
969
- const __filename$1 = (0, url.fileURLToPath)(require("url").pathToFileURL(__filename).href);
970
- var InitCommand = class extends _h3ravel_musket.Command {
971
- signature = `init
972
- {--f|force : Overwrite existing config}
973
- `;
974
- description = "Generate a default oapiex.config.ts in the current directory";
975
- async handle() {
976
- const cwd = process.cwd();
977
- const configPath = node_path.default.join(cwd, "oapiex.config.js");
978
- const force = Boolean(this.option("force", false));
979
- const configTemplate = this.buildConfigTemplate();
980
- try {
981
- await node_fs_promises.default.access(configPath);
982
- if (!force) {
983
- this.error(`Config file already exists at ${configPath}. Use --force to overwrite.`);
984
- process.exit(1);
985
- }
986
- } catch {}
987
- await node_fs_promises.default.writeFile(configPath, configTemplate, "utf8");
988
- this.line(`Created ${configPath} `);
1110
+ //#region src/generator/TypeScriptModuleRenderer.ts
1111
+ var TypeScriptModuleRenderer = class {
1112
+ /**
1113
+ * Render a TypeScript declaration (interface, type alias, or shape alias) into its
1114
+ * string representation.
1115
+ *
1116
+ * @param declaration
1117
+ * @returns
1118
+ */
1119
+ renderDeclaration(declaration) {
1120
+ switch (declaration.kind) {
1121
+ case "interface": return this.renderInterface(declaration);
1122
+ case "interface-alias": return `export interface ${declaration.name} extends ${declaration.target} {}`;
1123
+ case "type-alias": return `export type ${declaration.name} = ${declaration.target}`;
1124
+ case "shape-alias": return `export type ${declaration.name} = ${this.renderShape(declaration.shape)}`;
1125
+ }
989
1126
  }
990
- buildConfigTemplate() {
991
- const def = defaultConfig;
1127
+ /**
1128
+ * Render the TypeScript type definitions for an OpenAPI document, including the main
1129
+ * document structure and the individual operation definitions, using the provided type
1130
+ * references for each operation.
1131
+ *
1132
+ * @param rootTypeName
1133
+ * @param document
1134
+ * @param operationTypeRefs
1135
+ * @returns
1136
+ */
1137
+ renderOpenApiDocumentDefinitions(rootTypeName, document, operationTypeRefs) {
992
1138
  return [
993
- `import { defineConfig } from '${__filename$1.includes("node_modules") ? "oapiex" : "./src/Manager"}'`,
994
- "",
995
- "/**",
996
- " * See https://oapi-extractor.toneflix.net/configuration for docs",
997
- " */",
998
- "export default defineConfig({",
999
- ` outputFormat: '${def.outputFormat}',`,
1000
- ` outputShape: '${def.outputShape}',`,
1001
- ` browser: '${def.browser}',`,
1002
- ` requestTimeout: ${def.requestTimeout},`,
1003
- ` maxRedirects: ${def.maxRedirects},`,
1004
- ` userAgent: '${def.userAgent}',`,
1005
- ` retryCount: ${def.retryCount},`,
1006
- ` retryDelay: ${def.retryDelay},`,
1007
- "})"
1139
+ "export interface OpenApiInfo {\n title: string\n version: string\n}",
1140
+ "export interface OpenApiSchemaDefinition {\n type?: string\n description?: string\n default?: unknown\n properties?: Record<string, OpenApiSchemaDefinition>\n items?: OpenApiSchemaDefinition\n required?: string[]\n example?: unknown\n}",
1141
+ "export interface OpenApiParameterDefinition {\n name: string\n in: 'query' | 'header' | 'path' | 'cookie'\n required?: boolean\n description?: string\n schema?: OpenApiSchemaDefinition\n example?: unknown\n}",
1142
+ "export interface OpenApiMediaTypeDefinition<TExample = unknown> {\n schema?: OpenApiSchemaDefinition\n example?: TExample\n}",
1143
+ "export interface OpenApiResponseDefinition<_TResponse = unknown, TExample = unknown> {\n description: string\n content?: Record<string, OpenApiMediaTypeDefinition<TExample>>\n}",
1144
+ "export interface OpenApiRequestBodyDefinition<TInput = unknown> {\n description?: string\n required: boolean\n content: Record<string, OpenApiMediaTypeDefinition<TInput>>\n}",
1145
+ "export interface OpenApiOperationDefinition<_TResponse = unknown, TResponseExample = unknown, TInput = Record<string, never>, _TQuery = Record<string, never>, _THeader = Record<string, never>, _TParams = Record<string, never>> {\n summary?: string\n description?: string\n operationId?: string\n parameters?: OpenApiParameterDefinition[]\n requestBody?: OpenApiRequestBodyDefinition<TInput>\n responses: Record<string, OpenApiResponseDefinition<_TResponse, TResponseExample>>\n}",
1146
+ "export interface OpenApiSdkParameterManifest {\n name: string\n accessor: string\n in: 'query' | 'header' | 'path'\n required: boolean\n description?: string\n}",
1147
+ "export interface OpenApiSdkOperationManifest {\n path: string\n method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'\n methodName: string\n summary?: string\n description?: string\n operationId?: string\n requestBodyDescription?: string\n responseDescription?: string\n responseType: string\n inputType: string\n queryType: string\n headerType: string\n paramsType: string\n hasBody: boolean\n bodyRequired: boolean\n pathParams: OpenApiSdkParameterManifest[]\n queryParams: OpenApiSdkParameterManifest[]\n headerParams: OpenApiSdkParameterManifest[]\n}",
1148
+ "export interface OpenApiSdkGroupManifest {\n className: string\n propertyName: string\n operations: OpenApiSdkOperationManifest[]\n}",
1149
+ "export interface OpenApiSdkManifest {\n groups: OpenApiSdkGroupManifest[]\n}",
1150
+ "export interface OpenApiRuntimeBundle<TApi = unknown> {\n document: unknown\n manifest: OpenApiSdkManifest\n __api?: TApi\n}",
1151
+ Object.entries(document.paths).map(([path, operations]) => {
1152
+ const pathTypeName = this.derivePathTypeName(path);
1153
+ return [Object.keys(operations).map((method) => {
1154
+ const operationTypeName = this.deriveOperationInterfaceName(path, method);
1155
+ const refs = operationTypeRefs.get(`${path}::${method}`) ?? {
1156
+ response: "Record<string, never>",
1157
+ responseExample: "unknown",
1158
+ input: "Record<string, never>",
1159
+ query: "Record<string, never>",
1160
+ header: "Record<string, never>",
1161
+ params: "Record<string, never>"
1162
+ };
1163
+ return `export interface ${operationTypeName} extends OpenApiOperationDefinition<${refs.response}, ${refs.responseExample}, ${refs.input}, ${refs.query}, ${refs.header}, ${refs.params}> {}`;
1164
+ }).join("\n\n"), `export interface ${pathTypeName} {\n${Object.keys(operations).map((method) => ` ${method}: ${this.deriveOperationInterfaceName(path, method)}`).join("\n")}\n}`].join("\n\n");
1165
+ }).join("\n\n"),
1166
+ `export interface Paths {\n${Object.keys(document.paths).map((path) => ` ${this.formatPropertyKey(path)}: ${this.derivePathTypeName(path)}`).join("\n")}\n}`,
1167
+ `export interface ${rootTypeName} {\n openapi: '3.1.0'\n info: OpenApiInfo\n paths: Paths\n}`
1168
+ ].join("\n\n");
1169
+ }
1170
+ renderSdkApiInterface(rootTypeName, manifest) {
1171
+ return `export interface ${rootTypeName}Api {\n${manifest.groups.map((group) => {
1172
+ const methods = group.operations.map((operation) => ` ${operation.methodName}${this.renderSdkMethodSignature(operation)}`).join("\n");
1173
+ return ` ${group.propertyName}: {\n${methods}\n }`;
1174
+ }).join("\n")}\n}`;
1175
+ }
1176
+ renderSdkManifest(variableName, manifest) {
1177
+ return `export const ${variableName}Manifest = ${this.renderValue(manifest)} as const satisfies OpenApiSdkManifest`;
1178
+ }
1179
+ renderSdkBundle(variableName, rootTypeName) {
1180
+ return [
1181
+ `export const ${variableName}Sdk: OpenApiRuntimeBundle<${rootTypeName}Api> = {`,
1182
+ ` document: ${variableName},`,
1183
+ ` manifest: ${variableName}Manifest,`,
1184
+ "}"
1008
1185
  ].join("\n");
1009
1186
  }
1010
- };
1011
-
1012
- //#endregion
1013
- //#region src/OpenApiTransform.ts
1014
- const createOpenApiDocumentFromReadmeOperations = (operations, title = "Extracted API", version = "0.0.0") => {
1015
- const paths = {};
1016
- for (const operation of operations) {
1017
- const normalized = transformReadmeOperationToOpenApi(operation);
1018
- if (!normalized || shouldSkipNormalizedOperation(normalized)) continue;
1019
- paths[normalized.path] ??= {};
1020
- paths[normalized.path][normalized.method] = normalized.operation;
1187
+ /**
1188
+ * Render a value into a string representation suitable for inclusion in TypeScript
1189
+ * type definitions,
1190
+ *
1191
+ * @param value
1192
+ * @returns
1193
+ */
1194
+ renderValue(value) {
1195
+ return this.renderLiteral(value, 0);
1021
1196
  }
1022
- return {
1023
- openapi: "3.1.0",
1024
- info: {
1025
- title,
1026
- version
1027
- },
1028
- paths
1029
- };
1030
- };
1031
- const shouldSkipNormalizedOperation = (normalized) => {
1032
- return normalized.path === "/" && normalized.method === "get" && normalized.operation.operationId === "get" && Object.keys(normalized.operation.responses).length === 0;
1033
- };
1034
- const transformReadmeOperationToOpenApi = (operation) => {
1035
- if (!operation.method || !operation.url) return null;
1036
- const url = new URL(operation.url);
1037
- if (shouldSkipPlaceholderOperation(url, operation)) return null;
1038
- const method = operation.method.toLowerCase();
1039
- const path = decodeOpenApiPathname(url.pathname);
1040
- return {
1041
- path,
1042
- method,
1043
- operation: {
1044
- summary: operation.sidebarLinks.find((link) => link.active)?.label,
1045
- description: operation.description ?? void 0,
1046
- operationId: buildOperationId(method, path),
1047
- parameters: createParameters(operation.requestParams),
1048
- requestBody: createRequestBody(operation.requestParams, operation.requestExampleNormalized?.body, hasExtractedBodyParams(operation.requestParams) ? null : resolveFallbackRequestBodyExample(operation)),
1049
- responses: createResponses(operation.responseSchemas, operation.responseBodies)
1197
+ renderOpenApiDocumentValue(document) {
1198
+ return this.renderLiteral(this.normalizeOpenApiDocument(document), 0);
1199
+ }
1200
+ /**
1201
+ * Convert a string to camelCase, sanitizing it to create a valid TypeScript identifier.
1202
+ *
1203
+ * @param value
1204
+ * @returns
1205
+ */
1206
+ toCamelCase(value) {
1207
+ const typeName = this.sanitizeTypeName(value);
1208
+ return typeName.charAt(0).toLowerCase() + typeName.slice(1);
1209
+ }
1210
+ renderInterface(declaration) {
1211
+ const body = declaration.properties.map((property) => ` ${this.formatPropertyKey(property.key)}${property.optional ? "?" : ""}: ${this.renderShape(property.shape)}`).join("\n");
1212
+ return `export interface ${declaration.name} {\n${body}\n}`;
1213
+ }
1214
+ renderShape(shape) {
1215
+ switch (shape.kind) {
1216
+ case "primitive": return shape.type;
1217
+ case "array": return `${this.wrapUnion(this.renderShape(shape.item))}[]`;
1218
+ case "union": return shape.types.map((entry) => this.renderShape(entry)).join(" | ");
1219
+ case "object": return this.inlineObjectShape(shape);
1050
1220
  }
1051
- };
1052
- };
1053
- const shouldSkipPlaceholderOperation = (url, operation) => {
1054
- if (url.hostname !== "example.com" || url.pathname !== "/") return false;
1055
- return operation.requestParams.length === 0 && operation.responseSchemas.length === 0 && operation.responseBodies.length === 0 && operation.requestExampleNormalized?.url === "https://example.com/";
1056
- };
1057
- const decodeOpenApiPathname = (pathname) => {
1058
- return pathname.split("/").map((segment) => {
1059
- if (!segment) return segment;
1060
- try {
1061
- return decodeURIComponent(segment);
1062
- } catch {
1063
- return segment;
1221
+ }
1222
+ inlineObjectShape(shape) {
1223
+ if (shape.properties.length === 0) return "Record<string, never>";
1224
+ return `{ ${shape.properties.map((property) => `${this.formatPropertyKey(property.key)}${property.optional ? "?" : ""}: ${this.renderShape(property.shape)}`).join("; ")} }`;
1225
+ }
1226
+ wrapUnion(value) {
1227
+ return value.includes(" | ") ? `(${value})` : value;
1228
+ }
1229
+ derivePathTypeName(path) {
1230
+ const segments = path.split("/").map((segment) => segment.trim()).filter(Boolean).filter((segment) => !/^v\d+$/i.test(segment)).map((segment) => this.isPathParam(segment) ? `by ${this.stripPathParam(segment)}` : segment);
1231
+ return `${this.sanitizeTypeName(segments.join(" "))}Path`;
1232
+ }
1233
+ deriveOperationInterfaceName(path, method) {
1234
+ return `${this.derivePathTypeName(path)}${this.sanitizeTypeName(method)}Operation`;
1235
+ }
1236
+ sanitizeTypeName(value) {
1237
+ const normalized = value.replace(/[^A-Za-z0-9]+/g, " ").trim();
1238
+ if (!normalized) return "GeneratedEntity";
1239
+ const pascalCased = normalized.split(/\s+/).filter(Boolean).map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)).join("");
1240
+ return /^[A-Za-z_$]/.test(pascalCased) ? pascalCased : `Type${pascalCased}`;
1241
+ }
1242
+ formatPropertyKey(key) {
1243
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : `'${this.escapeStringLiteral(key)}'`;
1244
+ }
1245
+ renderSdkMethodSignature(operation) {
1246
+ const args = [];
1247
+ if (operation.pathParams.length > 0) args.push(`(params: ${operation.paramsType}`);
1248
+ if (operation.queryParams.length > 0) args.push(`${args.length === 0 ? "(" : ", "}query: ${operation.queryType}`);
1249
+ if (operation.hasBody) args.push(`${args.length === 0 ? "(" : ", "}body${operation.bodyRequired ? "" : "?"}: ${operation.inputType}`);
1250
+ if (operation.headerParams.length > 0) args.push(`${args.length === 0 ? "(" : ", "}headers?: ${operation.headerType}`);
1251
+ if (args.length === 0) return `(): Promise<${operation.responseType}>`;
1252
+ return `${args.join("")}): Promise<${operation.responseType}>`;
1253
+ }
1254
+ renderLiteral(value, indentLevel) {
1255
+ if (value === null) return "null";
1256
+ if (typeof value === "string") return `'${this.escapeStringLiteral(value)}'`;
1257
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
1258
+ if (Array.isArray(value)) {
1259
+ if (value.length === 0) return "[]";
1260
+ const nextIndent = this.indent(indentLevel + 1);
1261
+ const currentIndent = this.indent(indentLevel);
1262
+ return `[\n${value.map((entry) => `${nextIndent}${this.renderLiteral(entry, indentLevel + 1)}`).join(",\n")}\n${currentIndent}]`;
1064
1263
  }
1065
- }).join("/");
1066
- };
1067
- const hasExtractedBodyParams = (params) => {
1068
- return params.some((param) => param.in === "body" || param.in === null);
1069
- };
1070
- const createParameters = (params) => {
1071
- const parameters = params.filter((param) => isOpenApiParameterLocation(param.in)).map((param) => createParameter(param));
1072
- return parameters.length > 0 ? parameters : void 0;
1073
- };
1074
- const createRequestBody = (params, example, fallbackExample) => {
1075
- const bodyParams = params.filter((param) => param.in === "body" || param.in === null);
1076
- if (bodyParams.length === 0 && example == null) return;
1077
- const schema = buildRequestBodySchema(bodyParams, example, fallbackExample);
1078
- return {
1079
- required: bodyParams.length > 0 ? bodyParams.some((param) => param.required) : false,
1080
- content: { "application/json": {
1081
- schema,
1082
- ...example != null ? { example } : {}
1083
- } }
1084
- };
1085
- };
1086
- const buildRequestBodySchema = (params, example, fallbackExample) => {
1087
- const schema = mergeOpenApiSchemas(createExampleSchema(example), createExampleSchema(fallbackExample)) ?? { type: "object" };
1088
- if (example != null) schema.example = example;
1089
- else if (fallbackExample != null) schema.example = fallbackExample;
1090
- for (const param of params) insertRequestBodyParam(schema, param);
1091
- return schema;
1092
- };
1093
- const inferSchemaFromExample = (value) => {
1094
- if (Array.isArray(value)) return {
1095
- type: "array",
1096
- items: inferSchemaFromExample(value[0]) ?? {},
1097
- example: value
1098
- };
1099
- if (isRecord(value)) return {
1100
- type: "object",
1101
- properties: Object.fromEntries(Object.entries(value).map(([key, entryValue]) => [key, inferSchemaFromExample(entryValue) ?? {}])),
1102
- example: value
1103
- };
1104
- if (typeof value === "string") return {
1105
- type: "string",
1106
- example: value
1107
- };
1108
- if (typeof value === "number") return {
1109
- type: Number.isInteger(value) ? "integer" : "number",
1110
- example: value
1111
- };
1112
- if (typeof value === "boolean") return {
1113
- type: "boolean",
1114
- example: value
1115
- };
1116
- if (value === null) return {};
1117
- };
1118
- const insertRequestBodyParam = (rootSchema, param) => {
1119
- const path = param.path.length > 0 ? param.path : [param.name];
1120
- let currentSchema = rootSchema;
1121
- for (const [index, segment] of path.slice(0, -1).entries()) {
1122
- currentSchema.properties ??= {};
1123
- currentSchema.properties[segment] ??= { type: "object" };
1124
- if (param.required) currentSchema.required = Array.from(new Set([...currentSchema.required ?? [], segment]));
1125
- currentSchema = currentSchema.properties[segment];
1126
- currentSchema.type ??= "object";
1127
- if (index === path.length - 2 && param.required) currentSchema.required ??= [];
1128
- }
1129
- const leafKey = path[path.length - 1] ?? param.name;
1130
- currentSchema.properties ??= {};
1131
- currentSchema.properties[leafKey] = createParameterSchema(param);
1132
- if (param.required) currentSchema.required = Array.from(new Set([...currentSchema.required ?? [], leafKey]));
1133
- };
1134
- const createParameter = (param) => {
1135
- return {
1136
- name: param.name,
1137
- in: param.in,
1138
- required: param.in === "path" ? true : param.required,
1139
- description: param.description ?? void 0,
1140
- schema: createParameterSchema(param),
1141
- example: param.defaultValue ?? void 0
1142
- };
1143
- };
1144
- const createParameterSchema = (param) => {
1145
- return {
1146
- type: param.type ?? void 0,
1147
- description: param.description ?? void 0,
1148
- default: param.defaultValue ?? void 0
1149
- };
1150
- };
1151
- const createResponses = (schemas, responseBodies) => {
1152
- const responses = {};
1153
- for (const schema of schemas) {
1154
- if (!schema.statusCode) continue;
1155
- const content = createResponseContent(responseBodies.filter((body) => body.statusCode === schema.statusCode));
1156
- responses[schema.statusCode] = {
1157
- description: schema.description ?? schema.statusCode,
1158
- ...content ? { content } : {}
1159
- };
1264
+ if (typeof value === "object") {
1265
+ const entries = Object.entries(value);
1266
+ if (entries.length === 0) return "{}";
1267
+ const nextIndent = this.indent(indentLevel + 1);
1268
+ const currentIndent = this.indent(indentLevel);
1269
+ return `{\n${entries.map(([key, entry]) => `${nextIndent}${this.formatPropertyKey(key)}: ${this.renderLiteral(entry, indentLevel + 1)}`).join(",\n")}\n${currentIndent}}`;
1270
+ }
1271
+ return "undefined";
1160
1272
  }
1161
- for (const body of responseBodies) {
1162
- if (!body.statusCode || responses[body.statusCode]) continue;
1163
- const content = createResponseContent([body]);
1164
- responses[body.statusCode] = {
1165
- description: body.label ?? body.statusCode,
1166
- ...content ? { content } : {}
1167
- };
1273
+ normalizeOpenApiDocument(document) {
1274
+ return this.normalizeObject(document, (key, value, parent) => {
1275
+ if (key === "example" && parent && typeof parent === "object" && !Array.isArray(parent)) {
1276
+ const owner = parent;
1277
+ if (owner.schema && this.isPlainObject(owner.schema)) return this.normalizeExample(value, owner.schema);
1278
+ if (this.isSchemaLike(owner)) return this.normalizeExample(value, owner);
1279
+ }
1280
+ return value;
1281
+ });
1168
1282
  }
1169
- return responses;
1170
- };
1171
- const createResponseContent = (bodies) => {
1172
- if (bodies.length === 0) return;
1173
- const content = {};
1174
- for (const body of bodies) {
1175
- const contentType = body.contentType ?? (body.format === "json" ? "application/json" : "text/plain");
1176
- content[contentType] = {
1177
- schema: inferSchemaFromBody(body.body, body.format),
1178
- example: body.body
1179
- };
1283
+ normalizeObject(value, transform) {
1284
+ if (Array.isArray(value)) return value.map((entry) => this.normalizeObject(entry, transform)).filter((entry) => entry !== void 0);
1285
+ if (!this.isPlainObject(value)) return value;
1286
+ const output = {};
1287
+ for (const [key, entry] of Object.entries(value)) {
1288
+ const transformed = transform(key, entry, value);
1289
+ const normalized = this.normalizeObject(transformed, transform);
1290
+ if (normalized !== void 0) output[key] = normalized;
1291
+ }
1292
+ return output;
1180
1293
  }
1181
- return content;
1182
- };
1183
- const inferSchemaFromBody = (body, format) => {
1184
- if (format === "json") return inferSchemaFromExample(body);
1185
- if (format === "text") return {
1186
- type: "string",
1187
- example: body
1188
- };
1189
- };
1190
- const resolveFallbackRequestBodyExample = (operation) => {
1191
- const jsonResponseBody = operation.responseBodies.find((body) => body.format === "json")?.body;
1192
- if (jsonResponseBody != null) return jsonResponseBody;
1193
- if (typeof operation.responseExample === "object" && operation.responseExample !== null) return operation.responseExample;
1194
- if (typeof operation.responseExampleRaw === "string") return parsePossiblyTruncatedJson(operation.responseExampleRaw);
1195
- if (typeof operation.responseExample === "string") return parsePossiblyTruncatedJson(operation.responseExample);
1196
- return null;
1197
- };
1198
- const createExampleSchema = (value) => {
1199
- if (value == null) return null;
1200
- return inferSchemaFromExample(value) ?? null;
1201
- };
1202
- const mergeOpenApiSchemas = (left, right) => {
1203
- if (!left) return right;
1204
- if (!right) return left;
1205
- const merged = {
1206
- ...right,
1207
- ...left,
1208
- ...left.type || right.type ? { type: left.type ?? right.type } : {},
1209
- ...left.description || right.description ? { description: left.description ?? right.description } : {},
1210
- ...left.default !== void 0 || right.default !== void 0 ? { default: left.default ?? right.default } : {},
1211
- ...left.example !== void 0 || right.example !== void 0 ? { example: left.example ?? right.example } : {}
1212
- };
1213
- if (left.properties || right.properties) {
1214
- const propertyKeys = new Set([...Object.keys(left.properties ?? {}), ...Object.keys(right.properties ?? {})]);
1215
- merged.properties = Object.fromEntries(Array.from(propertyKeys).map((key) => [key, mergeOpenApiSchemas(left.properties?.[key] ?? null, right.properties?.[key] ?? null) ?? {}]));
1294
+ normalizeExample(example, schema) {
1295
+ if (example === void 0) return;
1296
+ const type = typeof schema.type === "string" ? schema.type : void 0;
1297
+ if (type === "string") {
1298
+ if (typeof example === "string") return example;
1299
+ if (typeof example === "number" || typeof example === "boolean") return String(example);
1300
+ return;
1301
+ }
1302
+ if (type === "number" || type === "integer") {
1303
+ if (typeof example === "number") return example;
1304
+ if (typeof example === "string" && example.trim() !== "" && Number.isFinite(Number(example))) return Number(example);
1305
+ return;
1306
+ }
1307
+ if (type === "boolean") {
1308
+ if (typeof example === "boolean") return example;
1309
+ if (example === "true") return true;
1310
+ if (example === "false") return false;
1311
+ return;
1312
+ }
1313
+ if (type === "array") {
1314
+ if (!Array.isArray(example)) return;
1315
+ const itemSchema = this.isPlainObject(schema.items) ? schema.items : void 0;
1316
+ if (!itemSchema) return example;
1317
+ return example.map((entry) => this.normalizeExample(entry, itemSchema)).filter((entry) => entry !== void 0);
1318
+ }
1319
+ if (type === "object") {
1320
+ if (!this.isPlainObject(example)) return;
1321
+ const properties = this.isPlainObject(schema.properties) ? schema.properties : {};
1322
+ const required = Array.isArray(schema.required) ? schema.required.filter((entry) => typeof entry === "string") : [];
1323
+ const normalized = {};
1324
+ for (const [key, entry] of Object.entries(example)) {
1325
+ const propertySchema = properties[key];
1326
+ const normalizedEntry = propertySchema ? this.normalizeExample(entry, propertySchema) : entry;
1327
+ if (normalizedEntry !== void 0) normalized[key] = normalizedEntry;
1328
+ }
1329
+ for (const requiredKey of required) {
1330
+ if (requiredKey in normalized) continue;
1331
+ const propertySchema = properties[requiredKey];
1332
+ if (!propertySchema) return;
1333
+ const fallback = propertySchema.default !== void 0 ? this.normalizeExample(propertySchema.default, propertySchema) : propertySchema.example !== void 0 ? this.normalizeExample(propertySchema.example, propertySchema) : void 0;
1334
+ if (fallback === void 0) return;
1335
+ normalized[requiredKey] = fallback;
1336
+ }
1337
+ return normalized;
1338
+ }
1339
+ return example;
1340
+ }
1341
+ isPlainObject(value) {
1342
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1343
+ }
1344
+ isSchemaLike(value) {
1345
+ return "type" in value || "properties" in value || "items" in value || "required" in value || "default" in value;
1346
+ }
1347
+ indent(level) {
1348
+ return " ".repeat(level);
1349
+ }
1350
+ escapeStringLiteral(value) {
1351
+ return value.replace(/\\/g, String.raw`\\`).replace(/'/g, String.raw`\'`).replace(/\r/g, String.raw`\r`).replace(/\n/g, String.raw`\n`).replace(/\t/g, String.raw`\t`).replace(/\f/g, String.raw`\f`).replace(/\x08/g, String.raw`\b`).replace(/\u2028/g, String.raw`\u2028`).replace(/\u2029/g, String.raw`\u2029`);
1352
+ }
1353
+ isPathParam(segment) {
1354
+ return segment.startsWith("{") && segment.endsWith("}") || /^:[A-Za-z0-9_]+$/.test(segment);
1355
+ }
1356
+ stripPathParam(segment) {
1357
+ return segment.replace(/^\{/, "").replace(/\}$/, "").replace(/^:/, "");
1216
1358
  }
1217
- if (left.items || right.items) merged.items = mergeOpenApiSchemas(left.items ?? null, right.items ?? null) ?? {};
1218
- if (left.required || right.required) merged.required = Array.from(new Set([...right.required ?? [], ...left.required ?? []]));
1219
- return merged;
1220
- };
1221
- const buildOperationId = (method, path) => {
1222
- return `${method}${path.replace(/\{([^}]+)\}/g, "$1").split("/").filter(Boolean).map((segment) => segment.replace(/[^a-zA-Z0-9]+/g, " ")).map((segment) => segment.trim()).filter(Boolean).map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1).replace(/\s+(.)/g, (_match, char) => char.toUpperCase())).join("")}`;
1223
1359
  };
1224
- const isOpenApiParameterLocation = (value) => {
1225
- return value === "query" || value === "header" || value === "path" || value === "cookie";
1360
+
1361
+ //#endregion
1362
+ //#region src/generator/TypeScriptNamingSupport.ts
1363
+ var TypeScriptNamingSupport = class TypeScriptNamingSupport {
1364
+ static contextualTailSegments = new Set([
1365
+ "history",
1366
+ "status",
1367
+ "detail",
1368
+ "details"
1369
+ ]);
1370
+ static nestedContextSegments = new Set([
1371
+ "account",
1372
+ "accounts",
1373
+ "transaction",
1374
+ "transactions",
1375
+ "wallet",
1376
+ "wallets",
1377
+ "virtual-account",
1378
+ "virtual-accounts",
1379
+ "history"
1380
+ ]);
1381
+ static roleSuffixes = [
1382
+ "Input",
1383
+ "Query",
1384
+ "Header",
1385
+ "Params"
1386
+ ];
1387
+ sanitizeTypeName(value) {
1388
+ const normalized = value.replace(/[^A-Za-z0-9]+/g, " ").trim();
1389
+ if (!normalized) return "GeneratedEntity";
1390
+ const pascalCased = normalized.split(/\s+/).filter(Boolean).map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)).join("");
1391
+ return /^[A-Za-z_$]/.test(pascalCased) ? pascalCased : `Type${pascalCased}`;
1392
+ }
1393
+ isPathParam(segment) {
1394
+ return segment.startsWith("{") && segment.endsWith("}") || /^:[A-Za-z0-9_]+$/.test(segment);
1395
+ }
1396
+ stripPathParam(segment) {
1397
+ return segment.replace(/^\{/, "").replace(/\}$/, "").replace(/^:/, "");
1398
+ }
1399
+ singularize(value) {
1400
+ if (/ies$/i.test(value)) return `${value.slice(0, -3)}y`;
1401
+ if (/(sses|shes|ches|xes|zes)$/i.test(value)) return value.slice(0, -2);
1402
+ if (value.endsWith("s") && !value.endsWith("ss") && value.length > 1) return value.slice(0, -1);
1403
+ return value;
1404
+ }
1405
+ pluralize(value) {
1406
+ if (/y$/i.test(value)) return `${value.slice(0, -1)}ies`;
1407
+ if (/s$/i.test(value)) return value;
1408
+ return `${value}s`;
1409
+ }
1410
+ toCamelCase(value) {
1411
+ const typeName = this.sanitizeTypeName(value);
1412
+ return typeName.charAt(0).toLowerCase() + typeName.slice(1);
1413
+ }
1414
+ deriveOperationNaming(path) {
1415
+ const pathSegments = this.getNormalizedPathSegments(path);
1416
+ const staticSegments = pathSegments.filter((segment) => !this.isPathParam(segment)).map((segment) => this.singularize(segment));
1417
+ const paramSegments = pathSegments.filter((segment) => this.isPathParam(segment)).map((segment) => this.singularize(this.stripPathParam(segment)));
1418
+ const tailSegment = staticSegments[staticSegments.length - 1] ?? "resource";
1419
+ const parentSegment = staticSegments[staticSegments.length - 2] ?? null;
1420
+ const hasPathParamBeforeTail = pathSegments.slice(0, -1).some((segment) => this.isPathParam(segment));
1421
+ const shouldPrefixParent = Boolean(parentSegment && (TypeScriptNamingSupport.contextualTailSegments.has(tailSegment.toLowerCase()) || hasPathParamBeforeTail && TypeScriptNamingSupport.nestedContextSegments.has(tailSegment.toLowerCase())));
1422
+ return {
1423
+ baseName: this.sanitizeTypeName(shouldPrefixParent ? `${parentSegment} ${tailSegment}` : tailSegment),
1424
+ collisionSuffix: paramSegments.length > 0 ? `By ${paramSegments.map((segment) => this.sanitizeTypeName(segment)).join(" And ")}` : parentSegment && !shouldPrefixParent ? this.sanitizeTypeName(parentSegment) : ""
1425
+ };
1426
+ }
1427
+ fallbackCollisionSuffix(method, path, baseName) {
1428
+ const pathSegments = this.getNormalizedPathSegments(path);
1429
+ const staticSegments = pathSegments.filter((segment) => !this.isPathParam(segment));
1430
+ const tailSegment = staticSegments[staticSegments.length - 1] ?? "";
1431
+ const hasParams = pathSegments.some((segment) => this.isPathParam(segment));
1432
+ if (method === "get" && !hasParams && /s$/i.test(tailSegment)) return "List";
1433
+ if (method === "post" && !hasParams) return "Create";
1434
+ if ((method === "put" || method === "patch") && hasParams) return "Update";
1435
+ if (method === "delete") return "Delete";
1436
+ return `${this.sanitizeTypeName(method)}${baseName}`;
1437
+ }
1438
+ insertCollisionSuffix(baseName, collisionName) {
1439
+ if (!collisionName) return baseName;
1440
+ for (const roleSuffix of TypeScriptNamingSupport.roleSuffixes) if (baseName.endsWith(roleSuffix) && baseName.length > roleSuffix.length) return `${baseName.slice(0, -roleSuffix.length)}${collisionName}${roleSuffix}`;
1441
+ return `${baseName}${collisionName}`;
1442
+ }
1443
+ deriveSdkGroupNamesBySignature(document, namespaceStrategy) {
1444
+ const pathBySignature = /* @__PURE__ */ new Map();
1445
+ for (const path of Object.keys(document.paths)) {
1446
+ const signature = this.getStaticPathSignature(path);
1447
+ if (!pathBySignature.has(signature)) pathBySignature.set(signature, path);
1448
+ }
1449
+ const entries = Array.from(pathBySignature.entries()).map(([signature, path]) => ({
1450
+ signature,
1451
+ staticSegments: signature.split("/").filter(Boolean),
1452
+ candidates: this.buildSdkGroupNameCandidates(path, namespaceStrategy)
1453
+ })).sort((left, right) => {
1454
+ return left.staticSegments.length - right.staticSegments.length || left.signature.localeCompare(right.signature);
1455
+ });
1456
+ const groupNamesBySignature = /* @__PURE__ */ new Map();
1457
+ const usedNames = /* @__PURE__ */ new Set();
1458
+ for (const entry of entries) {
1459
+ const className = entry.candidates.find((candidate) => !usedNames.has(candidate)) ?? this.createUniqueSdkGroupName(entry.candidates[entry.candidates.length - 1] ?? "Resource", usedNames);
1460
+ usedNames.add(className);
1461
+ groupNamesBySignature.set(entry.signature, className);
1462
+ }
1463
+ return groupNamesBySignature;
1464
+ }
1465
+ getStaticPathSegments(path) {
1466
+ return this.getNormalizedPathSegments(path).filter((segment) => !this.isPathParam(segment)).map((segment) => this.singularize(segment));
1467
+ }
1468
+ getStaticPathSignature(path) {
1469
+ return this.getStaticPathSegments(path).join("/");
1470
+ }
1471
+ getNormalizedPathSegments(path) {
1472
+ return path.split("/").map((segment) => segment.trim()).filter(Boolean).filter((segment) => !/^v\d+$/i.test(segment));
1473
+ }
1474
+ deriveSdkMethodName(method, path, operation, methodStrategy) {
1475
+ if (methodStrategy === "operation-id" && operation.operationId) return this.toCamelCase(this.sanitizeTypeName(operation.operationId));
1476
+ const hasPathParams = this.getNormalizedPathSegments(path).some((segment) => this.isPathParam(segment));
1477
+ if (method === "get") return this.endsWithPluralStaticSegment(path) ? "list" : hasPathParams ? "get" : "list";
1478
+ if (method === "post") return "create";
1479
+ if (method === "patch" || method === "put") return "update";
1480
+ if (method === "delete") return "delete";
1481
+ return this.toCamelCase(this.sanitizeTypeName(method));
1482
+ }
1483
+ ensureUniqueSdkMethodNames(operations) {
1484
+ const counts = /* @__PURE__ */ new Map();
1485
+ return operations.map((operation) => {
1486
+ const count = counts.get(operation.methodName) ?? 0;
1487
+ counts.set(operation.methodName, count + 1);
1488
+ if (count === 0) return operation;
1489
+ const suffix = this.sanitizeTypeName(this.fallbackCollisionSuffix(operation.method.toLowerCase(), operation.path, "Operation"));
1490
+ return {
1491
+ ...operation,
1492
+ methodName: `${operation.methodName}${suffix}`
1493
+ };
1494
+ });
1495
+ }
1496
+ createSdkParameterManifest(parameters, location, path) {
1497
+ return [...(parameters ?? []).filter((parameter) => parameter.in === location).sort((left, right) => left.name.localeCompare(right.name)), ...this.getInferredPathParameters(path, location, (parameters ?? []).filter((parameter) => parameter.in === location))].sort((left, right) => left.name.localeCompare(right.name)).map((parameter) => ({
1498
+ name: parameter.name,
1499
+ accessor: this.toParameterAccessor(parameter.name),
1500
+ in: location,
1501
+ required: parameter.required ?? false,
1502
+ description: parameter.description
1503
+ }));
1504
+ }
1505
+ getInferredPathParameters(path, location, existingParameters) {
1506
+ if (location !== "path" || !path) return [];
1507
+ const existingNames = new Set(existingParameters.map((parameter) => parameter.name));
1508
+ return this.getNormalizedPathSegments(path).filter((segment) => this.isPathParam(segment)).map((segment) => this.stripPathParam(segment)).filter((name) => !existingNames.has(name)).map((name) => ({
1509
+ name,
1510
+ in: "path",
1511
+ required: true,
1512
+ schema: { type: "string" }
1513
+ }));
1514
+ }
1515
+ buildSdkGroupNameCandidates(path, namespaceStrategy) {
1516
+ const normalizedSegments = this.getNormalizedPathSegments(path);
1517
+ const rawStaticSegments = normalizedSegments.filter((segment) => !this.isPathParam(segment));
1518
+ const staticSegments = rawStaticSegments.map((segment) => this.singularize(segment));
1519
+ const defaultName = this.deriveOperationNaming(path).baseName;
1520
+ const preferredName = this.getPreferredSdkGroupName(normalizedSegments, rawStaticSegments, staticSegments);
1521
+ const contextualNames = staticSegments.map((_, index, segments) => this.sanitizeTypeName(segments.slice(index).join(" "))).reverse();
1522
+ if (namespaceStrategy === "scoped") return Array.from(new Set([
1523
+ preferredName ?? "",
1524
+ this.sanitizeTypeName(staticSegments.join(" ")),
1525
+ ...contextualNames,
1526
+ defaultName
1527
+ ].filter(Boolean)));
1528
+ return Array.from(new Set([
1529
+ preferredName ?? "",
1530
+ defaultName,
1531
+ ...contextualNames
1532
+ ].filter(Boolean)));
1533
+ }
1534
+ getPreferredSdkGroupName(normalizedSegments, rawStaticSegments, staticSegments) {
1535
+ const tailSegment = rawStaticSegments[rawStaticSegments.length - 1];
1536
+ const tailBaseSegment = staticSegments[staticSegments.length - 1];
1537
+ const parentSegment = staticSegments[staticSegments.length - 2];
1538
+ const hasPathParams = normalizedSegments.some((segment) => this.isPathParam(segment));
1539
+ const hasPathParamBeforeTail = normalizedSegments.slice(0, -1).some((segment) => this.isPathParam(segment));
1540
+ if (!tailSegment || !tailBaseSegment || !parentSegment) return null;
1541
+ if (rawStaticSegments.length === 2 && !hasPathParams) return this.sanitizeTypeName(`${tailBaseSegment} ${parentSegment}`);
1542
+ if (!hasPathParamBeforeTail) return null;
1543
+ if (this.singularize(tailSegment) !== tailSegment) return this.sanitizeTypeName(`${parentSegment} ${tailBaseSegment}`);
1544
+ return this.sanitizeTypeName(`${tailBaseSegment} ${parentSegment}`);
1545
+ }
1546
+ createUniqueSdkGroupName(baseName, usedNames) {
1547
+ let suffix = 2;
1548
+ let candidate = baseName;
1549
+ while (usedNames.has(candidate)) {
1550
+ candidate = `${baseName}${suffix}`;
1551
+ suffix += 1;
1552
+ }
1553
+ return candidate;
1554
+ }
1555
+ endsWithPluralStaticSegment(path) {
1556
+ const tailSegment = this.getNormalizedPathSegments(path).at(-1);
1557
+ if (!tailSegment || this.isPathParam(tailSegment)) return false;
1558
+ return this.singularize(tailSegment) !== tailSegment;
1559
+ }
1560
+ toParameterAccessor(value) {
1561
+ const normalized = value.replace(/[^A-Za-z0-9]+/g, " ").trim();
1562
+ if (!normalized) return "value";
1563
+ const [first, ...rest] = normalized.split(/\s+/).filter(Boolean);
1564
+ const camelValue = [first.toLowerCase(), ...rest.map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1).toLowerCase())].join("");
1565
+ return /^[A-Za-z_$]/.test(camelValue) ? camelValue : `value${camelValue}`;
1566
+ }
1567
+ };
1568
+
1569
+ //#endregion
1570
+ //#region src/generator/TypeScriptShapeBuilder.ts
1571
+ var TypeScriptShapeBuilder = class {
1572
+ constructor(naming) {
1573
+ this.naming = naming;
1574
+ }
1575
+ createContext() {
1576
+ return {
1577
+ declarations: [],
1578
+ declarationByName: /* @__PURE__ */ new Map(),
1579
+ nameBySignature: /* @__PURE__ */ new Map(),
1580
+ usedNames: /* @__PURE__ */ new Set()
1581
+ };
1582
+ }
1583
+ namespaceTopLevelShape(shape, role) {
1584
+ if (shape.kind !== "object") return shape;
1585
+ return {
1586
+ ...shape,
1587
+ signature: `${role}:${shape.signature}`
1588
+ };
1589
+ }
1590
+ inferShapeFromExample(value, nameHint) {
1591
+ if (value === null) return {
1592
+ kind: "primitive",
1593
+ type: "null"
1594
+ };
1595
+ if (Array.isArray(value)) {
1596
+ if (value.length === 0) return {
1597
+ kind: "array",
1598
+ item: {
1599
+ kind: "primitive",
1600
+ type: "unknown"
1601
+ }
1602
+ };
1603
+ const itemShapes = this.dedupeShapes(value.map((entry) => this.inferShapeFromExample(entry, this.naming.singularize(nameHint))));
1604
+ return {
1605
+ kind: "array",
1606
+ item: itemShapes.length === 1 ? itemShapes[0] : {
1607
+ kind: "union",
1608
+ types: itemShapes
1609
+ }
1610
+ };
1611
+ }
1612
+ if (this.isRecord(value)) return this.createObjectShape(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => ({
1613
+ key,
1614
+ optional: false,
1615
+ shape: this.inferShapeFromExample(entry, key)
1616
+ })));
1617
+ switch (typeof value) {
1618
+ case "string": return {
1619
+ kind: "primitive",
1620
+ type: "string"
1621
+ };
1622
+ case "number": return {
1623
+ kind: "primitive",
1624
+ type: "number"
1625
+ };
1626
+ case "boolean": return {
1627
+ kind: "primitive",
1628
+ type: "boolean"
1629
+ };
1630
+ default: return {
1631
+ kind: "primitive",
1632
+ type: "unknown"
1633
+ };
1634
+ }
1635
+ }
1636
+ registerNamedShape(shape, preferredName, context, collisionSuffix) {
1637
+ if (shape.kind === "object") return this.registerObjectShape(shape, preferredName, context, collisionSuffix, true);
1638
+ const name = this.createUniqueTypeName(preferredName, context, collisionSuffix);
1639
+ const declaration = {
1640
+ kind: "shape-alias",
1641
+ name,
1642
+ shape: this.prepareNestedShape(shape, preferredName, context)
1643
+ };
1644
+ context.declarations.push(declaration);
1645
+ context.declarationByName.set(name, declaration);
1646
+ return name;
1647
+ }
1648
+ registerObjectShape(shape, preferredName, context, collisionSuffix, emitAlias = false) {
1649
+ const existingName = context.nameBySignature.get(shape.signature);
1650
+ const compatibleDeclaration = this.findCompatibleObjectDeclaration(shape, preferredName, context);
1651
+ if (existingName) {
1652
+ if (emitAlias && existingName !== preferredName && !context.declarationByName.has(preferredName)) {
1653
+ const aliasName = this.createUniqueTypeName(preferredName, context, collisionSuffix);
1654
+ if (aliasName !== existingName) {
1655
+ const aliasDeclaration = {
1656
+ kind: "interface-alias",
1657
+ name: aliasName,
1658
+ target: existingName
1659
+ };
1660
+ context.declarations.push(aliasDeclaration);
1661
+ context.declarationByName.set(aliasName, aliasDeclaration);
1662
+ }
1663
+ }
1664
+ return existingName;
1665
+ }
1666
+ if (compatibleDeclaration) {
1667
+ if (this.isObjectShapeAssignableTo(shape, compatibleDeclaration.rawShape)) {
1668
+ context.nameBySignature.set(shape.signature, compatibleDeclaration.name);
1669
+ return compatibleDeclaration.name;
1670
+ }
1671
+ const mergedShape = this.mergeObjectShapes(compatibleDeclaration.rawShape, shape);
1672
+ compatibleDeclaration.rawShape = mergedShape;
1673
+ compatibleDeclaration.properties = mergedShape.properties.map((property) => ({
1674
+ ...property,
1675
+ shape: this.prepareNestedShape(property.shape, property.key, context)
1676
+ }));
1677
+ context.nameBySignature.set(shape.signature, compatibleDeclaration.name);
1678
+ context.nameBySignature.set(mergedShape.signature, compatibleDeclaration.name);
1679
+ return compatibleDeclaration.name;
1680
+ }
1681
+ const declarationName = this.createUniqueTypeName(preferredName, context, collisionSuffix);
1682
+ const declaration = {
1683
+ kind: "interface",
1684
+ name: declarationName,
1685
+ baseName: this.naming.sanitizeTypeName(preferredName),
1686
+ rawShape: shape,
1687
+ properties: []
1688
+ };
1689
+ context.nameBySignature.set(shape.signature, declarationName);
1690
+ context.declarations.push(declaration);
1691
+ context.declarationByName.set(declarationName, declaration);
1692
+ declaration.properties = shape.properties.map((property) => ({
1693
+ ...property,
1694
+ shape: this.prepareNestedShape(property.shape, property.key, context)
1695
+ }));
1696
+ return declarationName;
1697
+ }
1698
+ resolveSdkResponseType(responses, fallbackType) {
1699
+ const successResponse = Object.entries(responses).filter(([statusCode]) => /^2\d\d$/.test(statusCode)).sort(([left], [right]) => left.localeCompare(right))[0]?.[1];
1700
+ if (!successResponse) return fallbackType;
1701
+ const mediaType = this.getPreferredMediaType(successResponse.content);
1702
+ if (!mediaType) return fallbackType;
1703
+ const responseSchema = this.resolveResponsePayloadSchema(mediaType.schema, mediaType.example).schema ?? mediaType.schema;
1704
+ return responseSchema && this.resolveSchemaType(responseSchema) === "array" ? `${fallbackType}[]` : fallbackType;
1705
+ }
1706
+ getSuccessResponseShape(responses) {
1707
+ const successResponse = Object.entries(responses).filter(([statusCode]) => /^2\d\d$/.test(statusCode)).sort(([left], [right]) => left.localeCompare(right))[0]?.[1];
1708
+ if (!successResponse) return this.emptyObjectShape;
1709
+ const mediaType = this.getPreferredMediaType(successResponse.content);
1710
+ if (!mediaType) return this.emptyObjectShape;
1711
+ const payload = this.resolveResponsePayloadSchema(mediaType.schema, mediaType.example);
1712
+ if (!payload.schema) return this.schemaToShape(mediaType.schema, "Response", mediaType.example);
1713
+ if (this.resolveSchemaType(payload.schema) === "array") return this.schemaToShape(payload.schema.items, "Item", this.extractExampleArrayItem(payload.example));
1714
+ return this.schemaToShape(payload.schema, "Response", payload.example);
1715
+ }
1716
+ getRequestInputShape(requestBody) {
1717
+ if (!requestBody) return this.emptyObjectShape;
1718
+ const mediaType = this.getPreferredMediaType(requestBody.content);
1719
+ if (!mediaType) return this.emptyObjectShape;
1720
+ return this.schemaToShape(mediaType.schema, "Input", mediaType.example);
1721
+ }
1722
+ getResponseExampleShape(responses) {
1723
+ const shapes = Object.entries(responses).sort(([left], [right]) => left.localeCompare(right)).flatMap(([, response]) => {
1724
+ const mediaType = this.getPreferredMediaType(response.content);
1725
+ if (!mediaType) return [];
1726
+ const fullExample = mediaType.example ?? mediaType.schema?.example;
1727
+ if (mediaType.schema) return [this.schemaToShape(mediaType.schema, "ResponseExample", fullExample)];
1728
+ if (fullExample !== void 0) return [this.inferShapeFromExample(fullExample, "ResponseExample")];
1729
+ return [];
1730
+ });
1731
+ const uniqueShapes = this.dedupeShapes(shapes);
1732
+ if (uniqueShapes.length === 0) return {
1733
+ kind: "primitive",
1734
+ type: "unknown"
1735
+ };
1736
+ return uniqueShapes.length === 1 ? uniqueShapes[0] : {
1737
+ kind: "union",
1738
+ types: uniqueShapes
1739
+ };
1740
+ }
1741
+ createParameterGroupShape(parameters, location, path) {
1742
+ const relevantParameters = (parameters ?? []).filter((parameter) => parameter.in === location).sort((left, right) => left.name.localeCompare(right.name));
1743
+ const mergedParameters = [...relevantParameters, ...this.naming.getInferredPathParameters(path, location, relevantParameters)].sort((left, right) => left.name.localeCompare(right.name));
1744
+ if (mergedParameters.length === 0) return this.emptyObjectShape;
1745
+ return this.createObjectShape(mergedParameters.map((parameter) => ({
1746
+ key: parameter.name,
1747
+ optional: !(parameter.required ?? false),
1748
+ shape: this.schemaToShape(parameter.schema, parameter.name, parameter.example)
1749
+ })));
1750
+ }
1751
+ get emptyObjectShape() {
1752
+ return this.createObjectShape([]);
1753
+ }
1754
+ findCompatibleObjectDeclaration(shape, preferredName, context) {
1755
+ const baseName = this.naming.sanitizeTypeName(preferredName);
1756
+ return context.declarations.find((declaration) => {
1757
+ if (declaration.kind !== "interface" || declaration.baseName !== baseName) return false;
1758
+ return this.isObjectShapeAssignableTo(shape, declaration.rawShape) || this.isObjectShapeAssignableTo(declaration.rawShape, shape) || this.canMergeObjectShapes(declaration.rawShape, shape);
1759
+ });
1760
+ }
1761
+ canMergeObjectShapes(left, right) {
1762
+ const keys = new Set([...left.properties.map((property) => property.key), ...right.properties.map((property) => property.key)]);
1763
+ for (const key of keys) {
1764
+ const leftProperty = left.properties.find((property) => property.key === key);
1765
+ const rightProperty = right.properties.find((property) => property.key === key);
1766
+ if (!leftProperty || !rightProperty) {
1767
+ if (!(leftProperty ?? rightProperty)?.optional) return false;
1768
+ continue;
1769
+ }
1770
+ if (!this.canMergeShapes(leftProperty.shape, rightProperty.shape)) return false;
1771
+ }
1772
+ return true;
1773
+ }
1774
+ isObjectShapeAssignableTo(source, target) {
1775
+ const targetProperties = new Map(target.properties.map((property) => [property.key, property]));
1776
+ for (const sourceProperty of source.properties) {
1777
+ const targetProperty = targetProperties.get(sourceProperty.key);
1778
+ if (!targetProperty) return false;
1779
+ if (sourceProperty.optional && !targetProperty.optional) return false;
1780
+ if (!this.isShapeAssignableTo(sourceProperty.shape, targetProperty.shape)) return false;
1781
+ }
1782
+ return target.properties.every((targetProperty) => {
1783
+ return source.properties.some((sourceProperty) => sourceProperty.key === targetProperty.key) || targetProperty.optional;
1784
+ });
1785
+ }
1786
+ isShapeAssignableTo(source, target) {
1787
+ if (target.kind === "union") return target.types.some((targetType) => this.isShapeAssignableTo(source, targetType));
1788
+ switch (source.kind) {
1789
+ case "primitive":
1790
+ if (target.kind !== "primitive") return false;
1791
+ return source.type === target.type;
1792
+ case "array":
1793
+ if (target.kind !== "array") return false;
1794
+ return this.isShapeAssignableTo(source.item, target.item);
1795
+ case "union": return source.types.every((sourceType) => this.isShapeAssignableTo(sourceType, target));
1796
+ case "object":
1797
+ if (target.kind !== "object") return false;
1798
+ return this.isObjectShapeAssignableTo(source, target);
1799
+ }
1800
+ }
1801
+ canMergeShapes(left, right) {
1802
+ if (left.kind === "union") return left.types.every((leftType) => this.canMergeShapes(leftType, right));
1803
+ if (right.kind === "union") return right.types.every((rightType) => this.canMergeShapes(left, rightType));
1804
+ if (left.kind === "primitive" && right.kind === "primitive") return true;
1805
+ if (left.kind === "array" && right.kind === "array") return this.canMergeShapes(left.item, right.item);
1806
+ if (left.kind === "object" && right.kind === "object") return this.canMergeObjectShapes(left, right);
1807
+ return false;
1808
+ }
1809
+ mergeObjectShapes(left, right) {
1810
+ const keys = new Set([...left.properties.map((property) => property.key), ...right.properties.map((property) => property.key)]);
1811
+ return this.createObjectShape(Array.from(keys).map((key) => {
1812
+ const leftProperty = left.properties.find((property) => property.key === key);
1813
+ const rightProperty = right.properties.find((property) => property.key === key);
1814
+ if (leftProperty && rightProperty) return {
1815
+ key,
1816
+ optional: leftProperty.optional || rightProperty.optional,
1817
+ shape: this.mergeShapes(leftProperty.shape, rightProperty.shape)
1818
+ };
1819
+ return {
1820
+ key,
1821
+ optional: true,
1822
+ shape: (leftProperty ?? rightProperty).shape
1823
+ };
1824
+ }));
1825
+ }
1826
+ mergeShapes(left, right) {
1827
+ if (left.kind === "union" || right.kind === "union") return this.createUnionShape(left, right);
1828
+ if (left.kind !== right.kind) return this.createUnionShape(left, right);
1829
+ switch (left.kind) {
1830
+ case "primitive": return right.kind === "primitive" && left.type === right.type ? left : this.createUnionShape(left, right);
1831
+ case "array":
1832
+ if (right.kind !== "array") return left;
1833
+ return {
1834
+ kind: "array",
1835
+ item: this.mergeShapes(left.item, right.item)
1836
+ };
1837
+ case "object":
1838
+ if (right.kind !== "object") return left;
1839
+ return this.mergeObjectShapes(left, right);
1840
+ }
1841
+ }
1842
+ createUnionShape(...shapes) {
1843
+ const flattened = shapes.flatMap((shape) => shape.kind === "union" ? shape.types : [shape]);
1844
+ const deduped = this.dedupeShapes(flattened);
1845
+ return deduped.length === 1 ? deduped[0] : {
1846
+ kind: "union",
1847
+ types: deduped
1848
+ };
1849
+ }
1850
+ prepareNestedShape(shape, keyHint, context) {
1851
+ if (shape.kind === "object") return {
1852
+ kind: "primitive",
1853
+ type: this.registerObjectShape(shape, this.naming.sanitizeTypeName(this.naming.singularize(keyHint)), context, this.naming.sanitizeTypeName(keyHint))
1854
+ };
1855
+ if (shape.kind === "array") return {
1856
+ kind: "array",
1857
+ item: this.prepareNestedShape(shape.item, this.naming.singularize(keyHint), context)
1858
+ };
1859
+ if (shape.kind === "union") {
1860
+ const preparedTypes = this.dedupeShapes(shape.types.map((entry, index) => {
1861
+ return this.prepareNestedShape(entry, this.getUnionMemberKeyHint(keyHint, index, entry), context);
1862
+ }));
1863
+ if (preparedTypes.length === 1) return preparedTypes[0];
1864
+ return {
1865
+ kind: "union",
1866
+ types: preparedTypes
1867
+ };
1868
+ }
1869
+ return shape;
1870
+ }
1871
+ getUnionMemberKeyHint(keyHint, index, shape) {
1872
+ if (shape.kind !== "object" && shape.kind !== "array") return keyHint;
1873
+ const sanitizedKeyHint = this.naming.sanitizeTypeName(keyHint);
1874
+ if (sanitizedKeyHint.endsWith("ResponseExample")) return `${sanitizedKeyHint}Variant${index + 1}`;
1875
+ return keyHint;
1876
+ }
1877
+ schemaToShape(schema, nameHint, fallbackExample) {
1878
+ if (!schema) return this.inferShapeFromExample(fallbackExample, nameHint);
1879
+ const schemaType = this.resolveSchemaType(schema);
1880
+ if (schemaType === "array") return {
1881
+ kind: "array",
1882
+ item: this.schemaToShape(schema.items, this.naming.singularize(nameHint), this.extractExampleArrayItem(schema.example) ?? this.extractExampleArrayItem(fallbackExample))
1883
+ };
1884
+ if (schemaType === "object") {
1885
+ const propertyExamples = this.isRecord(schema.example) ? schema.example : this.isRecord(fallbackExample) ? fallbackExample : void 0;
1886
+ const properties = Object.entries(schema.properties ?? {}).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => ({
1887
+ key,
1888
+ optional: !(schema.required ?? []).includes(key),
1889
+ shape: this.schemaToShape(entry, key, propertyExamples?.[key])
1890
+ }));
1891
+ if (properties.length > 0) return this.createObjectShape(properties);
1892
+ return this.inferShapeFromExample(schema.example ?? fallbackExample, nameHint);
1893
+ }
1894
+ if (schemaType === "integer" || schemaType === "number") return {
1895
+ kind: "primitive",
1896
+ type: "number"
1897
+ };
1898
+ if (schemaType === "string") return {
1899
+ kind: "primitive",
1900
+ type: "string"
1901
+ };
1902
+ if (schemaType === "boolean") return {
1903
+ kind: "primitive",
1904
+ type: "boolean"
1905
+ };
1906
+ if (schema.example === null || fallbackExample === null) return {
1907
+ kind: "primitive",
1908
+ type: "null"
1909
+ };
1910
+ if (schema.example !== void 0 || fallbackExample !== void 0) return this.inferShapeFromExample(schema.example ?? fallbackExample, nameHint);
1911
+ return {
1912
+ kind: "primitive",
1913
+ type: "unknown"
1914
+ };
1915
+ }
1916
+ dedupeShapes(shapes) {
1917
+ const seen = /* @__PURE__ */ new Set();
1918
+ return shapes.filter((shape) => {
1919
+ const signature = this.getShapeSignature(shape);
1920
+ if (seen.has(signature)) return false;
1921
+ seen.add(signature);
1922
+ return true;
1923
+ });
1924
+ }
1925
+ createObjectShape(properties) {
1926
+ const normalizedProperties = properties.map((property) => ({ ...property })).sort((left, right) => left.key.localeCompare(right.key));
1927
+ return {
1928
+ kind: "object",
1929
+ signature: JSON.stringify(normalizedProperties.map((property) => ({
1930
+ key: property.key,
1931
+ optional: property.optional,
1932
+ shape: this.getShapeSignature(property.shape)
1933
+ }))),
1934
+ properties: normalizedProperties
1935
+ };
1936
+ }
1937
+ getShapeSignature(shape) {
1938
+ switch (shape.kind) {
1939
+ case "primitive": return `primitive:${shape.type}`;
1940
+ case "array": return `array:${this.getShapeSignature(shape.item)}`;
1941
+ case "union": return `union:${shape.types.map((entry) => this.getShapeSignature(entry)).join("|")}`;
1942
+ case "object": return `object:${shape.signature}`;
1943
+ }
1944
+ }
1945
+ getPreferredMediaType(content) {
1946
+ if (!content) return;
1947
+ return content["application/json"] ?? content["application/*+json"] ?? Object.values(content)[0];
1948
+ }
1949
+ resolveResponsePayloadSchema(schema, example) {
1950
+ for (const path of [["data"], ["meta", "data"]]) {
1951
+ const candidate = this.getSchemaCandidateAtPath(schema, example, path);
1952
+ if (candidate) return candidate;
1953
+ }
1954
+ return {};
1955
+ }
1956
+ getSchemaCandidateAtPath(schema, example, path) {
1957
+ const schemaAtPath = this.getSchemaAtPath(schema, path);
1958
+ const exampleAtPath = this.getExampleAtPath(example, path);
1959
+ if (!schemaAtPath && exampleAtPath === void 0) return;
1960
+ if (schemaAtPath) return {
1961
+ schema: schemaAtPath.example === void 0 && exampleAtPath !== void 0 ? {
1962
+ ...schemaAtPath,
1963
+ example: exampleAtPath
1964
+ } : schemaAtPath,
1965
+ example: exampleAtPath ?? schemaAtPath.example
1966
+ };
1967
+ return {
1968
+ schema: {
1969
+ ...this.inferSchemaTypeFromExample(exampleAtPath),
1970
+ example: exampleAtPath
1971
+ },
1972
+ example: exampleAtPath
1973
+ };
1974
+ }
1975
+ getSchemaAtPath(schema, path) {
1976
+ let currentSchema = schema;
1977
+ for (const segment of path) {
1978
+ if (!currentSchema?.properties?.[segment]) return;
1979
+ currentSchema = currentSchema.properties[segment];
1980
+ }
1981
+ return currentSchema;
1982
+ }
1983
+ getExampleAtPath(example, path) {
1984
+ let currentValue = example;
1985
+ for (const segment of path) {
1986
+ if (!this.isRecord(currentValue) || !(segment in currentValue)) return;
1987
+ currentValue = currentValue[segment];
1988
+ }
1989
+ return currentValue;
1990
+ }
1991
+ inferSchemaTypeFromExample(value) {
1992
+ if (Array.isArray(value)) return {
1993
+ type: "array",
1994
+ items: value.map((entry) => this.inferSchemaTypeFromExample(entry)).find((entry) => this.hasSchemaDetails(entry)) ?? {}
1995
+ };
1996
+ if (this.isRecord(value)) return {
1997
+ type: "object",
1998
+ properties: Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, this.inferSchemaTypeFromExample(entry)]))
1999
+ };
2000
+ if (typeof value === "string") return { type: "string" };
2001
+ if (typeof value === "number") return { type: "number" };
2002
+ if (typeof value === "boolean") return { type: "boolean" };
2003
+ return {};
2004
+ }
2005
+ hasSchemaDetails(schema) {
2006
+ return Boolean(schema?.type || schema?.properties || schema?.items || schema?.example !== void 0);
2007
+ }
2008
+ resolveSchemaType(schema) {
2009
+ return schema.type ?? (schema.properties ? "object" : void 0);
2010
+ }
2011
+ extractExampleArrayItem(value) {
2012
+ return Array.isArray(value) ? value[0] : void 0;
2013
+ }
2014
+ createUniqueTypeName(preferredName, context, collisionSuffix) {
2015
+ const baseName = this.naming.sanitizeTypeName(preferredName) || "GeneratedEntity";
2016
+ const collisionName = this.naming.sanitizeTypeName(collisionSuffix);
2017
+ let candidate = baseName;
2018
+ let suffix = 2;
2019
+ if (!context.usedNames.has(candidate)) {
2020
+ context.usedNames.add(candidate);
2021
+ return candidate;
2022
+ }
2023
+ candidate = this.naming.insertCollisionSuffix(baseName, collisionName);
2024
+ if (!context.usedNames.has(candidate)) {
2025
+ context.usedNames.add(candidate);
2026
+ return candidate;
2027
+ }
2028
+ while (context.usedNames.has(candidate)) {
2029
+ candidate = `${baseName}${suffix}`;
2030
+ suffix += 1;
2031
+ }
2032
+ context.usedNames.add(candidate);
2033
+ return candidate;
2034
+ }
2035
+ isRecord(value) {
2036
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2037
+ }
2038
+ };
2039
+
2040
+ //#endregion
2041
+ //#region src/generator/TypeScriptTypeBuilder.ts
2042
+ var TypeScriptTypeBuilder = class {
2043
+ naming = new TypeScriptNamingSupport();
2044
+ shapes = new TypeScriptShapeBuilder(this.naming);
2045
+ createContext() {
2046
+ return this.shapes.createContext();
2047
+ }
2048
+ collectSemanticModels(document) {
2049
+ const models = [];
2050
+ for (const [path, operations] of Object.entries(document.paths)) {
2051
+ const naming = this.naming.deriveOperationNaming(path);
2052
+ const baseName = naming.baseName;
2053
+ const sortedOperations = Object.entries(operations).sort(([, leftOperation], [, rightOperation]) => {
2054
+ return this.getOperationPriority(rightOperation) - this.getOperationPriority(leftOperation);
2055
+ });
2056
+ for (const [method, operation] of sortedOperations) {
2057
+ const collisionSuffix = naming.collisionSuffix || this.naming.fallbackCollisionSuffix(method, path, baseName);
2058
+ models.push({
2059
+ path,
2060
+ method,
2061
+ name: baseName,
2062
+ role: "response",
2063
+ shape: this.shapes.getSuccessResponseShape(operation.responses),
2064
+ collisionSuffix
2065
+ });
2066
+ models.push({
2067
+ path,
2068
+ method,
2069
+ name: `${baseName}ResponseExample`,
2070
+ role: "responseExample",
2071
+ shape: this.shapes.getResponseExampleShape(operation.responses),
2072
+ collisionSuffix
2073
+ });
2074
+ models.push({
2075
+ path,
2076
+ method,
2077
+ name: `${baseName}Input`,
2078
+ role: "input",
2079
+ shape: this.shapes.getRequestInputShape(operation.requestBody),
2080
+ collisionSuffix
2081
+ });
2082
+ models.push({
2083
+ path,
2084
+ method,
2085
+ name: `${baseName}Query`,
2086
+ role: "query",
2087
+ shape: this.shapes.createParameterGroupShape(operation.parameters, "query", path),
2088
+ collisionSuffix
2089
+ });
2090
+ models.push({
2091
+ path,
2092
+ method,
2093
+ name: `${baseName}Header`,
2094
+ role: "header",
2095
+ shape: this.shapes.createParameterGroupShape(operation.parameters, "header", path),
2096
+ collisionSuffix
2097
+ });
2098
+ models.push({
2099
+ path,
2100
+ method,
2101
+ name: `${baseName}Params`,
2102
+ role: "params",
2103
+ shape: this.shapes.createParameterGroupShape(operation.parameters, "path", path),
2104
+ collisionSuffix
2105
+ });
2106
+ }
2107
+ }
2108
+ return models;
2109
+ }
2110
+ buildSdkManifest(document, operationTypeRefs, options = {}) {
2111
+ const sdkGroupNamesBySignature = this.naming.deriveSdkGroupNamesBySignature(document, options.namespaceStrategy ?? "smart");
2112
+ const groups = /* @__PURE__ */ new Map();
2113
+ for (const [path, operations] of Object.entries(document.paths)) {
2114
+ const staticSignature = this.naming.getStaticPathSegments(path).join("/");
2115
+ const className = sdkGroupNamesBySignature.get(staticSignature) ?? "Resource";
2116
+ const propertyName = this.naming.toCamelCase(this.naming.pluralize(className));
2117
+ const group = groups.get(propertyName) ?? {
2118
+ className,
2119
+ propertyName,
2120
+ operations: []
2121
+ };
2122
+ for (const [method, operation] of Object.entries(operations)) {
2123
+ const refs = operationTypeRefs.get(`${path}::${method}`) ?? {
2124
+ response: "Record<string, never>",
2125
+ responseExample: "unknown",
2126
+ input: "Record<string, never>",
2127
+ query: "Record<string, never>",
2128
+ header: "Record<string, never>",
2129
+ params: "Record<string, never>"
2130
+ };
2131
+ group.operations.push({
2132
+ path,
2133
+ method: method.toUpperCase(),
2134
+ methodName: this.naming.deriveSdkMethodName(method, path, operation, options.methodStrategy ?? "smart"),
2135
+ summary: operation.summary,
2136
+ description: operation.description,
2137
+ operationId: operation.operationId,
2138
+ requestBodyDescription: operation.requestBody?.description,
2139
+ responseDescription: this.resolveSuccessResponseDescription(operation.responses),
2140
+ responseType: this.shapes.resolveSdkResponseType(operation.responses, refs.response),
2141
+ inputType: refs.input,
2142
+ queryType: refs.query,
2143
+ headerType: refs.header,
2144
+ paramsType: refs.params,
2145
+ hasBody: Boolean(operation.requestBody),
2146
+ bodyRequired: operation.requestBody?.required ?? false,
2147
+ pathParams: this.naming.createSdkParameterManifest(operation.parameters, "path", path),
2148
+ queryParams: this.naming.createSdkParameterManifest(operation.parameters, "query", path),
2149
+ headerParams: this.naming.createSdkParameterManifest(operation.parameters, "header", path)
2150
+ });
2151
+ }
2152
+ groups.set(propertyName, group);
2153
+ }
2154
+ return { groups: Array.from(groups.values()).map((group) => ({
2155
+ ...group,
2156
+ operations: this.naming.ensureUniqueSdkMethodNames(group.operations)
2157
+ })).sort((left, right) => left.propertyName.localeCompare(right.propertyName)) };
2158
+ }
2159
+ inferShapeFromExample(value, nameHint) {
2160
+ return this.shapes.inferShapeFromExample(value, nameHint);
2161
+ }
2162
+ sanitizeTypeName(value) {
2163
+ return this.naming.sanitizeTypeName(value);
2164
+ }
2165
+ registerNamedShape(shape, preferredName, context, collisionSuffix) {
2166
+ return this.shapes.registerNamedShape(shape, preferredName, context, collisionSuffix);
2167
+ }
2168
+ namespaceTopLevelShape(shape, role) {
2169
+ return this.shapes.namespaceTopLevelShape(shape, role);
2170
+ }
2171
+ registerObjectShape(shape, preferredName, context, collisionSuffix, emitAlias = false) {
2172
+ return this.shapes.registerObjectShape(shape, preferredName, context, collisionSuffix, emitAlias);
2173
+ }
2174
+ getOperationPriority(operation) {
2175
+ return Number(Boolean(operation.requestBody)) * 10;
2176
+ }
2177
+ resolveSuccessResponseDescription(responses) {
2178
+ for (const statusCode of [
2179
+ "200",
2180
+ "201",
2181
+ "202",
2182
+ "204"
2183
+ ]) {
2184
+ const description = responses[statusCode]?.description?.trim();
2185
+ if (description) return description;
2186
+ }
2187
+ for (const response of Object.values(responses)) {
2188
+ const description = response.description?.trim();
2189
+ if (description) return description;
2190
+ }
2191
+ }
2192
+ };
2193
+
2194
+ //#endregion
2195
+ //#region src/generator/TypeScriptGenerator.ts
2196
+ var TypeScriptGenerator = class TypeScriptGenerator {
2197
+ typeBuilder = new TypeScriptTypeBuilder();
2198
+ moduleRenderer = new TypeScriptModuleRenderer();
2199
+ /**
2200
+ * Static helper method to generate a TypeScript module string from a generic JSON-like
2201
+ * value, inferring types and creating type definitions as needed, using the provided root
2202
+ * type name.
2203
+ *
2204
+ * @param value
2205
+ * @param rootTypeName
2206
+ * @returns
2207
+ */
2208
+ static generateModule = (value, rootTypeName = "GeneratedOutput", options = {}) => {
2209
+ return new TypeScriptGenerator().generate(value, rootTypeName, options);
2210
+ };
2211
+ /**
2212
+ * Generate a TypeScript module string from a generic JSON-like value, inferring types
2213
+ * and creating type definitions as needed.
2214
+ *
2215
+ * @param value
2216
+ * @param rootTypeName
2217
+ * @returns
2218
+ */
2219
+ generate(value, rootTypeName = "GeneratedOutput", options = {}) {
2220
+ if (this.isOpenApiDocumentLike(value)) return this.generateModule(value, rootTypeName, options);
2221
+ return this.generateGenericModule(value, rootTypeName);
2222
+ }
2223
+ /**
2224
+ * Generate a TypeScript module string from an OpenAPI document, including type definitions
2225
+ *
2226
+ * @param document
2227
+ * @param rootTypeName
2228
+ * @returns
2229
+ */
2230
+ generateModule(document, rootTypeName, options = {}) {
2231
+ const context = this.typeBuilder.createContext();
2232
+ const operationTypeRefs = /* @__PURE__ */ new Map();
2233
+ for (const model of this.typeBuilder.collectSemanticModels(document)) {
2234
+ const operationKey = `${model.path}::${model.method}`;
2235
+ const resolvedName = this.typeBuilder.registerNamedShape(this.typeBuilder.namespaceTopLevelShape(model.shape, model.role), model.name, context, model.collisionSuffix);
2236
+ const existingRefs = operationTypeRefs.get(operationKey) ?? {
2237
+ response: "Record<string, never>",
2238
+ responseExample: "unknown",
2239
+ input: "Record<string, never>",
2240
+ query: "Record<string, never>",
2241
+ header: "Record<string, never>",
2242
+ params: "Record<string, never>"
2243
+ };
2244
+ existingRefs[model.role] = resolvedName;
2245
+ operationTypeRefs.set(operationKey, existingRefs);
2246
+ }
2247
+ const declarations = context.declarations.map((declaration) => this.moduleRenderer.renderDeclaration(declaration)).join("\n\n");
2248
+ const variableName = this.moduleRenderer.toCamelCase(rootTypeName);
2249
+ const sdkManifest = this.typeBuilder.buildSdkManifest(document, operationTypeRefs, options);
2250
+ return [
2251
+ declarations,
2252
+ this.moduleRenderer.renderOpenApiDocumentDefinitions(rootTypeName, document, operationTypeRefs),
2253
+ this.moduleRenderer.renderSdkApiInterface(rootTypeName, sdkManifest),
2254
+ this.moduleRenderer.renderSdkManifest(variableName, sdkManifest),
2255
+ `export const ${variableName}: ${rootTypeName} = ${this.moduleRenderer.renderOpenApiDocumentValue(document)}`,
2256
+ this.moduleRenderer.renderSdkBundle(variableName, rootTypeName),
2257
+ "",
2258
+ `export default ${variableName}`
2259
+ ].filter(Boolean).join("\n\n");
2260
+ }
2261
+ generateGenericModule(value, rootTypeName) {
2262
+ const context = this.typeBuilder.createContext();
2263
+ const rootShape = this.typeBuilder.inferShapeFromExample(value, rootTypeName);
2264
+ const rootSanitizedName = this.typeBuilder.sanitizeTypeName(rootTypeName);
2265
+ let rootType = rootSanitizedName;
2266
+ if (rootShape.kind === "object") rootType = this.typeBuilder.registerObjectShape(rootShape, rootSanitizedName, context, rootSanitizedName);
2267
+ else {
2268
+ const declaration = {
2269
+ kind: "shape-alias",
2270
+ name: rootSanitizedName,
2271
+ shape: rootShape
2272
+ };
2273
+ context.declarations.push(declaration);
2274
+ context.declarationByName.set(rootSanitizedName, declaration);
2275
+ }
2276
+ const rootAlias = rootType === rootTypeName ? "" : `export type ${rootTypeName} = ${rootType}`;
2277
+ const variableName = this.moduleRenderer.toCamelCase(rootTypeName);
2278
+ return [
2279
+ context.declarations.map((declaration) => this.moduleRenderer.renderDeclaration(declaration)).join("\n\n"),
2280
+ rootAlias,
2281
+ `export const ${variableName}: ${rootTypeName} = ${this.moduleRenderer.renderValue(value)}`,
2282
+ "",
2283
+ `export default ${variableName}`
2284
+ ].filter(Boolean).join("\n\n");
2285
+ }
2286
+ isOpenApiDocumentLike(value) {
2287
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
2288
+ const candidate = value;
2289
+ if (typeof candidate.info !== "object" || candidate.info === null || Array.isArray(candidate.info)) return false;
2290
+ const info = candidate.info;
2291
+ return candidate.openapi === "3.1.0" && typeof info.title === "string" && typeof info.version === "string" && typeof candidate.paths === "object" && candidate.paths !== null && !Array.isArray(candidate.paths);
2292
+ }
2293
+ };
2294
+
2295
+ //#endregion
2296
+ //#region src/generator/OutputGenerator.ts
2297
+ var OutputGenerator = class {
2298
+ /**
2299
+ * Serialize the extracted payload into the desired output format, optionally
2300
+ * generating TypeScript types when requested.
2301
+ *
2302
+ * @param payload The extracted payload to serialize.
2303
+ * @param outputFormat The desired output format ('json', 'js', 'ts', 'pretty').
2304
+ * @param rootTypeName The root type name to use when generating TypeScript types.
2305
+ * @returns A promise that resolves to the serialized output string.
2306
+
2307
+ */
2308
+ static serializeOutput = async (payload, outputFormat, rootTypeName = "ExtractedApiDocument", typeScriptOptions = {}) => {
2309
+ if (outputFormat === "js") return prettier.default.format(`export default ${JSON.stringify(payload, null, 2)}`, {
2310
+ parser: "babel",
2311
+ semi: false,
2312
+ singleQuote: true
2313
+ });
2314
+ if (outputFormat === "ts") return prettier.default.format(TypeScriptGenerator.generateModule(payload, rootTypeName, typeScriptOptions), {
2315
+ parser: "typescript",
2316
+ semi: false,
2317
+ singleQuote: true
2318
+ });
2319
+ return JSON.stringify(payload, null, outputFormat === "json" ? 0 : 2);
2320
+ };
2321
+ /**
2322
+ * Build a safe file path for the output based on the workspace root, source
2323
+ * identifier, desired shape, and output format.
2324
+ *
2325
+ * @param workspaceRoot The root directory of the workspace.
2326
+ * @param source The original source identifier
2327
+ * @param shape The desired output shape ('raw' or 'openapi').
2328
+ * @param outputFormat The desired output format ('json', 'js', 'ts', 'pretty').
2329
+ * @returns The constructed file path for the output.
2330
+ */
2331
+ static buildFilePath = (workspaceRoot, source, shape, outputFormat) => {
2332
+ const ext = {
2333
+ pretty: "txt",
2334
+ json: "json",
2335
+ js: "js",
2336
+ ts: "ts"
2337
+ }[outputFormat];
2338
+ const safeSource = this.toSafeSourceName(source);
2339
+ const shapeSuffix = shape === "openapi" ? ".openapi" : "";
2340
+ return node_path.default.join(workspaceRoot, "output", `${safeSource || "output"}${shapeSuffix}.${ext}`);
2341
+ };
2342
+ static buildArtifactDirectory = (workspaceRoot, source, artifact) => {
2343
+ const safeSource = this.toSafeSourceName(source);
2344
+ return node_path.default.join(workspaceRoot, "output", `${safeSource || artifact}.${artifact}`);
2345
+ };
2346
+ static toSafeSourceName(source) {
2347
+ return source.replace(/[^a-zA-Z0-9_-]+/g, "_").replace(/^_+|_+$/g, "");
2348
+ }
2349
+ /**
2350
+ * Determine the appropriate root type name for TypeScript generation based on the desired
2351
+ * output shape. This helps ensure that generated types are meaningful and contextually relevant.
2352
+ *
2353
+ * @param shape The desired output shape ('raw' or 'openapi').
2354
+ * @returns The root type name to use for TypeScript generation.
2355
+ */
2356
+ static getRootTypeName = (shape) => {
2357
+ return shape === "openapi" ? "ExtractedApiDocument" : "ExtractedPayload";
2358
+ };
2359
+ };
2360
+
2361
+ //#endregion
2362
+ //#region src/generator/SdkPackageGenerator.ts
2363
+ var SdkPackageGenerator = class {
2364
+ typeBuilder = new TypeScriptTypeBuilder();
2365
+ typeScriptGenerator = new TypeScriptGenerator();
2366
+ generate(document, options = {}) {
2367
+ const outputMode = options.outputMode ?? "both";
2368
+ const signatureStyle = options.signatureStyle ?? "grouped";
2369
+ const rootTypeName = options.rootTypeName ?? "ExtractedApiDocument";
2370
+ const schemaModule = options.schemaModule ?? this.typeScriptGenerator.generateModule(document, rootTypeName, options);
2371
+ const operationTypeRefs = this.createOperationTypeRefs(document);
2372
+ const manifest = this.typeBuilder.buildSdkManifest(document, operationTypeRefs, options);
2373
+ const classNames = manifest.groups.map((group) => group.className);
2374
+ const files = {
2375
+ "package.json": this.renderPackageJson(options),
2376
+ "README.md": this.renderReadme(manifest, options, outputMode, signatureStyle),
2377
+ "src/Schema.ts": schemaModule,
2378
+ "src/index.ts": this.renderIndexFile(classNames, outputMode, rootTypeName),
2379
+ "tsconfig.json": this.renderTsconfig(),
2380
+ "tsdown.config.ts": this.renderTsdownConfig(),
2381
+ "vitest.config.ts": this.renderVitestConfig(),
2382
+ "tests/exports.test.ts": this.renderExportsTest(rootTypeName, outputMode)
2383
+ };
2384
+ if (outputMode !== "runtime") {
2385
+ files["src/BaseApi.ts"] = this.renderBaseApi();
2386
+ files["src/ApiBinder.ts"] = this.renderApiBinder(manifest);
2387
+ for (const group of manifest.groups) files[`src/Apis/${group.className}.ts`] = this.renderApiClass(group, signatureStyle);
2388
+ files["src/Core.ts"] = this.renderCoreFile();
2389
+ }
2390
+ return files;
2391
+ }
2392
+ createOperationTypeRefs(document) {
2393
+ const context = this.typeBuilder.createContext();
2394
+ const operationTypeRefs = /* @__PURE__ */ new Map();
2395
+ for (const model of this.typeBuilder.collectSemanticModels(document)) {
2396
+ const operationKey = `${model.path}::${model.method}`;
2397
+ const resolvedName = this.typeBuilder.registerNamedShape(this.typeBuilder.namespaceTopLevelShape(model.shape, model.role), model.name, context, model.collisionSuffix);
2398
+ const existingRefs = operationTypeRefs.get(operationKey) ?? {
2399
+ response: "Record<string, never>",
2400
+ responseExample: "unknown",
2401
+ input: "Record<string, never>",
2402
+ query: "Record<string, never>",
2403
+ header: "Record<string, never>",
2404
+ params: "Record<string, never>"
2405
+ };
2406
+ existingRefs[model.role] = resolvedName;
2407
+ operationTypeRefs.set(operationKey, existingRefs);
2408
+ }
2409
+ return operationTypeRefs;
2410
+ }
2411
+ renderBaseApi() {
2412
+ return [
2413
+ "import { BaseApi as KitBaseApi } from '@oapiex/sdk-kit'",
2414
+ "",
2415
+ "export class BaseApi extends KitBaseApi {}"
2416
+ ].join("\n");
2417
+ }
2418
+ renderApiBinder(manifest) {
2419
+ return [
2420
+ "import { BaseApi } from './BaseApi'",
2421
+ "",
2422
+ ...manifest.groups.map((group) => `import { ${group.className} } from './Apis/${group.className}'`),
2423
+ "",
2424
+ "export class ApiBinder extends BaseApi {",
2425
+ ...manifest.groups.map((group) => ` ${group.propertyName}!: ${group.className}`),
2426
+ "",
2427
+ " protected override boot () {",
2428
+ ...manifest.groups.map((group) => ` this.${group.propertyName} = new ${group.className}(this.core)`),
2429
+ " }",
2430
+ "}"
2431
+ ].join("\n");
2432
+ }
2433
+ renderApiClass(group, signatureStyle) {
2434
+ const typeImportContext = this.createTypeImportContext(group, signatureStyle);
2435
+ const imports = ["import { BaseApi } from '../BaseApi'", "import { Http } from '@oapiex/sdk-kit'"];
2436
+ if (typeImportContext.specifiers.length > 0) imports.splice(1, 0, `import type { ${typeImportContext.specifiers.join(", ")} } from '../Schema'`);
2437
+ return [
2438
+ ...imports,
2439
+ "",
2440
+ `export class ${group.className} extends BaseApi {`,
2441
+ "",
2442
+ ...group.operations.flatMap((operation) => [this.renderApiMethod(operation, signatureStyle, typeImportContext.aliasMap), ""]).slice(0, -1),
2443
+ "}"
2444
+ ].join("\n");
2445
+ }
2446
+ renderApiMethod(operation, signatureStyle, aliasMap) {
2447
+ const signature = signatureStyle === "flat" ? this.renderFlatSignature(operation, aliasMap) : this.renderGroupedSignature(operation, aliasMap);
2448
+ const urlPathArgs = signatureStyle === "flat" ? this.renderFlatObjectLiteral(operation.paramsType, operation.pathParams) : operation.pathParams.length > 0 ? "params" : "{}";
2449
+ const urlQueryArgs = signatureStyle === "flat" ? this.renderFlatObjectLiteral(operation.queryType, operation.queryParams) : operation.queryParams.length > 0 ? "query" : "{}";
2450
+ const headerArgs = signatureStyle === "flat" ? this.renderFlatHeaders(operation) : operation.headerParams.length > 0 ? "((headers ? { ...headers } : {}) as Record<string, string | undefined>)" : "{}";
2451
+ const bodyArg = signatureStyle === "flat" ? operation.hasBody ? "body" : "{}" : operation.hasBody ? "body ?? {}" : "{}";
2452
+ const docComment = this.renderMethodDocComment(operation, signatureStyle, aliasMap);
2453
+ return [
2454
+ ...docComment ? [docComment] : [],
2455
+ ` async ${operation.methodName} ${signature}: Promise<${this.rewriteTypeReference(operation.responseType, aliasMap)}> {`,
2456
+ " await this.core.validateAccess()",
2457
+ "",
2458
+ ` const { data } = await Http.send<${this.rewriteTypeReference(operation.responseType, aliasMap)}>(`,
2459
+ ` this.core.builder.buildTargetUrl('${operation.path}', ${urlPathArgs}, ${urlQueryArgs}),`,
2460
+ ` '${operation.method}',`,
2461
+ ` ${bodyArg},`,
2462
+ ` ${headerArgs}`,
2463
+ " )",
2464
+ "",
2465
+ " return data",
2466
+ " }"
2467
+ ].join("\n");
2468
+ }
2469
+ renderMethodDocComment(operation, signatureStyle, aliasMap) {
2470
+ const lines = [];
2471
+ const summary = operation.summary?.trim();
2472
+ const description = operation.description?.trim();
2473
+ const operationId = operation.operationId?.trim();
2474
+ const responseType = this.rewriteTypeReference(operation.responseType, aliasMap);
2475
+ const responseDescription = operation.responseDescription?.trim();
2476
+ if (summary) lines.push(summary);
2477
+ if (description && description !== summary) {
2478
+ if (lines.length > 0) lines.push("");
2479
+ lines.push(...this.wrapDocText(description));
2480
+ }
2481
+ const metadataLines = [`HTTP ${operation.method} ${operation.path}`, ...operationId ? [`Operation ID: ${operationId}`] : []];
2482
+ if (metadataLines.length > 0) {
2483
+ if (lines.length > 0) lines.push("");
2484
+ lines.push(...metadataLines);
2485
+ }
2486
+ const parameterDocs = signatureStyle === "flat" ? this.renderFlatParameterDocs(operation, aliasMap) : this.renderGroupedParameterDocs(operation, aliasMap);
2487
+ if (parameterDocs.length > 0) {
2488
+ if (lines.length > 0) lines.push("");
2489
+ lines.push(...parameterDocs);
2490
+ }
2491
+ lines.push(`@returns ${responseDescription ? `${responseDescription} ` : ""}${responseType}`.trim());
2492
+ return [
2493
+ " /**",
2494
+ ...lines.map((line) => line ? ` * ${line}` : " *"),
2495
+ " */"
2496
+ ].join("\n");
2497
+ }
2498
+ renderGroupedParameterDocs(operation, aliasMap) {
2499
+ const docs = [];
2500
+ if (operation.pathParams.length > 0) docs.push(this.renderParamDoc("params", operation.paramsType, aliasMap, this.describeParameterGroup(operation.pathParams, "path parameters")));
2501
+ if (operation.queryParams.length > 0) docs.push(this.renderParamDoc("query", operation.queryType, aliasMap, this.describeParameterGroup(operation.queryParams, "query parameters")));
2502
+ if (operation.hasBody) docs.push(this.renderParamDoc("body", operation.inputType, aliasMap, operation.requestBodyDescription?.trim() || "Request body"));
2503
+ if (operation.headerParams.length > 0) docs.push(this.renderParamDoc("headers", operation.headerType, aliasMap, this.describeParameterGroup(operation.headerParams, "request headers")));
2504
+ return docs;
2505
+ }
2506
+ renderFlatParameterDocs(operation, aliasMap) {
2507
+ return [
2508
+ ...operation.pathParams.map((parameter) => this.renderParamDoc(parameter.accessor, `${operation.paramsType}[${JSON.stringify(parameter.name)}]`, aliasMap, parameter.description?.trim() || `Path parameter ${parameter.name}`)),
2509
+ ...operation.queryParams.map((parameter) => this.renderParamDoc(parameter.accessor, `${operation.queryType}[${JSON.stringify(parameter.name)}]`, aliasMap, parameter.description?.trim() || `Query parameter ${parameter.name}`)),
2510
+ ...operation.hasBody ? [this.renderParamDoc("body", operation.inputType, aliasMap, operation.requestBodyDescription?.trim() || "Request body")] : [],
2511
+ ...operation.headerParams.map((parameter) => this.renderParamDoc(parameter.accessor, `${operation.headerType}[${JSON.stringify(parameter.name)}]`, aliasMap, parameter.description?.trim() || `Header ${parameter.name}`))
2512
+ ];
2513
+ }
2514
+ renderParamDoc(name, typeRef, aliasMap, description) {
2515
+ return `@param ${name} ${description} Type: ${this.rewriteTypeReference(typeRef, aliasMap)}`;
2516
+ }
2517
+ describeParameterGroup(parameters, fallback) {
2518
+ const described = parameters.map((parameter) => parameter.description?.trim() ? `${parameter.name}: ${parameter.description.trim()}` : parameter.name).filter(Boolean);
2519
+ if (described.length === 0) return fallback;
2520
+ return described.join("; ");
2521
+ }
2522
+ wrapDocText(text) {
2523
+ return text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
2524
+ }
2525
+ renderGroupedSignature(operation, aliasMap) {
2526
+ const args = [];
2527
+ if (operation.pathParams.length > 0) args.push(`params: ${this.rewriteTypeReference(operation.paramsType, aliasMap)}`);
2528
+ if (operation.queryParams.length > 0) args.push(`query: ${this.rewriteTypeReference(operation.queryType, aliasMap)}`);
2529
+ if (operation.hasBody) args.push(`body${operation.bodyRequired ? "" : "?"}: ${this.rewriteTypeReference(operation.inputType, aliasMap)}`);
2530
+ if (operation.headerParams.length > 0) args.push(`headers?: ${this.rewriteTypeReference(operation.headerType, aliasMap)}`);
2531
+ return `(${args.join(", ")})`;
2532
+ }
2533
+ renderFlatSignature(operation, aliasMap) {
2534
+ return `(${[
2535
+ ...operation.pathParams.map((parameter) => `${parameter.accessor}${parameter.required ? "" : "?"}: ${this.rewriteTypeReference(operation.paramsType, aliasMap)}[${JSON.stringify(parameter.name)}]`),
2536
+ ...operation.queryParams.map((parameter) => `${parameter.accessor}${parameter.required ? "" : "?"}: ${this.rewriteTypeReference(operation.queryType, aliasMap)}[${JSON.stringify(parameter.name)}]`),
2537
+ ...operation.hasBody ? [`body${operation.bodyRequired ? "" : "?"}: ${this.rewriteTypeReference(operation.inputType, aliasMap)}`] : [],
2538
+ ...operation.headerParams.map((parameter) => `${parameter.accessor}${parameter.required ? "" : "?"}: ${this.rewriteTypeReference(operation.headerType, aliasMap)}[${JSON.stringify(parameter.name)}]`)
2539
+ ].join(", ")})`;
2540
+ }
2541
+ createTypeImportContext(group, signatureStyle) {
2542
+ const requiredTypeRefs = /* @__PURE__ */ new Set();
2543
+ for (const operation of group.operations) {
2544
+ requiredTypeRefs.add(operation.responseType);
2545
+ if (operation.hasBody) requiredTypeRefs.add(operation.inputType);
2546
+ if (operation.queryParams.length > 0) requiredTypeRefs.add(operation.queryType);
2547
+ if (operation.headerParams.length > 0) requiredTypeRefs.add(operation.headerType);
2548
+ if (operation.pathParams.length > 0) requiredTypeRefs.add(operation.paramsType);
2549
+ if (signatureStyle === "flat") continue;
2550
+ }
2551
+ const identifiers = Array.from(new Set(Array.from(requiredTypeRefs).flatMap((typeRef) => this.collectTypeIdentifiers(typeRef)))).sort();
2552
+ const aliasMap = /* @__PURE__ */ new Map();
2553
+ return {
2554
+ specifiers: identifiers.map((identifier) => {
2555
+ if (identifier === group.className) {
2556
+ const aliasedName = `${identifier}Model`;
2557
+ aliasMap.set(identifier, aliasedName);
2558
+ return `${identifier} as ${aliasedName}`;
2559
+ }
2560
+ return identifier;
2561
+ }),
2562
+ aliasMap
2563
+ };
2564
+ }
2565
+ rewriteTypeReference(typeRef, aliasMap) {
2566
+ let rewritten = typeRef;
2567
+ for (const [identifier, alias] of aliasMap.entries()) rewritten = rewritten.replace(new RegExp(`\\b${identifier}\\b`, "g"), alias);
2568
+ return rewritten;
2569
+ }
2570
+ renderFlatObjectLiteral(_typeName, parameters) {
2571
+ if (parameters.length === 0) return "{}";
2572
+ return `{ ${parameters.map((parameter) => `${JSON.stringify(parameter.name)}: ${parameter.accessor}`).join(", ")} }`;
2573
+ }
2574
+ renderFlatHeaders(operation) {
2575
+ if (operation.headerParams.length === 0) return "{}";
2576
+ return `({ ${operation.headerParams.map((parameter) => `${JSON.stringify(parameter.name)}: ${parameter.accessor}`).join(", ")} } as Record<string, string | undefined>)`;
2577
+ }
2578
+ renderCoreFile() {
2579
+ return [
2580
+ "import { Core as KitCore } from '@oapiex/sdk-kit'",
2581
+ "",
2582
+ "import { ApiBinder } from './ApiBinder'",
2583
+ "",
2584
+ "export class Core extends KitCore {",
2585
+ " static override apiClass = ApiBinder",
2586
+ "",
2587
+ " declare api: ApiBinder",
2588
+ "}"
2589
+ ].join("\n");
2590
+ }
2591
+ renderPackageJson(options) {
2592
+ return JSON.stringify({
2593
+ name: options.packageName ?? "generated-sdk",
2594
+ type: "module",
2595
+ version: options.packageVersion ?? "0.1.0",
2596
+ private: true,
2597
+ description: options.packageDescription ?? "Generated SDK scaffold emitted by oapiex.",
2598
+ main: "./dist/index.cjs",
2599
+ module: "./dist/index.js",
2600
+ types: "./dist/index.d.ts",
2601
+ exports: {
2602
+ ".": {
2603
+ import: "./dist/index.js",
2604
+ require: "./dist/index.cjs"
2605
+ },
2606
+ "./package.json": "./package.json"
2607
+ },
2608
+ files: ["dist"],
2609
+ scripts: {
2610
+ test: "pnpm vitest --run",
2611
+ "test:watch": "pnpm vitest",
2612
+ build: "tsdown"
2613
+ },
2614
+ dependencies: { [options.sdkKitPackageName ?? "@oapiex/sdk-kit"]: "^0.1.1" },
2615
+ devDependencies: {
2616
+ "@types/node": "^20.14.5",
2617
+ tsdown: "^0.20.1",
2618
+ typescript: "^5.4.5",
2619
+ vitest: "^3.2.4"
2620
+ }
2621
+ }, null, 2);
2622
+ }
2623
+ renderReadme(manifest, options, outputMode, signatureStyle) {
2624
+ const packageName = options.packageName ?? "generated-sdk";
2625
+ const title = `# ${packageName}`;
2626
+ const description = this.renderReadmeDescription(outputMode);
2627
+ const usage = this.renderReadmeUsage(manifest, packageName, outputMode, signatureStyle);
2628
+ const exports = this.renderReadmeExports(outputMode);
2629
+ return [
2630
+ title,
2631
+ "",
2632
+ description,
2633
+ "",
2634
+ "## Install",
2635
+ "",
2636
+ "```bash",
2637
+ `pnpm add ${packageName}`,
2638
+ "```",
2639
+ "",
2640
+ "## Quick Start",
2641
+ "",
2642
+ "```ts",
2643
+ usage,
2644
+ "```",
2645
+ "",
2646
+ "## Main Exports",
2647
+ "",
2648
+ ...exports.map((line) => `- ${line}`),
2649
+ "",
2650
+ "## Commands",
2651
+ "",
2652
+ "```bash",
2653
+ "pnpm test",
2654
+ "pnpm build",
2655
+ "```"
2656
+ ].join("\n");
2657
+ }
2658
+ renderReadmeDescription(outputMode) {
2659
+ if (outputMode === "runtime") return "Generated runtime-first TypeScript SDK emitted by oapiex.";
2660
+ if (outputMode === "classes") return "Generated class-based TypeScript SDK emitted by oapiex.";
2661
+ return "Generated TypeScript SDK emitted by oapiex with both class-based and runtime-first entrypoints.";
2662
+ }
2663
+ renderReadmeUsage(manifest, packageName, outputMode, signatureStyle) {
2664
+ const exampleOperation = this.pickExampleOperation(manifest);
2665
+ const runtimeSnippet = this.renderReadmeClientSnippet(packageName, "runtime", signatureStyle, exampleOperation);
2666
+ if (outputMode === "runtime") return runtimeSnippet;
2667
+ const classSnippet = this.renderReadmeClientSnippet(packageName, "classes", signatureStyle, exampleOperation);
2668
+ if (outputMode === "classes") return classSnippet;
2669
+ const typeImports = exampleOperation ? this.collectReadmeTypeImports(exampleOperation.operation) : [];
2670
+ return [
2671
+ typeImports.length > 0 ? `import { Core, createClient, type ${typeImports.join(", type ")} } from '${packageName}'` : `import { Core, createClient } from '${packageName}'`,
2672
+ "",
2673
+ ...this.renderReadmeClientBody("sdk", "classes", signatureStyle, exampleOperation),
2674
+ "",
2675
+ "// --- OR ---",
2676
+ "",
2677
+ ...this.renderReadmeClientBody("runtimeSdk", "runtime", signatureStyle, exampleOperation)
2678
+ ].join("\n");
2679
+ }
2680
+ renderReadmeClientSnippet(packageName, mode, signatureStyle, exampleOperation) {
2681
+ const importNames = mode === "runtime" ? ["createClient"] : ["Core"];
2682
+ const typeImports = exampleOperation ? this.collectReadmeTypeImports(exampleOperation.operation) : [];
2683
+ const importLine = typeImports.length > 0 ? `import { ${importNames.join(", ")}, type ${typeImports.join(", type ")} } from '${packageName}'` : `import { ${importNames.join(", ")} } from '${packageName}'`;
2684
+ const sdkVariable = mode === "runtime" ? "runtimeSdk" : "sdk";
2685
+ return [
2686
+ importLine,
2687
+ "",
2688
+ ...this.renderReadmeClientBody(sdkVariable, mode, signatureStyle, exampleOperation)
2689
+ ].join("\n");
2690
+ }
2691
+ renderReadmeClientBody(sdkVariable, mode, signatureStyle, exampleOperation) {
2692
+ const initLine = mode === "runtime" ? `const ${sdkVariable} = createClient({` : `const ${sdkVariable} = new Core({`;
2693
+ const callLines = exampleOperation ? this.renderReadmeOperationCall(sdkVariable, exampleOperation, mode === "runtime" ? "grouped" : signatureStyle) : [];
2694
+ return [
2695
+ initLine,
2696
+ " clientId: process.env.CLIENT_ID!,",
2697
+ " clientSecret: process.env.CLIENT_SECRET!,",
2698
+ " environment: 'sandbox',",
2699
+ "})",
2700
+ ...callLines.length > 0 ? ["", ...callLines] : []
2701
+ ];
2702
+ }
2703
+ renderReadmeExports(outputMode) {
2704
+ if (outputMode === "runtime") return ["`createClient()` for a typed runtime SDK instance", "`Schema` exports for request, response, params, query, and header types"];
2705
+ if (outputMode === "classes") return ["`Core` as the class-based SDK entrypoint", "generated API classes plus `Schema` type exports"];
2706
+ return [
2707
+ "`Core` for class-based usage",
2708
+ "`createClient()` for runtime-first usage",
2709
+ "`Schema` exports for generated request, response, params, query, and header types"
2710
+ ];
2711
+ }
2712
+ pickExampleOperation(manifest) {
2713
+ const group = manifest.groups[0];
2714
+ const operation = group?.operations[0];
2715
+ if (!group || !operation) return null;
2716
+ return {
2717
+ group,
2718
+ operation
2719
+ };
2720
+ }
2721
+ collectReadmeTypeImports(operation) {
2722
+ const types = /* @__PURE__ */ new Set();
2723
+ if (operation.pathParams.length > 0) types.add(operation.paramsType);
2724
+ if (operation.queryParams.length > 0) types.add(operation.queryType);
2725
+ if (operation.hasBody) types.add(operation.inputType);
2726
+ if (operation.headerParams.length > 0) types.add(operation.headerType);
2727
+ return Array.from(types).sort();
2728
+ }
2729
+ renderReadmeOperationCall(sdkVariable, exampleOperation, signatureStyle) {
2730
+ const { group, operation } = exampleOperation;
2731
+ const args = signatureStyle === "flat" ? this.renderReadmeFlatArgs(operation) : this.renderReadmeGroupedArgs(operation);
2732
+ return [
2733
+ `await ${sdkVariable}.api.${group.propertyName}.${operation.methodName}(`,
2734
+ ...args.map((arg) => ` ${arg},`),
2735
+ ")"
2736
+ ];
2737
+ }
2738
+ renderReadmeGroupedArgs(operation) {
2739
+ const args = [];
2740
+ if (operation.pathParams.length > 0) args.push(`{} as ${operation.paramsType}`);
2741
+ if (operation.queryParams.length > 0) args.push(`{} as ${operation.queryType}`);
2742
+ if (operation.hasBody) args.push(`{} as ${operation.inputType}`);
2743
+ if (operation.headerParams.length > 0) args.push(`{} as ${operation.headerType}`);
2744
+ return args;
2745
+ }
2746
+ renderReadmeFlatArgs(operation) {
2747
+ return [
2748
+ ...operation.pathParams.map((parameter) => `{} as ${operation.paramsType}[${JSON.stringify(parameter.name)}]`),
2749
+ ...operation.queryParams.map((parameter) => `{} as ${operation.queryType}[${JSON.stringify(parameter.name)}]`),
2750
+ ...operation.hasBody ? [`{} as ${operation.inputType}`] : [],
2751
+ ...operation.headerParams.map((parameter) => `{} as ${operation.headerType}[${JSON.stringify(parameter.name)}]`)
2752
+ ];
2753
+ }
2754
+ renderTsconfig() {
2755
+ return JSON.stringify({
2756
+ compilerOptions: {
2757
+ rootDir: ".",
2758
+ outDir: "./dist",
2759
+ target: "esnext",
2760
+ module: "es2022",
2761
+ moduleResolution: "bundler",
2762
+ esModuleInterop: true,
2763
+ strict: true,
2764
+ allowJs: true,
2765
+ skipLibCheck: true,
2766
+ resolveJsonModule: true
2767
+ },
2768
+ include: ["./src/**/*", "./tests/**/*"],
2769
+ exclude: ["./dist", "./node_modules"]
2770
+ }, null, 2);
2771
+ }
2772
+ renderTsdownConfig() {
2773
+ return [
2774
+ "import { defineConfig } from 'tsdown'",
2775
+ "",
2776
+ "export default defineConfig({",
2777
+ " entry: {",
2778
+ " index: 'src/index.ts',",
2779
+ " },",
2780
+ " exports: true,",
2781
+ " format: ['esm', 'cjs'],",
2782
+ " outDir: 'dist',",
2783
+ " dts: true,",
2784
+ " sourcemap: false,",
2785
+ " external: ['@oapiex/sdk-kit'],",
2786
+ " clean: true,",
2787
+ "})"
2788
+ ].join("\n");
2789
+ }
2790
+ renderVitestConfig() {
2791
+ return [
2792
+ "import { defineConfig } from 'vitest/config'",
2793
+ "",
2794
+ "export default defineConfig({",
2795
+ " test: {",
2796
+ " name: 'generated-sdk',",
2797
+ " environment: 'node',",
2798
+ " include: ['tests/*.{test,spec}.?(c|m)[jt]s?(x)'],",
2799
+ " },",
2800
+ "})"
2801
+ ].join("\n");
2802
+ }
2803
+ renderExportsTest(rootTypeName, outputMode) {
2804
+ const rootExportName = `${rootTypeName.charAt(0).toLowerCase()}${rootTypeName.slice(1)}`;
2805
+ const assertions = [
2806
+ " expect(sdk.createClient).toBeTypeOf('function')",
2807
+ " expect(sdk.createSdk).toBeTypeOf('function')",
2808
+ ` expect(sdk.${rootExportName}Sdk).toBeDefined()`,
2809
+ ` expect(sdk.${rootExportName}Manifest).toBeDefined()`
2810
+ ];
2811
+ if (outputMode !== "runtime") {
2812
+ assertions.unshift(" expect(sdk.Core).toBeTypeOf('function')");
2813
+ assertions.unshift(" expect(sdk.BaseApi).toBeTypeOf('function')");
2814
+ }
2815
+ return [
2816
+ "import { describe, expect, it } from 'vitest'",
2817
+ "",
2818
+ "import * as sdk from '../src/index'",
2819
+ "",
2820
+ "describe('generated sdk exports', () => {",
2821
+ " it('exposes the generated schema and runtime helpers', () => {",
2822
+ ...assertions,
2823
+ " })",
2824
+ "})"
2825
+ ].join("\n");
2826
+ }
2827
+ renderIndexFile(classNames, outputMode, rootTypeName) {
2828
+ const rootExportName = `${rootTypeName.charAt(0).toLowerCase()}${rootTypeName.slice(1)}`;
2829
+ const lines = [
2830
+ `import type { ${rootTypeName}Api } from './Schema'`,
2831
+ `import { ${rootExportName}Sdk } from './Schema'`,
2832
+ "import { createSdk as createBoundSdk } from '@oapiex/sdk-kit'",
2833
+ "import type { BaseApi as KitBaseApi, Core as KitCore, InitOptions } from '@oapiex/sdk-kit'",
2834
+ "",
2835
+ "export * from './Schema'"
2836
+ ];
2837
+ if (outputMode !== "runtime") {
2838
+ lines.push("export { ApiBinder } from './ApiBinder'");
2839
+ lines.push("export { BaseApi } from './BaseApi'");
2840
+ lines.push(...classNames.map((className) => `export { ${className} as ${className}Api } from './Apis/${className}'`));
2841
+ lines.push("export { Core } from './Core'");
2842
+ }
2843
+ lines.push("");
2844
+ lines.push("export const createClient = (");
2845
+ lines.push(" options: InitOptions");
2846
+ lines.push(`): KitCore & { api: KitBaseApi & ${rootTypeName}Api } =>`);
2847
+ lines.push(` createBoundSdk(${rootExportName}Sdk, options) as KitCore & { api: KitBaseApi & ${rootTypeName}Api }`);
2848
+ lines.push("");
2849
+ lines.push("export {");
2850
+ lines.push(" BadRequestException,");
2851
+ lines.push(" Builder,");
2852
+ lines.push(" ForbiddenRequestException,");
2853
+ lines.push(" Http,");
2854
+ lines.push(" HttpException,");
2855
+ lines.push(" UnauthorizedRequestException,");
2856
+ lines.push(" WebhookValidator,");
2857
+ lines.push(" createSdk,");
2858
+ lines.push("} from '@oapiex/sdk-kit'");
2859
+ lines.push("");
2860
+ lines.push("export type {");
2861
+ lines.push(" InitOptions,");
2862
+ lines.push(" UnifiedResponse,");
2863
+ lines.push(" XGenericObject,");
2864
+ lines.push("} from '@oapiex/sdk-kit'");
2865
+ return lines.join("\n");
2866
+ }
2867
+ collectTypeIdentifiers(typeRef) {
2868
+ return Array.from(new Set((typeRef.match(/\b[A-Z][A-Za-z0-9_]*/g) ?? []).filter((identifier) => {
2869
+ return !["Record", "Promise"].includes(identifier);
2870
+ })));
2871
+ }
2872
+ };
2873
+
2874
+ //#endregion
2875
+ //#region src/OpenApiTransform.ts
2876
+ var OpenApiTransformer = class {
2877
+ createDocument(operations, title = "Extracted API", version = "0.0.0") {
2878
+ const paths = {};
2879
+ for (const operation of operations) {
2880
+ const normalized = this.transformOperation(operation);
2881
+ if (!normalized || this.shouldSkipNormalizedOperation(normalized)) continue;
2882
+ paths[normalized.path] ??= {};
2883
+ paths[normalized.path][normalized.method] = normalized.operation;
2884
+ }
2885
+ return {
2886
+ openapi: "3.1.0",
2887
+ info: {
2888
+ title,
2889
+ version
2890
+ },
2891
+ paths
2892
+ };
2893
+ }
2894
+ transformOperation(operation) {
2895
+ if (!operation.method || !operation.url) return null;
2896
+ const url = new URL(operation.url);
2897
+ if (this.shouldSkipPlaceholderOperation(url, operation)) return null;
2898
+ const method = operation.method.toLowerCase();
2899
+ const path = this.decodeOpenApiPathname(url.pathname);
2900
+ return {
2901
+ path,
2902
+ method,
2903
+ operation: {
2904
+ summary: operation.sidebarLinks.find((link) => link.active)?.label,
2905
+ description: operation.description ?? void 0,
2906
+ operationId: this.buildOperationId(method, path),
2907
+ parameters: this.createParameters(operation.requestParams),
2908
+ requestBody: this.createRequestBody(operation.requestParams, operation.requestExampleNormalized?.body, this.hasExtractedBodyParams(operation.requestParams) ? null : this.resolveFallbackRequestBodyExample(operation)),
2909
+ responses: this.createResponses(operation.responseSchemas, operation.responseBodies)
2910
+ }
2911
+ };
2912
+ }
2913
+ shouldSkipNormalizedOperation(normalized) {
2914
+ return normalized.path === "/" && normalized.method === "get" && normalized.operation.operationId === "get" && Object.keys(normalized.operation.responses).length === 0;
2915
+ }
2916
+ shouldSkipPlaceholderOperation(url, operation) {
2917
+ if (url.hostname !== "example.com" || url.pathname !== "/") return false;
2918
+ return operation.requestParams.length === 0 && operation.responseSchemas.length === 0 && operation.responseBodies.length === 0 && operation.requestExampleNormalized?.url === "https://example.com/";
2919
+ }
2920
+ decodeOpenApiPathname(pathname) {
2921
+ return pathname.split("/").map((segment) => {
2922
+ if (!segment) return segment;
2923
+ try {
2924
+ return decodeURIComponent(segment);
2925
+ } catch {
2926
+ return segment;
2927
+ }
2928
+ }).join("/");
2929
+ }
2930
+ hasExtractedBodyParams(params) {
2931
+ return params.some((param) => param.in === "body" || param.in === null);
2932
+ }
2933
+ createParameters(params) {
2934
+ const parameters = params.filter((param) => this.isOpenApiParameterLocation(param.in)).map((param) => this.createParameter(param));
2935
+ return parameters.length > 0 ? parameters : void 0;
2936
+ }
2937
+ createRequestBody(params, example, fallbackExample) {
2938
+ const bodyParams = params.filter((param) => param.in === "body" || param.in === null);
2939
+ if (bodyParams.length === 0 && example == null) return;
2940
+ const schema = this.buildRequestBodySchema(bodyParams, example, fallbackExample);
2941
+ return {
2942
+ required: bodyParams.length > 0 ? bodyParams.some((param) => param.required) : false,
2943
+ content: { "application/json": {
2944
+ schema,
2945
+ ...example != null ? { example } : {}
2946
+ } }
2947
+ };
2948
+ }
2949
+ buildRequestBodySchema(params, example, fallbackExample) {
2950
+ const schema = this.mergeOpenApiSchemas(this.createExampleSchema(example), this.createExampleSchema(fallbackExample)) ?? { type: "object" };
2951
+ if (example != null) schema.example = example;
2952
+ else if (fallbackExample != null) schema.example = fallbackExample;
2953
+ for (const param of params) this.insertRequestBodyParam(schema, param);
2954
+ return schema;
2955
+ }
2956
+ inferSchemaFromExample(value) {
2957
+ if (Array.isArray(value)) return {
2958
+ type: "array",
2959
+ items: this.inferSchemaFromExample(value[0]) ?? {},
2960
+ example: value
2961
+ };
2962
+ if (isRecord(value)) return {
2963
+ type: "object",
2964
+ properties: Object.fromEntries(Object.entries(value).map(([key, entryValue]) => [key, this.inferSchemaFromExample(entryValue) ?? {}])),
2965
+ example: value
2966
+ };
2967
+ if (typeof value === "string") return {
2968
+ type: "string",
2969
+ example: value
2970
+ };
2971
+ if (typeof value === "number") return {
2972
+ type: Number.isInteger(value) ? "integer" : "number",
2973
+ example: value
2974
+ };
2975
+ if (typeof value === "boolean") return {
2976
+ type: "boolean",
2977
+ example: value
2978
+ };
2979
+ if (value === null) return {};
2980
+ }
2981
+ insertRequestBodyParam(rootSchema, param) {
2982
+ const path = param.path.length > 0 ? param.path : [param.name];
2983
+ let currentSchema = rootSchema;
2984
+ for (const [index, segment] of path.slice(0, -1).entries()) {
2985
+ currentSchema.properties ??= {};
2986
+ currentSchema.properties[segment] ??= { type: "object" };
2987
+ if (param.required) currentSchema.required = Array.from(new Set([...currentSchema.required ?? [], segment]));
2988
+ currentSchema = currentSchema.properties[segment];
2989
+ currentSchema.type ??= "object";
2990
+ if (index === path.length - 2 && param.required) currentSchema.required ??= [];
2991
+ }
2992
+ const leafKey = path[path.length - 1] ?? param.name;
2993
+ currentSchema.properties ??= {};
2994
+ currentSchema.properties[leafKey] = this.createParameterSchema(param);
2995
+ if (param.required) currentSchema.required = Array.from(new Set([...currentSchema.required ?? [], leafKey]));
2996
+ }
2997
+ createParameter(param) {
2998
+ return {
2999
+ name: param.name,
3000
+ in: param.in,
3001
+ required: param.in === "path" ? true : param.required,
3002
+ description: param.description ?? void 0,
3003
+ schema: this.createParameterSchema(param),
3004
+ example: param.defaultValue ?? void 0
3005
+ };
3006
+ }
3007
+ createParameterSchema(param) {
3008
+ return {
3009
+ type: param.type ?? void 0,
3010
+ description: param.description ?? void 0,
3011
+ default: param.defaultValue ?? void 0
3012
+ };
3013
+ }
3014
+ createResponses(schemas, responseBodies) {
3015
+ const responses = {};
3016
+ for (const schema of schemas) {
3017
+ if (!schema.statusCode) continue;
3018
+ const matchingBodies = responseBodies.filter((body) => body.statusCode === schema.statusCode);
3019
+ const content = this.createResponseContent(matchingBodies);
3020
+ responses[schema.statusCode] = {
3021
+ description: schema.description ?? schema.statusCode,
3022
+ ...content ? { content } : {}
3023
+ };
3024
+ }
3025
+ for (const body of responseBodies) {
3026
+ if (!body.statusCode || responses[body.statusCode]) continue;
3027
+ const content = this.createResponseContent([body]);
3028
+ responses[body.statusCode] = {
3029
+ description: body.label ?? body.statusCode,
3030
+ ...content ? { content } : {}
3031
+ };
3032
+ }
3033
+ return responses;
3034
+ }
3035
+ createResponseContent(bodies) {
3036
+ if (bodies.length === 0) return;
3037
+ const content = {};
3038
+ for (const body of bodies) {
3039
+ const contentType = body.contentType ?? (body.format === "json" ? "application/json" : "text/plain");
3040
+ const normalizedExample = this.normalizeResponseExampleValue(body.body, body.format);
3041
+ content[contentType] = {
3042
+ schema: this.inferSchemaFromBody(normalizedExample, body.format),
3043
+ example: normalizedExample
3044
+ };
3045
+ }
3046
+ return content;
3047
+ }
3048
+ inferSchemaFromBody(body, format) {
3049
+ if (format === "json") return this.inferSchemaFromExample(body);
3050
+ if (format === "text") return {
3051
+ type: "string",
3052
+ example: body
3053
+ };
3054
+ }
3055
+ normalizeResponseExampleValue(body, format) {
3056
+ if (format !== "json" || typeof body !== "string") return body;
3057
+ return JsonRepair.parsePossiblyTruncated(body) ?? parseLooseStructuredValue(body) ?? body;
3058
+ }
3059
+ resolveFallbackRequestBodyExample(operation) {
3060
+ const jsonResponseBody = operation.responseBodies.find((body) => body.format === "json")?.body;
3061
+ if (jsonResponseBody != null) return jsonResponseBody;
3062
+ if (typeof operation.responseExample === "object" && operation.responseExample !== null) return operation.responseExample;
3063
+ if (typeof operation.responseExampleRaw === "string") return JsonRepair.parsePossiblyTruncated(operation.responseExampleRaw);
3064
+ if (typeof operation.responseExample === "string") return JsonRepair.parsePossiblyTruncated(operation.responseExample);
3065
+ return null;
3066
+ }
3067
+ createExampleSchema(value) {
3068
+ if (value == null) return null;
3069
+ return this.inferSchemaFromExample(value) ?? null;
3070
+ }
3071
+ mergeOpenApiSchemas(left, right) {
3072
+ if (!left) return right;
3073
+ if (!right) return left;
3074
+ const merged = {
3075
+ ...right,
3076
+ ...left,
3077
+ ...left.type || right.type ? { type: left.type ?? right.type } : {},
3078
+ ...left.description || right.description ? { description: left.description ?? right.description } : {},
3079
+ ...left.default !== void 0 || right.default !== void 0 ? { default: left.default ?? right.default } : {},
3080
+ ...left.example !== void 0 || right.example !== void 0 ? { example: left.example ?? right.example } : {}
3081
+ };
3082
+ if (left.properties || right.properties) {
3083
+ const propertyKeys = new Set([...Object.keys(left.properties ?? {}), ...Object.keys(right.properties ?? {})]);
3084
+ merged.properties = Object.fromEntries(Array.from(propertyKeys).map((key) => [key, this.mergeOpenApiSchemas(left.properties?.[key] ?? null, right.properties?.[key] ?? null) ?? {}]));
3085
+ }
3086
+ if (left.items || right.items) merged.items = this.mergeOpenApiSchemas(left.items ?? null, right.items ?? null) ?? {};
3087
+ if (left.required || right.required) merged.required = Array.from(new Set([...right.required ?? [], ...left.required ?? []]));
3088
+ return merged;
3089
+ }
3090
+ buildOperationId(method, path) {
3091
+ return `${method}${path.replace(/\{([^}]+)\}/g, "$1").split("/").filter(Boolean).map((segment) => segment.replace(/[^a-zA-Z0-9]+/g, " ")).map((segment) => segment.trim()).filter(Boolean).map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1).replace(/\s+(.)/g, (_match, char) => char.toUpperCase())).join("")}`;
3092
+ }
3093
+ isOpenApiParameterLocation(value) {
3094
+ return value === "query" || value === "header" || value === "path" || value === "cookie";
3095
+ }
3096
+ };
3097
+ const transformer = new OpenApiTransformer();
3098
+
3099
+ //#endregion
3100
+ //#region src/Commands/GenerateCommand.ts
3101
+ var GenerateCommand = class extends _h3ravel_musket.Command {
3102
+ signature = `generate
3103
+ {artifact : Artifact to generate [sdk]}
3104
+ {source? : Documentation URL/local source or parsed TypeScript output file}
3105
+ {--d|dir? : Output directory for the generated artifact}
3106
+ {--n|name? : Package name for generated SDK packages}
3107
+ {--O|output-mode=both : SDK output mode [runtime,classes,both]}
3108
+ {--S|signature-style=grouped : SDK method signature style [flat,grouped]}
3109
+ {--N|namespace-strategy=smart : Namespace naming strategy [smart,scoped]}
3110
+ {--M|method-strategy=smart : Method naming strategy [smart,operation-id]}
3111
+ {--r|root-type-name=ExtractedApiDocument : Root type name for the generated Schema.ts module}
3112
+ {--B|browser? : Remote loader [axios,happy-dom,jsdom,puppeteer]}
3113
+ {--t|timeout? : Request/browser timeout in milliseconds}
3114
+ {--c|crawl : Crawl sidebar links and parse every discovered operation}
3115
+ {--b|base-url? : Base URL used to resolve sidebar links when crawling from a local file}
3116
+ `;
3117
+ description = "Generate artifacts such as SDK packages from documentation sources or parsed TypeScript outputs";
3118
+ async handle() {
3119
+ const conf = this.app.getConfig();
3120
+ const artifact = String(this.argument("artifact", "")).trim().toLowerCase();
3121
+ const source = String(this.argument("source", "")).trim();
3122
+ const browser = String(this.option("browser", conf.browser)).trim().toLowerCase();
3123
+ const timeoutOption = String(this.option("timeout", "")).trim();
3124
+ const crawl = this.option("crawl");
3125
+ const baseUrl = String(this.option("baseUrl", "")).trim() || null;
3126
+ const packageDir = await this.resolveOutputDirectory(source);
3127
+ const spinner = this.spinner(`Generating ${artifact} artifact...`).start();
3128
+ let startedBrowserSession = false;
3129
+ try {
3130
+ const start = Date.now();
3131
+ if (!isSupportedBrowser(browser)) throw new Error(`Unsupported browser: ${browser}`);
3132
+ if (artifact !== "sdk") throw new Error(`Unsupported artifact: ${artifact}`);
3133
+ if (!source) throw new Error("The sdk artifact requires a source argument");
3134
+ if (!this.isTypeScriptArtifactSource(source) && !isSupportedBrowser(browser)) throw new Error(`Unsupported browser: ${browser}`);
3135
+ const requestTimeout = this.resolveTimeoutOverride(timeoutOption, conf.requestTimeout);
3136
+ const namespaceStrategy = this.parseNamespaceStrategy(this.option("namespaceStrategy", "smart"));
3137
+ const methodStrategy = this.parseMethodStrategy(this.option("methodStrategy", "smart"));
3138
+ const outputMode = this.parseOutputMode(this.option("outputMode", "both"));
3139
+ const signatureStyle = this.parseSignatureStyle(this.option("signatureStyle", "grouped"));
3140
+ const rootTypeName = String(this.option("rootTypeName", "ExtractedApiDocument")).trim() || "ExtractedApiDocument";
3141
+ this.app.configure({
3142
+ browser,
3143
+ requestTimeout
3144
+ });
3145
+ if (!this.isTypeScriptArtifactSource(source) && crawl) {
3146
+ await startBrowserSession(this.app.getConfig());
3147
+ startedBrowserSession = true;
3148
+ }
3149
+ const sdkSource = await this.resolveSdkSource({
3150
+ source,
3151
+ crawl,
3152
+ baseUrl,
3153
+ rootTypeName,
3154
+ namespaceStrategy,
3155
+ methodStrategy
3156
+ });
3157
+ const packageName = this.resolvePackageName(packageDir);
3158
+ const files = new SdkPackageGenerator().generate(sdkSource.document, {
3159
+ outputMode,
3160
+ signatureStyle,
3161
+ rootTypeName,
3162
+ namespaceStrategy,
3163
+ methodStrategy,
3164
+ schemaModule: sdkSource.schemaModule,
3165
+ packageName: String(this.option("name", "")).trim() || packageName
3166
+ });
3167
+ await this.writePackageFiles(packageDir, files);
3168
+ const duration = Date.now() - start;
3169
+ _h3ravel_shared.Logger.twoColumnDetail(_h3ravel_shared.Logger.log([["Generated", "green"], [`${duration / 1e3}s`, "gray"]], " ", false), packageDir.replace(process.cwd(), "."));
3170
+ spinner.succeed("Artifact generation completed");
3171
+ } catch (error) {
3172
+ const message = error instanceof Error ? error.message : "Unknown error";
3173
+ spinner.fail(`Failed to generate artifact: ${message}`);
3174
+ process.exitCode = 1;
3175
+ } finally {
3176
+ if (startedBrowserSession) await endBrowserSession();
3177
+ }
3178
+ }
3179
+ /**
3180
+ * Resolves the SDK source by either loading a pre-generated TypeScript artifact or
3181
+ * crawling/parsing HTML documentation based on the provided options and source string.
3182
+ *
3183
+ * @param options The options for resolving the SDK source.
3184
+ * @returns An object containing the OpenAPI document and the generated schema module as a string.
3185
+ */
3186
+ async resolveSdkSource(options) {
3187
+ if (this.isTypeScriptArtifactSource(options.source)) return this.loadSdkSourceFromTypeScriptArtifact(options.source);
3188
+ const operation = extractReadmeOperationFromHtml(await this.app.loadHtmlSource(options.source, true));
3189
+ const payload = options.crawl ? await this.app.crawlReadmeOperations(options.source, operation, options.baseUrl) : operation;
3190
+ const document = this.buildOpenApiPayload(payload);
3191
+ return {
3192
+ document,
3193
+ schemaModule: await prettier.default.format(TypeScriptGenerator.generateModule(document, options.rootTypeName, {
3194
+ namespaceStrategy: options.namespaceStrategy,
3195
+ methodStrategy: options.methodStrategy
3196
+ }), {
3197
+ parser: "typescript",
3198
+ semi: false,
3199
+ singleQuote: true
3200
+ })
3201
+ };
3202
+ }
3203
+ /**
3204
+ * Loads the SDK source from a pre-generated TypeScript artifact.
3205
+ *
3206
+ * @param source
3207
+ * @returns
3208
+ */
3209
+ async loadSdkSourceFromTypeScriptArtifact(source) {
3210
+ const filePath = node_path.default.resolve(process.cwd(), source);
3211
+ const schemaModule = await node_fs_promises.default.readFile(filePath, "utf8");
3212
+ const importedModule = await import(`${(0, node_url.pathToFileURL)(filePath).href}?t=${Date.now()}`);
3213
+ const documentCandidate = importedModule.default ?? Object.values(importedModule).find((value) => this.isOpenApiDocumentLike(value));
3214
+ if (!this.isOpenApiDocumentLike(documentCandidate)) throw new Error("The provided TypeScript source does not export an OpenAPI document");
3215
+ return {
3216
+ document: documentCandidate,
3217
+ schemaModule
3218
+ };
3219
+ }
3220
+ /**
3221
+ * Builds an OpenAPI document from the extracted operations.
3222
+ *
3223
+ * @param payload
3224
+ * @returns
3225
+ */
3226
+ buildOpenApiPayload(payload) {
3227
+ if ("operations" in payload) return transformer.createDocument(payload.operations, "Extracted API", "0.0.0");
3228
+ return transformer.createDocument([payload], "Extracted API", "0.0.0");
3229
+ }
3230
+ resolveTimeoutOverride(value, fallback) {
3231
+ if (!value) return fallback;
3232
+ const parsed = Number(value);
3233
+ if (!Number.isFinite(parsed) || parsed <= 0) throw new Error(`Invalid timeout override: ${value}`);
3234
+ return parsed;
3235
+ }
3236
+ /**
3237
+ * Resolves the output directory for the generated SDK package.
3238
+ *
3239
+ * @param source The source string used to determine the output directory.
3240
+ * @returns The resolved output directory path.
3241
+ */
3242
+ async resolveOutputDirectory(source, explicitDir) {
3243
+ explicitDir ??= String(this.option("dir", "")).trim();
3244
+ const dir = explicitDir ? node_path.default.resolve(process.cwd(), explicitDir) : OutputGenerator.buildArtifactDirectory(process.cwd(), source, "sdk");
3245
+ if ((0, node_fs.existsSync)(dir)) {
3246
+ if ((0, node_fs.readdirSync)(dir).length > 0) switch (await this.choice(`Output directory (${explicitDir}) already exists and is not empty, what would you like to do?`, [
3247
+ {
3248
+ name: "Overwrite",
3249
+ value: "overwrite"
3250
+ },
3251
+ {
3252
+ name: "Try to merge",
3253
+ value: "merge"
3254
+ },
3255
+ {
3256
+ name: "Choose a different directory",
3257
+ value: "choose"
3258
+ },
3259
+ {
3260
+ name: "Cancel",
3261
+ value: "cancel"
3262
+ }
3263
+ ])) {
3264
+ case "overwrite":
3265
+ await node_fs_promises.default.rm(dir, {
3266
+ recursive: true,
3267
+ force: true
3268
+ });
3269
+ break;
3270
+ case "choose": {
3271
+ const newDir = await this.ask("Please enter a new output directory (relative to current directory):", explicitDir);
3272
+ return this.resolveOutputDirectory(source, newDir);
3273
+ }
3274
+ case "cancel":
3275
+ this.info("Operation cancelled by user");
3276
+ return process.exit(0);
3277
+ default: break;
3278
+ }
3279
+ }
3280
+ return dir;
3281
+ }
3282
+ resolvePackageName(packageDir) {
3283
+ const explicitName = String(this.option("name", "")).trim();
3284
+ if (explicitName) return explicitName;
3285
+ return node_path.default.basename(packageDir).replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "generated-sdk";
3286
+ }
3287
+ parseOutputMode(value) {
3288
+ const normalized = String(value ?? "both").trim().toLowerCase();
3289
+ if (normalized === "runtime" || normalized === "classes" || normalized === "both") return normalized;
3290
+ throw new Error(`Unsupported sdk output mode: ${normalized}`);
3291
+ }
3292
+ parseSignatureStyle(value) {
3293
+ const normalized = String(value ?? "grouped").trim().toLowerCase();
3294
+ if (normalized === "flat" || normalized === "grouped") return normalized;
3295
+ throw new Error(`Unsupported signature style: ${normalized}`);
3296
+ }
3297
+ parseNamespaceStrategy(value) {
3298
+ const normalized = String(value ?? "smart").trim().toLowerCase();
3299
+ if (normalized === "smart" || normalized === "scoped") return normalized;
3300
+ throw new Error(`Unsupported namespace strategy: ${normalized}`);
3301
+ }
3302
+ parseMethodStrategy(value) {
3303
+ const normalized = String(value ?? "smart").trim().toLowerCase();
3304
+ if (normalized === "smart" || normalized === "operation-id") return normalized;
3305
+ throw new Error(`Unsupported method strategy: ${normalized}`);
3306
+ }
3307
+ /**
3308
+ * Checks if the provided source string points to a TypeScript or JavaScript file, which is
3309
+ * used to determine if the source should be loaded as a pre-generated artifact instead
3310
+ * of crawling/parsing HTML documentation.
3311
+ *
3312
+ * @param source The source string to check.
3313
+ * @returns True if the source is a TypeScript or JavaScript file, false otherwise.
3314
+ */
3315
+ isTypeScriptArtifactSource(source) {
3316
+ return /\.(?:[cm]?ts|[cm]?js)$/i.test(source);
3317
+ }
3318
+ /**
3319
+ * Type guard to check if a value conforms to the OpenApiDocumentLike interface.
3320
+ *
3321
+ * @param value
3322
+ * @returns
3323
+ */
3324
+ isOpenApiDocumentLike(value) {
3325
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
3326
+ const candidate = value;
3327
+ const info = candidate.info;
3328
+ return candidate.openapi === "3.1.0" && typeof info === "object" && info !== null && !Array.isArray(info) && typeof info.title === "string" && typeof info.version === "string" && typeof candidate.paths === "object" && candidate.paths !== null && !Array.isArray(candidate.paths);
3329
+ }
3330
+ /**
3331
+ * Writes the generated files to the output directory, creating any necessary subdirectories.
3332
+ *
3333
+ * @param packageDir
3334
+ * @param files
3335
+ */
3336
+ async writePackageFiles(packageDir, files) {
3337
+ await Promise.all(Object.entries(files).map(async ([relativePath, content]) => {
3338
+ const filePath = node_path.default.join(packageDir, relativePath);
3339
+ await node_fs_promises.default.mkdir(node_path.default.dirname(filePath), { recursive: true });
3340
+ await node_fs_promises.default.writeFile(filePath, content, "utf8");
3341
+ }));
3342
+ }
3343
+ };
3344
+
3345
+ //#endregion
3346
+ //#region src/Commands/InitCommand.ts
3347
+ const __filename$1 = (0, url.fileURLToPath)(require("url").pathToFileURL(__filename).href);
3348
+ var InitCommand = class extends _h3ravel_musket.Command {
3349
+ signature = `init
3350
+ {--f|force : Overwrite existing config}
3351
+ {--p|pkg? : Generate config for another package (e.g. sdk-kit) instead of oapiex [sdk]}
3352
+ `;
3353
+ description = "Generate a default oapiex.config.ts in the current directory";
3354
+ async handle() {
3355
+ const cwd = process.cwd();
3356
+ const configPath = node_path.default.join(cwd, "oapiex.config.js");
3357
+ const force = this.option("force", false);
3358
+ const pkg = this.option("pkg", "base").trim().toLowerCase();
3359
+ const configTemplate = {
3360
+ base: this.buildConfigTemplate(),
3361
+ sdk: this.buildSdkConfigTemplate()
3362
+ };
3363
+ if (!["base", "sdk"].includes(pkg)) return void this.error(`Invalid package option: ${pkg}`);
3364
+ try {
3365
+ await node_fs_promises.default.access(configPath);
3366
+ if (!force) {
3367
+ this.error(`Config file already exists at ${configPath}. Use --force to overwrite.`);
3368
+ process.exit(1);
3369
+ }
3370
+ } catch {}
3371
+ await node_fs_promises.default.writeFile(configPath, configTemplate[pkg], "utf8");
3372
+ this.line(`Created ${configPath} `);
3373
+ }
3374
+ buildConfigTemplate() {
3375
+ const def = defaultConfig;
3376
+ return [
3377
+ `import { defineConfig } from '${__filename$1.includes("node_modules") ? "oapiex" : "./src/Manager"}'`,
3378
+ "",
3379
+ "/**",
3380
+ " * See https://toneflix.github.io/oapiex/configuration for docs",
3381
+ " */",
3382
+ "export default defineConfig({",
3383
+ ` outputFormat: '${def.outputFormat}',`,
3384
+ ` outputShape: '${def.outputShape}',`,
3385
+ ` browser: '${def.browser}',`,
3386
+ ` requestTimeout: ${def.requestTimeout},`,
3387
+ ` maxRedirects: ${def.maxRedirects},`,
3388
+ ` userAgent: '${def.userAgent}',`,
3389
+ ` retryCount: ${def.retryCount},`,
3390
+ ` retryDelay: ${def.retryDelay},`,
3391
+ "})"
3392
+ ].join("\n");
3393
+ }
3394
+ buildSdkConfigTemplate() {
3395
+ return [
3396
+ "import { defineConfig } from '@oapiex/sdk-kit'",
3397
+ "",
3398
+ "/**",
3399
+ " * See https://toneflix.github.io/oapiex/configuration for docs",
3400
+ " */",
3401
+ "export default defineConfig({",
3402
+ " environment: 'sandbox',",
3403
+ " urls: {",
3404
+ " live: 'https://live.oapiex.com',",
3405
+ " sandbox: 'https://sandbox.oapiex.com',",
3406
+ " },",
3407
+ "})"
3408
+ ].join("\n");
3409
+ }
1226
3410
  };
1227
3411
 
1228
3412
  //#endregion
@@ -1230,9 +3414,10 @@ const isOpenApiParameterLocation = (value) => {
1230
3414
  var ParseCommand = class extends _h3ravel_musket.Command {
1231
3415
  signature = `parse
1232
3416
  {source : Local HTML file path or remote URL}
1233
- {--O|output=pretty : Output format [pretty,json,js]}
3417
+ {--O|output=pretty : Output format [pretty,json,js,ts]}
1234
3418
  {--S|shape=raw : Result shape [raw,openapi]}
1235
3419
  {--B|browser? : Remote loader [axios,happy-dom,jsdom,puppeteer]}
3420
+ {--t|timeout? : Request/browser timeout in milliseconds}
1236
3421
  {--c|crawl : Crawl sidebar links and parse every discovered operation}
1237
3422
  {--b|base-url? : Base URL used to resolve sidebar links when crawling from a local file}
1238
3423
  `;
@@ -1243,6 +3428,7 @@ var ParseCommand = class extends _h3ravel_musket.Command {
1243
3428
  const output = String(this.option("output", conf.outputFormat)).trim().toLowerCase();
1244
3429
  const shape = String(this.option("shape", conf.outputShape)).trim().toLowerCase();
1245
3430
  const browser = String(this.option("browser", conf.browser)).trim().toLowerCase();
3431
+ const timeoutOption = String(this.option("timeout", "")).trim();
1246
3432
  const crawl = this.option("crawl");
1247
3433
  const baseUrl = String(this.option("baseUrl", "")).trim() || null;
1248
3434
  const spinner = this.spinner(`${crawl ? "Crawling and p" : "P"}arsing source...`).start();
@@ -1250,7 +3436,11 @@ var ParseCommand = class extends _h3ravel_musket.Command {
1250
3436
  try {
1251
3437
  const start = Date.now();
1252
3438
  if (!isSupportedBrowser(browser)) throw new Error(`Unsupported browser: ${browser}`);
1253
- this.app.configure({ browser });
3439
+ const requestTimeout = this.resolveTimeoutOverride(timeoutOption, conf.requestTimeout);
3440
+ this.app.configure({
3441
+ browser,
3442
+ requestTimeout
3443
+ });
1254
3444
  if (crawl) {
1255
3445
  await startBrowserSession(this.app.getConfig());
1256
3446
  startedBrowserSession = true;
@@ -1258,7 +3448,7 @@ var ParseCommand = class extends _h3ravel_musket.Command {
1258
3448
  const operation = extractReadmeOperationFromHtml(await this.app.loadHtmlSource(source, true));
1259
3449
  const payload = crawl ? await this.app.crawlReadmeOperations(source, operation, baseUrl) : operation;
1260
3450
  const normalizedPayload = shape === "openapi" ? this.buildOpenApiPayload(payload) : payload;
1261
- const serialized = output === "js" ? `export default ${JSON.stringify(normalizedPayload, null, 2)}` : JSON.stringify(normalizedPayload, null, output === "json" ? 0 : 2);
3451
+ const serialized = await OutputGenerator.serializeOutput(normalizedPayload, output, OutputGenerator.getRootTypeName(shape));
1262
3452
  const filePath = await this.saveOutputToFile(serialized, source, shape, output);
1263
3453
  const duration = Date.now() - start;
1264
3454
  _h3ravel_shared.Logger.twoColumnDetail(_h3ravel_shared.Logger.log([["Output", "green"], [`${duration / 1e3}s`, "gray"]], " ", false), filePath.replace(process.cwd(), "."));
@@ -1271,27 +3461,22 @@ var ParseCommand = class extends _h3ravel_musket.Command {
1271
3461
  if (startedBrowserSession) await endBrowserSession();
1272
3462
  }
1273
3463
  }
3464
+ resolveTimeoutOverride = (value, fallback) => {
3465
+ if (!value) return fallback;
3466
+ const parsed = Number(value);
3467
+ if (!Number.isFinite(parsed) || parsed <= 0) throw new Error(`Invalid timeout override: ${value}`);
3468
+ return parsed;
3469
+ };
1274
3470
  buildOpenApiPayload = (payload) => {
1275
- if ("operations" in payload) return createOpenApiDocumentFromReadmeOperations(payload.operations, "Extracted API", "0.0.0");
1276
- return createOpenApiDocumentFromReadmeOperations([payload], "Extracted API", "0.0.0");
3471
+ if ("operations" in payload) return transformer.createDocument(payload.operations, "Extracted API", "0.0.0");
3472
+ return transformer.createDocument([payload], "Extracted API", "0.0.0");
1277
3473
  };
1278
3474
  saveOutputToFile = async (content, source, shape, outputFormat) => {
1279
- const ext = {
1280
- pretty: "txt",
1281
- json: "json",
1282
- js: "js"
1283
- }[outputFormat];
1284
- const outputDir = node_path.default.resolve(process.cwd(), "output");
1285
- await node_fs_promises.default.mkdir(outputDir, { recursive: true });
1286
- const filename = `${source.replace(/[^a-zA-Z0-9_-]+/g, "_").replace(/^_+|_+$/g, "") || "output"}${shape === "openapi" ? ".openapi" : ""}.${ext}`;
1287
- const filePath = node_path.default.join(outputDir, filename);
1288
- if (outputFormat === "js") content = await prettier.default.format(content, {
1289
- parser: "babel",
1290
- semi: false,
1291
- singleQuote: true
1292
- });
1293
- await node_fs_promises.default.writeFile(filePath, content, "utf8");
1294
- return filePath;
3475
+ const outputDir = OutputGenerator.buildFilePath(process.cwd(), source, shape, outputFormat);
3476
+ const outputDirname = node_path.default.dirname(outputDir);
3477
+ await node_fs_promises.default.mkdir(outputDirname, { recursive: true });
3478
+ await node_fs_promises.default.writeFile(outputDir, content, "utf8");
3479
+ return outputDir;
1295
3480
  };
1296
3481
  };
1297
3482
 
@@ -1332,21 +3517,16 @@ async function resolveConfig(cliOverrides = {}) {
1332
3517
 
1333
3518
  //#endregion
1334
3519
  exports.Application = Application;
3520
+ exports.GenerateCommand = GenerateCommand;
1335
3521
  exports.InitCommand = InitCommand;
3522
+ exports.JsonRepair = JsonRepair;
3523
+ exports.OpenApiTransformer = OpenApiTransformer;
3524
+ exports.OutputGenerator = OutputGenerator;
1336
3525
  exports.ParseCommand = ParseCommand;
3526
+ exports.SdkPackageGenerator = SdkPackageGenerator;
3527
+ exports.TypeScriptGenerator = TypeScriptGenerator;
1337
3528
  exports.browser = browser;
1338
- exports.buildOperationId = buildOperationId;
1339
3529
  exports.buildOperationUrl = buildOperationUrl;
1340
- exports.buildRequestBodySchema = buildRequestBodySchema;
1341
- exports.createExampleSchema = createExampleSchema;
1342
- exports.createOpenApiDocumentFromReadmeOperations = createOpenApiDocumentFromReadmeOperations;
1343
- exports.createParameter = createParameter;
1344
- exports.createParameterSchema = createParameterSchema;
1345
- exports.createParameters = createParameters;
1346
- exports.createRequestBody = createRequestBody;
1347
- exports.createResponseContent = createResponseContent;
1348
- exports.createResponses = createResponses;
1349
- exports.decodeOpenApiPathname = decodeOpenApiPathname;
1350
3530
  exports.defaultConfig = defaultConfig;
1351
3531
  exports.defineConfig = defineConfig;
1352
3532
  exports.endBrowserSession = endBrowserSession;
@@ -1375,6 +3555,7 @@ exports.extractResponseSchemas = extractResponseSchemas;
1375
3555
  exports.extractResponseSchemasFromOpenApi = extractResponseSchemasFromOpenApi;
1376
3556
  exports.extractSidebarLinkLabel = extractSidebarLinkLabel;
1377
3557
  exports.extractSidebarLinks = extractSidebarLinks;
3558
+ exports.extractStablePageHtml = extractStablePageHtml;
1378
3559
  exports.extractStringLiteralValue = extractStringLiteralValue;
1379
3560
  exports.findParameterRoot = findParameterRoot;
1380
3561
  exports.flattenOpenApiSchemaProperties = flattenOpenApiSchemaProperties;
@@ -1385,20 +3566,14 @@ Object.defineProperty(exports, 'globalConfig', {
1385
3566
  return globalConfig;
1386
3567
  }
1387
3568
  });
1388
- exports.hasExtractedBodyParams = hasExtractedBodyParams;
1389
3569
  exports.inferParameterLocation = inferParameterLocation;
1390
3570
  exports.inferParameterLocationFromText = inferParameterLocationFromText;
1391
3571
  exports.inferParameterPath = inferParameterPath;
1392
3572
  exports.inferParameterType = inferParameterType;
1393
- exports.inferSchemaFromBody = inferSchemaFromBody;
1394
- exports.inferSchemaFromExample = inferSchemaFromExample;
1395
- exports.insertRequestBodyParam = insertRequestBodyParam;
1396
- exports.isOpenApiParameterLocation = isOpenApiParameterLocation;
1397
3573
  exports.isRecord = isRecord;
1398
3574
  exports.isRequiredParameter = isRequiredParameter;
1399
3575
  exports.isSupportedBrowser = isSupportedBrowser;
1400
3576
  exports.loadUserConfig = loadUserConfig;
1401
- exports.mergeOpenApiSchemas = mergeOpenApiSchemas;
1402
3577
  exports.mergeReadmeOperations = mergeReadmeOperations;
1403
3578
  exports.mergeSsrPropsIntoRenderedHtml = mergeSsrPropsIntoRenderedHtml;
1404
3579
  exports.normalizeCurlSnippet = normalizeCurlSnippet;
@@ -1407,18 +3582,14 @@ exports.normalizeRequestCodeSnippet = normalizeRequestCodeSnippet;
1407
3582
  exports.normalizeResponseBody = normalizeResponseBody;
1408
3583
  exports.normalizeStructuredRequestBody = normalizeStructuredRequestBody;
1409
3584
  exports.parseLooseStructuredValue = parseLooseStructuredValue;
1410
- exports.parsePossiblyTruncatedJson = parsePossiblyTruncatedJson;
1411
3585
  exports.readInputValue = readInputValue;
1412
3586
  exports.readText = readText;
1413
3587
  exports.readTexts = readTexts;
1414
3588
  exports.resolveConfig = resolveConfig;
1415
- exports.resolveFallbackRequestBodyExample = resolveFallbackRequestBodyExample;
1416
3589
  exports.resolveOpenApiMediaExample = resolveOpenApiMediaExample;
1417
3590
  exports.resolveParameterInput = resolveParameterInput;
1418
3591
  exports.resolveReadmeSidebarUrls = resolveReadmeSidebarUrls;
1419
3592
  exports.resolveSsrOperation = resolveSsrOperation;
1420
- exports.shouldSkipNormalizedOperation = shouldSkipNormalizedOperation;
1421
- exports.shouldSkipPlaceholderOperation = shouldSkipPlaceholderOperation;
1422
3593
  exports.startBrowserSession = startBrowserSession;
1423
3594
  exports.supportedBrowsers = supportedBrowsers;
1424
- exports.transformReadmeOperationToOpenApi = transformReadmeOperationToOpenApi;
3595
+ exports.transformer = transformer;