oapiex 0.1.2 → 0.2.2

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