typedapi-client-helpers 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/LICENSE +21 -0
  3. package/README.md +221 -220
  4. package/dist/cjs/httpClient.js +258 -0
  5. package/dist/cjs/httpClient.js.map +1 -0
  6. package/dist/cjs/index.js +32 -0
  7. package/dist/cjs/index.js.map +1 -0
  8. package/dist/cjs/interfaces/ApiResult.js +3 -0
  9. package/dist/cjs/interfaces/ApiResult.js.map +1 -0
  10. package/dist/cjs/interfaces/Filter.js +3 -0
  11. package/dist/cjs/interfaces/Filter.js.map +1 -0
  12. package/dist/cjs/interfaces/SortType.js +3 -0
  13. package/dist/cjs/interfaces/SortType.js.map +1 -0
  14. package/dist/cjs/package.json +3 -0
  15. package/dist/cjs/types/ApiCallOptions.js +3 -0
  16. package/dist/cjs/types/ApiCallOptions.js.map +1 -0
  17. package/dist/cjs/types/ApiMethodArguments.js +3 -0
  18. package/dist/cjs/types/ApiMethodArguments.js.map +1 -0
  19. package/dist/cjs/types/ApiMethodOptions.js +3 -0
  20. package/dist/cjs/types/ApiMethodOptions.js.map +1 -0
  21. package/dist/cjs/types/ApiMethodTypes.js +3 -0
  22. package/dist/cjs/types/ApiMethodTypes.js.map +1 -0
  23. package/dist/cjs/types/HttpResponse.js +3 -0
  24. package/dist/cjs/types/HttpResponse.js.map +1 -0
  25. package/dist/cjs/types/httpClientTypes.js +3 -0
  26. package/dist/cjs/types/httpClientTypes.js.map +1 -0
  27. package/dist/cjs/utils/SortTypeFromSortDirection.js +43 -0
  28. package/dist/cjs/utils/SortTypeFromSortDirection.js.map +1 -0
  29. package/dist/cjs/utils/buildQuery.js +96 -0
  30. package/dist/cjs/utils/buildQuery.js.map +1 -0
  31. package/dist/cjs/utils/extractArgsCallbacksAndParams.js +51 -0
  32. package/dist/cjs/utils/extractArgsCallbacksAndParams.js.map +1 -0
  33. package/dist/cjs/utils/extractArgsToastsAndParams.js +49 -0
  34. package/dist/cjs/utils/extractArgsToastsAndParams.js.map +1 -0
  35. package/dist/cjs/utils/handleApiResponse.js +124 -0
  36. package/dist/cjs/utils/handleApiResponse.js.map +1 -0
  37. package/dist/cjs/utils/toFormData.js +58 -0
  38. package/dist/cjs/utils/toFormData.js.map +1 -0
  39. package/dist/cjs/utils/wireMapping.js +123 -0
  40. package/dist/cjs/utils/wireMapping.js.map +1 -0
  41. package/dist/esm/httpClient.js +251 -0
  42. package/dist/esm/httpClient.js.map +1 -0
  43. package/dist/esm/index.js +16 -0
  44. package/dist/esm/index.js.map +1 -0
  45. package/dist/esm/interfaces/ApiResult.js +2 -0
  46. package/dist/esm/interfaces/ApiResult.js.map +1 -0
  47. package/dist/esm/interfaces/Filter.js +2 -0
  48. package/dist/esm/interfaces/Filter.js.map +1 -0
  49. package/dist/esm/interfaces/SortType.js +2 -0
  50. package/dist/esm/interfaces/SortType.js.map +1 -0
  51. package/dist/esm/types/ApiCallOptions.js +2 -0
  52. package/dist/esm/types/ApiCallOptions.js.map +1 -0
  53. package/dist/esm/types/ApiMethodArguments.js +2 -0
  54. package/dist/esm/types/ApiMethodArguments.js.map +1 -0
  55. package/dist/esm/types/ApiMethodOptions.js +2 -0
  56. package/dist/esm/types/ApiMethodOptions.js.map +1 -0
  57. package/dist/esm/types/ApiMethodTypes.js +2 -0
  58. package/dist/esm/types/ApiMethodTypes.js.map +1 -0
  59. package/dist/esm/types/HttpResponse.js +2 -0
  60. package/dist/esm/types/HttpResponse.js.map +1 -0
  61. package/dist/esm/types/httpClientTypes.js +2 -0
  62. package/dist/esm/types/httpClientTypes.js.map +1 -0
  63. package/dist/esm/utils/SortTypeFromSortDirection.js +38 -0
  64. package/dist/esm/utils/SortTypeFromSortDirection.js.map +1 -0
  65. package/dist/esm/utils/buildQuery.js +93 -0
  66. package/dist/esm/utils/buildQuery.js.map +1 -0
  67. package/dist/esm/utils/extractArgsCallbacksAndParams.js +48 -0
  68. package/dist/esm/utils/extractArgsCallbacksAndParams.js.map +1 -0
  69. package/dist/esm/utils/extractArgsToastsAndParams.js +46 -0
  70. package/dist/esm/utils/extractArgsToastsAndParams.js.map +1 -0
  71. package/dist/esm/utils/handleApiResponse.js +121 -0
  72. package/dist/esm/utils/handleApiResponse.js.map +1 -0
  73. package/dist/esm/utils/toFormData.js +55 -0
  74. package/dist/esm/utils/toFormData.js.map +1 -0
  75. package/dist/esm/utils/wireMapping.js +119 -0
  76. package/dist/esm/utils/wireMapping.js.map +1 -0
  77. package/dist/types/httpClient.d.ts +63 -0
  78. package/dist/types/httpClient.d.ts.map +1 -0
  79. package/dist/types/index.d.ts +16 -0
  80. package/dist/types/index.d.ts.map +1 -0
  81. package/dist/types/interfaces/ApiResult.d.ts +32 -0
  82. package/dist/types/interfaces/ApiResult.d.ts.map +1 -0
  83. package/dist/types/interfaces/Filter.d.ts +31 -0
  84. package/dist/types/interfaces/Filter.d.ts.map +1 -0
  85. package/dist/types/interfaces/SortType.d.ts +7 -0
  86. package/dist/types/interfaces/SortType.d.ts.map +1 -0
  87. package/dist/types/types/ApiCallOptions.d.ts +26 -0
  88. package/dist/types/types/ApiCallOptions.d.ts.map +1 -0
  89. package/dist/types/types/ApiMethodArguments.d.ts +6 -0
  90. package/dist/types/types/ApiMethodArguments.d.ts.map +1 -0
  91. package/dist/types/types/ApiMethodOptions.d.ts +8 -0
  92. package/dist/types/types/ApiMethodOptions.d.ts.map +1 -0
  93. package/dist/types/types/ApiMethodTypes.d.ts +47 -0
  94. package/dist/types/types/ApiMethodTypes.d.ts.map +1 -0
  95. package/dist/types/types/HttpResponse.d.ts +2 -0
  96. package/dist/types/types/HttpResponse.d.ts.map +1 -0
  97. package/dist/types/types/httpClientTypes.d.ts +2 -0
  98. package/dist/types/types/httpClientTypes.d.ts.map +1 -0
  99. package/dist/types/utils/SortTypeFromSortDirection.d.ts +13 -0
  100. package/dist/types/utils/SortTypeFromSortDirection.d.ts.map +1 -0
  101. package/dist/types/utils/buildQuery.d.ts +16 -0
  102. package/dist/types/utils/buildQuery.d.ts.map +1 -0
  103. package/dist/types/utils/extractArgsCallbacksAndParams.d.ts +22 -0
  104. package/dist/types/utils/extractArgsCallbacksAndParams.d.ts.map +1 -0
  105. package/dist/types/utils/extractArgsToastsAndParams.d.ts +20 -0
  106. package/dist/types/utils/extractArgsToastsAndParams.d.ts.map +1 -0
  107. package/dist/types/utils/handleApiResponse.d.ts +11 -0
  108. package/dist/types/utils/handleApiResponse.d.ts.map +1 -0
  109. package/dist/types/utils/toFormData.d.ts +10 -0
  110. package/dist/types/utils/toFormData.d.ts.map +1 -0
  111. package/dist/types/utils/wireMapping.d.ts +30 -0
  112. package/dist/types/utils/wireMapping.d.ts.map +1 -0
  113. package/package.json +90 -82
  114. package/scripts/build.mjs +74 -0
  115. package/scripts/generate-api-minimal.mjs +568 -476
  116. package/scripts/openapi-generator/controller-template.mjs +1 -1
  117. package/scripts/openapi-generator/index.mjs +357 -113
  118. package/scripts/openapi-generator/method-template.mjs +201 -244
  119. package/scripts/openapi-generator/names.mjs +37 -86
  120. package/scripts/openapi-generator/openapi-utils.mjs +411 -147
  121. package/scripts/openapi-generator/schema.mjs +518 -238
  122. package/dist/index.cjs +0 -536
  123. package/dist/index.d.cts +0 -285
  124. package/dist/index.d.ts +0 -285
  125. package/dist/index.js +0 -495
@@ -1,476 +1,568 @@
1
- #!/usr/bin/env node
2
- import fs from "node:fs";
3
- import path from "node:path";
4
- import { fileURLToPath } from "node:url";
5
- import { generateApi } from "./openapi-generator/index.mjs";
6
-
7
- const __filename = fileURLToPath(import.meta.url);
8
- const __dirname = path.dirname(__filename);
9
-
10
- const packageRoot = path.resolve(__dirname, "..");
11
- const runtimePackageJsonPath = path.join(packageRoot, "package.json");
12
- const runtimePackageJson = JSON.parse(
13
- fs.readFileSync(runtimePackageJsonPath, "utf8"),
14
- );
15
-
16
- const consumerPackageJsonPath = path.resolve(process.cwd(), "package.json");
17
- const consumerPackageJson = fs.existsSync(consumerPackageJsonPath)
18
- ? JSON.parse(fs.readFileSync(consumerPackageJsonPath, "utf8"))
19
- : {};
20
-
21
- const cwd = process.cwd();
22
- const config = {
23
- ...(runtimePackageJson.config ?? {}),
24
- ...(consumerPackageJson.config ?? {}),
25
- };
26
-
27
- const runtimePackageName =
28
- path.resolve(cwd) === packageRoot ? "../.." : runtimePackageJson.name;
29
-
30
- const swaggerFile = getStringSetting(
31
- process.env.SWAGGER_FILE,
32
- process.env.npm_config_swagger_file,
33
- config.swaggerFile,
34
- config.input,
35
- );
36
-
37
- const swaggerUrl = getStringSetting(
38
- process.env.SWAGGER_URL,
39
- process.env.npm_config_swagger_url,
40
- config.swaggerUrl,
41
- config.url,
42
- );
43
-
44
- const defaultSwaggerUrl = getStringSetting(
45
- config.defaultSwaggerUrl,
46
- "https://localhost:7000/swagger/v1/swagger.json",
47
- );
48
-
49
- const swaggerBackupPath = path.resolve(
50
- cwd,
51
- getStringSetting(
52
- process.env.TYPED_API_SWAGGER_BACKUP_FILE,
53
- process.env.npm_config_typed_api_swagger_backup_file,
54
- config.typedApiSwaggerBackupFile,
55
- "swagger/swagger.backup.json",
56
- ),
57
- );
58
-
59
- const apiRoot = path.resolve(
60
- cwd,
61
- getStringSetting(
62
- process.env.API_OUTPUT,
63
- process.env.npm_config_api_output,
64
- config.apiOutput,
65
- config.output,
66
- "src/api",
67
- ),
68
- );
69
-
70
- const methodsDir = path.join(apiRoot, "methods");
71
-
72
- const defaultFunctionsPathSetting = getStringSetting(
73
- process.env.TYPED_API_DEFAULT_FUNCTIONS_PATH,
74
- process.env.npm_config_typed_api_default_functions_path,
75
- config.typedApiDefaultFunctionsPath,
76
- "src/defaultApiFunctions",
77
- );
78
-
79
- const defaultFunctionsFilePath = path.resolve(cwd, defaultFunctionsPathSetting);
80
-
81
- const defaultSuccessHandlerName = getStringSetting(
82
- process.env.TYPED_API_DEFAULT_SUCCESS_HANDLER,
83
- process.env.npm_config_typed_api_default_success_handler,
84
- config.typedApiDefaultSuccessHandler,
85
- "handleGoodResult",
86
- );
87
-
88
- const defaultErrorHandlerName = getStringSetting(
89
- process.env.TYPED_API_DEFAULT_ERROR_HANDLER,
90
- process.env.npm_config_typed_api_default_error_handler,
91
- config.typedApiDefaultErrorHandler,
92
- "handleErrors",
93
- );
94
-
95
- const generatorOptions = {
96
- cleanOutput: getBooleanSetting(
97
- process.env.TYPED_API_CLEAN_OUTPUT,
98
- process.env.npm_config_typed_api_clean_output,
99
- process.env.npm_config_clean_output,
100
- config.typedApiCleanOutput,
101
- config.cleanOutput,
102
- true,
103
- ),
104
- moduleNameFirstTag: getBooleanSetting(
105
- process.env.TYPED_API_MODULE_NAME_FIRST_TAG,
106
- process.env.npm_config_typed_api_module_name_first_tag,
107
- process.env.npm_config_module_name_first_tag,
108
- config.typedApiModuleNameFirstTag,
109
- config.moduleNameFirstTag,
110
- true,
111
- ),
112
- defaultResponseAsSuccess: getBooleanSetting(
113
- process.env.TYPED_API_DEFAULT_RESPONSE_AS_SUCCESS,
114
- process.env.npm_config_typed_api_default_response_as_success,
115
- process.env.npm_config_default_response_as_success,
116
- config.typedApiDefaultResponseAsSuccess,
117
- config.defaultResponseAsSuccess,
118
- false,
119
- ),
120
- generateUnionEnums: getBooleanSetting(
121
- process.env.TYPED_API_GENERATE_UNION_ENUMS,
122
- process.env.npm_config_typed_api_generate_union_enums,
123
- process.env.npm_config_generate_union_enums,
124
- config.typedApiGenerateUnionEnums,
125
- config.generateUnionEnums,
126
- false,
127
- ),
128
- enumNamesAsValues: getBooleanSetting(
129
- process.env.TYPED_API_ENUM_NAMES_AS_VALUES,
130
- process.env.npm_config_typed_api_enum_names_as_values,
131
- process.env.npm_config_enum_names_as_values,
132
- config.typedApiEnumNamesAsValues,
133
- config.enumNamesAsValues,
134
- false,
135
- ),
136
- useTypeOnlyImports: getBooleanSetting(
137
- process.env.TYPED_API_USE_TYPE_ONLY_IMPORTS,
138
- process.env.npm_config_typed_api_use_type_only_imports,
139
- process.env.npm_config_use_type_only_imports,
140
- config.typedApiUseTypeOnlyImports,
141
- config.useTypeOnlyImports,
142
- true,
143
- ),
144
- useFilterFormValues: getBooleanSetting(
145
- process.env.TYPED_API_USE_FILTER_FORM_VALUES,
146
- process.env.npm_config_typed_api_use_filter_form_values,
147
- config.typedApiUseFilterFormValues,
148
- true,
149
- ),
150
- baseUrl: getStringSetting(
151
- process.env.TYPED_API_BASE_URL,
152
- process.env.npm_config_typed_api_base_url,
153
- config.typedApiBaseUrl,
154
- ),
155
- runtimePackageName,
156
- };
157
-
158
- function getStringSetting(...values) {
159
- for (const value of values) {
160
- if (typeof value === "string" && value.trim() !== "") {
161
- return value.trim();
162
- }
163
- }
164
-
165
- return undefined;
166
- }
167
-
168
- function getBooleanSetting(...values) {
169
- const fallback = values[values.length - 1];
170
-
171
- for (const value of values.slice(0, -1)) {
172
- if (typeof value === "boolean") return value;
173
- if (typeof value === "string" && value.trim() !== "") {
174
- const normalized = value.trim().toLowerCase();
175
- if (["1", "true", "yes", "on"].includes(normalized)) return true;
176
- if (["0", "false", "no", "off"].includes(normalized)) return false;
177
- }
178
- }
179
-
180
- return Boolean(fallback);
181
- }
182
-
183
- function fileExists(filePath) {
184
- try {
185
- return fs.existsSync(filePath) && fs.statSync(filePath).isFile();
186
- } catch {
187
- return false;
188
- }
189
- }
190
-
191
- function displayPath(filePath) {
192
- return path.relative(cwd, filePath) || ".";
193
- }
194
-
195
- function writeSwaggerBackup(content, sourceLabel) {
196
- fs.mkdirSync(path.dirname(swaggerBackupPath), { recursive: true });
197
- fs.writeFileSync(swaggerBackupPath, content);
198
-
199
- console.log(
200
- `Updated Swagger backup from ${sourceLabel}: ${displayPath(swaggerBackupPath)}`,
201
- );
202
- }
203
-
204
- function copySwaggerBackupFromFile(filePath) {
205
- const content = fs.readFileSync(filePath, "utf8");
206
- JSON.parse(content);
207
- writeSwaggerBackup(content, path.relative(cwd, filePath));
208
- }
209
-
210
- async function downloadSwaggerBackupFromUrl(url) {
211
- const response = await fetch(url);
212
-
213
- if (!response.ok) {
214
- throw new Error(
215
- `Swagger URL returned ${response.status} ${response.statusText}: ${url}`,
216
- );
217
- }
218
-
219
- const content = await response.text();
220
- JSON.parse(content);
221
- writeSwaggerBackup(content, url);
222
- }
223
-
224
- function useBackupSwaggerInput(reason) {
225
- if (!fileExists(swaggerBackupPath)) {
226
- return undefined;
227
- }
228
-
229
- console.warn(
230
- `${reason} Using Swagger backup: ${displayPath(swaggerBackupPath)}`,
231
- );
232
-
233
- return {
234
- input: swaggerBackupPath,
235
- label: swaggerBackupPath,
236
- };
237
- }
238
-
239
- async function resolveSwaggerInput() {
240
- if (swaggerFile) {
241
- const swaggerFilePath = path.resolve(cwd, swaggerFile);
242
-
243
- if (fileExists(swaggerFilePath)) {
244
- copySwaggerBackupFromFile(swaggerFilePath);
245
-
246
- return {
247
- input: swaggerFilePath,
248
- label: swaggerFilePath,
249
- };
250
- }
251
-
252
- const backupInput = useBackupSwaggerInput(
253
- `Swagger file was not found: ${swaggerFilePath}.`,
254
- );
255
-
256
- if (backupInput) {
257
- return backupInput;
258
- }
259
-
260
- throw new Error(
261
- `Swagger file was not found and no backup exists: ${swaggerFilePath}`,
262
- );
263
- }
264
-
265
- if (swaggerUrl) {
266
- try {
267
- await downloadSwaggerBackupFromUrl(swaggerUrl);
268
-
269
- return {
270
- input: swaggerBackupPath,
271
- label: swaggerUrl,
272
- };
273
- } catch (error) {
274
- const backupInput = useBackupSwaggerInput(
275
- `Swagger URL is not available: ${swaggerUrl}.`,
276
- );
277
-
278
- if (backupInput) {
279
- return backupInput;
280
- }
281
-
282
- throw error;
283
- }
284
- }
285
-
286
- const defaultSwaggerFile = path.resolve(cwd, "swagger/swagger.json");
287
-
288
- if (fileExists(defaultSwaggerFile)) {
289
- copySwaggerBackupFromFile(defaultSwaggerFile);
290
-
291
- return {
292
- input: defaultSwaggerFile,
293
- label: defaultSwaggerFile,
294
- };
295
- }
296
-
297
- try {
298
- await downloadSwaggerBackupFromUrl(defaultSwaggerUrl);
299
-
300
- return {
301
- input: swaggerBackupPath,
302
- label: defaultSwaggerUrl,
303
- };
304
- } catch {
305
- const backupInput = useBackupSwaggerInput(
306
- "Default Swagger file was not found and default Swagger URL is not available.",
307
- );
308
-
309
- if (backupInput) {
310
- return backupInput;
311
- }
312
-
313
- throw new Error(
314
- `No Swagger input found. Add swagger/swagger.json, configure swaggerFile, or configure swaggerUrl.`,
315
- );
316
- }
317
- }
318
-
319
- function createImportStatement({ names, from, typeOnly = false }) {
320
- const cleanNames = [...new Set(names.filter(Boolean))];
321
- if (cleanNames.length === 0) return "";
322
- const importKeyword = typeOnly ? "import type" : "import";
323
- return `${importKeyword} { ${cleanNames.join(", ")} } from ${JSON.stringify(from)};`;
324
- }
325
-
326
- function createDefaultFunctionsFileIfMissing(filePath) {
327
- const extension = path.extname(filePath);
328
- const resolvedFilePath = extension ? filePath : `${filePath}.ts`;
329
-
330
- if (fs.existsSync(resolvedFilePath)) {
331
- return resolvedFilePath;
332
- }
333
-
334
- fs.mkdirSync(path.dirname(resolvedFilePath), { recursive: true });
335
-
336
- fs.writeFileSync(
337
- resolvedFilePath,
338
- `${createImportStatement({
339
- names: ["ApiErrorResult", "ApiSuccessResult"],
340
- from: runtimePackageName,
341
- typeOnly: generatorOptions.useTypeOnlyImports !== false,
342
- })}
343
-
344
- export function ${defaultSuccessHandlerName}<T>(
345
- _response: ApiSuccessResult<T>,
346
- ): void | Promise<void> {
347
- // Add your default success handling here.
348
- }
349
-
350
- export function ${defaultErrorHandlerName}<T>(
351
- _error: ApiErrorResult<T>,
352
- ): void | Promise<void> {
353
- // Add your default error handling here.
354
- }
355
- `,
356
- );
357
-
358
- console.log(
359
- `Created default API handler file: ${path.relative(process.cwd(), resolvedFilePath)}`,
360
- );
361
-
362
- return resolvedFilePath;
363
- }
364
-
365
- function toRelativeImportPath(fromDirectory, filePath) {
366
- let relativePath = path.relative(fromDirectory, filePath);
367
-
368
- if (!relativePath.startsWith(".")) {
369
- relativePath = `./${relativePath}`;
370
- }
371
-
372
- return relativePath
373
- .replaceAll(path.sep, "/")
374
- .replace(/\.(ts|tsx|js|jsx)$/, "");
375
- }
376
-
377
- function escapeRegExp(value) {
378
- return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
379
- }
380
-
381
- function fileExportsName(source, exportName) {
382
- const escapedName = escapeRegExp(exportName);
383
-
384
- const directExportPattern = new RegExp(
385
- `export\\s+(?:async\\s+)?(?:function|const|let|var|class)\\s+${escapedName}\\b`,
386
- "m",
387
- );
388
-
389
- if (directExportPattern.test(source)) {
390
- return true;
391
- }
392
-
393
- const namedExportBlocks = source.matchAll(/export\s*\{([\s\S]*?)\}/g);
394
-
395
- for (const match of namedExportBlocks) {
396
- const exportedNames = match[1]
397
- .split(",")
398
- .map((part) => part.trim())
399
- .filter(Boolean);
400
-
401
- if (
402
- exportedNames.some((part) => {
403
- const [localName, aliasName] = part
404
- .split(/\s+as\s+/)
405
- .map((name) => name.trim());
406
-
407
- return (
408
- aliasName === exportName || (!aliasName && localName === exportName)
409
- );
410
- })
411
- ) {
412
- return true;
413
- }
414
- }
415
-
416
- return false;
417
- }
418
-
419
- function resolveDefaultHandlers() {
420
- const defaultFunctionsFile = createDefaultFunctionsFileIfMissing(
421
- defaultFunctionsFilePath,
422
- );
423
- const source = fs.readFileSync(defaultFunctionsFile, "utf8");
424
- const hasSuccessHandler = fileExportsName(source, defaultSuccessHandlerName);
425
- const hasErrorHandler = fileExportsName(source, defaultErrorHandlerName);
426
-
427
- if (!hasSuccessHandler || !hasErrorHandler) {
428
- const missingNames = [
429
- ...(!hasSuccessHandler ? [defaultSuccessHandlerName] : []),
430
- ...(!hasErrorHandler ? [defaultErrorHandlerName] : []),
431
- ];
432
-
433
- console.warn(
434
- `Default API handlers were not imported because ${path.relative(
435
- process.cwd(),
436
- defaultFunctionsFile,
437
- )} does not export: ${missingNames.join(", ")}.`,
438
- );
439
-
440
- return undefined;
441
- }
442
-
443
- return {
444
- path: toRelativeImportPath(methodsDir, defaultFunctionsFile),
445
- success: defaultSuccessHandlerName,
446
- error: defaultErrorHandlerName,
447
- };
448
- }
449
-
450
- async function main() {
451
- const swaggerInput = await resolveSwaggerInput();
452
-
453
- console.log(`Generating API from: ${swaggerInput.label}`);
454
- console.log(`Output folder: ${apiRoot}`);
455
- console.log(
456
- "Generator: local typedapi OpenAPI generator, function-only methods",
457
- );
458
-
459
- const result = await generateApi({
460
- input: swaggerInput.input,
461
- output: apiRoot,
462
- ...generatorOptions,
463
- defaultHandlers: resolveDefaultHandlers(),
464
- });
465
-
466
- for (const file of result.files) {
467
- console.log(`Generated ${path.relative(cwd, file)}`);
468
- }
469
-
470
- console.log("API generation completed.");
471
- }
472
-
473
- main().catch((error) => {
474
- console.error(error);
475
- process.exit(1);
476
- });
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { generateApi } from "./openapi-generator/index.mjs";
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+
10
+ const packageRoot = path.resolve(__dirname, "..");
11
+ const runtimePackageJsonPath = path.join(packageRoot, "package.json");
12
+ const runtimePackageJson = JSON.parse(
13
+ fs.readFileSync(runtimePackageJsonPath, "utf8"),
14
+ );
15
+
16
+ const consumerPackageJsonPath = path.resolve(process.cwd(), "package.json");
17
+ const consumerPackageJson = fs.existsSync(consumerPackageJsonPath)
18
+ ? JSON.parse(fs.readFileSync(consumerPackageJsonPath, "utf8"))
19
+ : {};
20
+
21
+ const cwd = process.cwd();
22
+ const cliArguments = new Set(process.argv.slice(2));
23
+
24
+ if (cliArguments.has("--help") || cliArguments.has("-h")) {
25
+ console.log(`typedapi-generate [options]
26
+
27
+ Options:
28
+ --check Fail when generated files differ
29
+ --strict Do not fall back to a stale Swagger backup
30
+ --offline Generate only from the configured Swagger backup
31
+ --verbose Print additional diagnostics
32
+ --help Show this help`);
33
+ process.exit(0);
34
+ }
35
+
36
+ const checkMode = cliArguments.has("--check");
37
+ const strictMode = cliArguments.has("--strict");
38
+ const offlineMode = cliArguments.has("--offline");
39
+ const verboseMode = cliArguments.has("--verbose");
40
+
41
+ const config = {
42
+ ...(runtimePackageJson.config ?? {}),
43
+ ...(consumerPackageJson.config ?? {}),
44
+ };
45
+
46
+ const runtimePackageName =
47
+ path.resolve(cwd) === packageRoot ? "../.." : runtimePackageJson.name;
48
+
49
+ const swaggerFile = getStringSetting(
50
+ process.env.SWAGGER_FILE,
51
+ process.env.npm_config_swagger_file,
52
+ config.swaggerFile,
53
+ config.input,
54
+ );
55
+
56
+ const swaggerUrl = getStringSetting(
57
+ process.env.SWAGGER_URL,
58
+ process.env.npm_config_swagger_url,
59
+ config.swaggerUrl,
60
+ config.url,
61
+ );
62
+
63
+ const defaultSwaggerUrl = getStringSetting(
64
+ config.defaultSwaggerUrl,
65
+ "https://localhost:7000/swagger/v1/swagger.json",
66
+ );
67
+
68
+ const swaggerBackupPath = path.resolve(
69
+ cwd,
70
+ getStringSetting(
71
+ process.env.TYPED_API_SWAGGER_BACKUP_FILE,
72
+ process.env.npm_config_typed_api_swagger_backup_file,
73
+ config.typedApiSwaggerBackupFile,
74
+ "swagger/swagger.backup.json",
75
+ ),
76
+ );
77
+
78
+ const swaggerDownloadTimeoutMs = getNumberSetting(
79
+ process.env.TYPED_API_DOWNLOAD_TIMEOUT_MS,
80
+ process.env.npm_config_typed_api_download_timeout_ms,
81
+ config.typedApiDownloadTimeoutMs,
82
+ 15000,
83
+ );
84
+
85
+ const apiRoot = path.resolve(
86
+ cwd,
87
+ getStringSetting(
88
+ process.env.API_OUTPUT,
89
+ process.env.npm_config_api_output,
90
+ config.apiOutput,
91
+ config.output,
92
+ "src/api",
93
+ ),
94
+ );
95
+
96
+ const methodsDir = path.join(apiRoot, "methods");
97
+
98
+ const defaultFunctionsPathSetting = getStringSetting(
99
+ process.env.TYPED_API_DEFAULT_FUNCTIONS_PATH,
100
+ process.env.npm_config_typed_api_default_functions_path,
101
+ config.typedApiDefaultFunctionsPath,
102
+ "src/defaultApiFunctions",
103
+ );
104
+
105
+ const defaultFunctionsFilePath = path.resolve(cwd, defaultFunctionsPathSetting);
106
+
107
+ const defaultSuccessHandlerName = getStringSetting(
108
+ process.env.TYPED_API_DEFAULT_SUCCESS_HANDLER,
109
+ process.env.npm_config_typed_api_default_success_handler,
110
+ config.typedApiDefaultSuccessHandler,
111
+ "handleGoodResult",
112
+ );
113
+
114
+ const defaultErrorHandlerName = getStringSetting(
115
+ process.env.TYPED_API_DEFAULT_ERROR_HANDLER,
116
+ process.env.npm_config_typed_api_default_error_handler,
117
+ config.typedApiDefaultErrorHandler,
118
+ "handleErrors",
119
+ );
120
+
121
+ const generatorOptions = {
122
+ cleanOutput: getBooleanSetting(
123
+ process.env.TYPED_API_CLEAN_OUTPUT,
124
+ process.env.npm_config_typed_api_clean_output,
125
+ process.env.npm_config_clean_output,
126
+ config.typedApiCleanOutput,
127
+ config.cleanOutput,
128
+ true,
129
+ ),
130
+ moduleNameFirstTag: getBooleanSetting(
131
+ process.env.TYPED_API_MODULE_NAME_FIRST_TAG,
132
+ process.env.npm_config_typed_api_module_name_first_tag,
133
+ process.env.npm_config_module_name_first_tag,
134
+ config.typedApiModuleNameFirstTag,
135
+ config.moduleNameFirstTag,
136
+ true,
137
+ ),
138
+ defaultResponseAsSuccess: getBooleanSetting(
139
+ process.env.TYPED_API_DEFAULT_RESPONSE_AS_SUCCESS,
140
+ process.env.npm_config_typed_api_default_response_as_success,
141
+ process.env.npm_config_default_response_as_success,
142
+ config.typedApiDefaultResponseAsSuccess,
143
+ config.defaultResponseAsSuccess,
144
+ false,
145
+ ),
146
+ generateUnionEnums: getBooleanSetting(
147
+ process.env.TYPED_API_GENERATE_UNION_ENUMS,
148
+ process.env.npm_config_typed_api_generate_union_enums,
149
+ process.env.npm_config_generate_union_enums,
150
+ config.typedApiGenerateUnionEnums,
151
+ config.generateUnionEnums,
152
+ false,
153
+ ),
154
+ enumNamesAsValues: getBooleanSetting(
155
+ process.env.TYPED_API_ENUM_NAMES_AS_VALUES,
156
+ process.env.npm_config_typed_api_enum_names_as_values,
157
+ process.env.npm_config_enum_names_as_values,
158
+ config.typedApiEnumNamesAsValues,
159
+ config.enumNamesAsValues,
160
+ false,
161
+ ),
162
+ useTypeOnlyImports: getBooleanSetting(
163
+ process.env.TYPED_API_USE_TYPE_ONLY_IMPORTS,
164
+ process.env.npm_config_typed_api_use_type_only_imports,
165
+ process.env.npm_config_use_type_only_imports,
166
+ config.typedApiUseTypeOnlyImports,
167
+ config.useTypeOnlyImports,
168
+ true,
169
+ ),
170
+ useFilterFormValues: getBooleanSetting(
171
+ process.env.TYPED_API_USE_FILTER_FORM_VALUES,
172
+ process.env.npm_config_typed_api_use_filter_form_values,
173
+ config.typedApiUseFilterFormValues,
174
+ true,
175
+ ),
176
+ generateMissingOperationIds: getBooleanSetting(
177
+ process.env.TYPED_API_GENERATE_MISSING_OPERATION_IDS,
178
+ process.env.npm_config_typed_api_generate_missing_operation_ids,
179
+ config.typedApiGenerateMissingOperationIds,
180
+ false,
181
+ ),
182
+ methodNameStyle: getMethodNameStyle(
183
+ process.env.TYPED_API_METHOD_NAME_STYLE,
184
+ process.env.npm_config_typed_api_method_name_style,
185
+ config.typedApiMethodNameStyle,
186
+ "operationId",
187
+ ),
188
+ prefixMethodNamesWithController: getBooleanSetting(
189
+ process.env.TYPED_API_PREFIX_METHOD_NAMES_WITH_CONTROLLER,
190
+ process.env.npm_config_typed_api_prefix_method_names_with_controller,
191
+ config.typedApiPrefixMethodNamesWithController,
192
+ true,
193
+ ),
194
+ check: checkMode,
195
+ baseUrl: getStringSetting(
196
+ process.env.TYPED_API_BASE_URL,
197
+ process.env.npm_config_typed_api_base_url,
198
+ config.typedApiBaseUrl,
199
+ ),
200
+ runtimePackageName,
201
+ };
202
+
203
+ function getStringSetting(...values) {
204
+ for (const value of values) {
205
+ if (typeof value === "string" && value.trim() !== "") {
206
+ return value.trim();
207
+ }
208
+ }
209
+
210
+ return undefined;
211
+ }
212
+
213
+ function getMethodNameStyle(...values) {
214
+ const value = getStringSetting(...values) ?? "operationId";
215
+ const normalized = value.replace(/[\s_-]/g, "").toLowerCase();
216
+
217
+ if (["operationid", "operation"].includes(normalized)) return "operationId";
218
+ if (["action", "actionname", "controllermethod"].includes(normalized)) return "action";
219
+
220
+ throw new Error(
221
+ `Invalid typedApiMethodNameStyle ${JSON.stringify(value)}. Use "operationId" or "action".`,
222
+ );
223
+ }
224
+
225
+ function getBooleanSetting(...values) {
226
+ const fallback = values[values.length - 1];
227
+
228
+ for (const value of values.slice(0, -1)) {
229
+ if (typeof value === "boolean") return value;
230
+ if (typeof value === "string" && value.trim() !== "") {
231
+ const normalized = value.trim().toLowerCase();
232
+ if (["1", "true", "yes", "on"].includes(normalized)) return true;
233
+ if (["0", "false", "no", "off"].includes(normalized)) return false;
234
+ }
235
+ }
236
+
237
+ return Boolean(fallback);
238
+ }
239
+
240
+ function getNumberSetting(...values) {
241
+ for (const value of values) {
242
+ const number = typeof value === "number" ? value : Number(value);
243
+ if (Number.isFinite(number) && number > 0) return number;
244
+ }
245
+ return undefined;
246
+ }
247
+
248
+ function fileExists(filePath) {
249
+ try {
250
+ return fs.existsSync(filePath) && fs.statSync(filePath).isFile();
251
+ } catch {
252
+ return false;
253
+ }
254
+ }
255
+
256
+ function displayPath(filePath) {
257
+ return path.relative(cwd, filePath) || ".";
258
+ }
259
+
260
+ function writeSwaggerBackup(content, sourceLabel) {
261
+ fs.mkdirSync(path.dirname(swaggerBackupPath), { recursive: true });
262
+ const temporaryPath = `${swaggerBackupPath}.tmp-${process.pid}-${Date.now()}`;
263
+ fs.writeFileSync(temporaryPath, content);
264
+ fs.renameSync(temporaryPath, swaggerBackupPath);
265
+
266
+ console.log(
267
+ `Updated Swagger backup from ${sourceLabel}: ${displayPath(swaggerBackupPath)}`,
268
+ );
269
+ }
270
+
271
+ function copySwaggerBackupFromFile(filePath) {
272
+ const content = fs.readFileSync(filePath, "utf8");
273
+ JSON.parse(content);
274
+ writeSwaggerBackup(content, path.relative(cwd, filePath));
275
+ }
276
+
277
+ async function downloadSwaggerBackupFromUrl(url) {
278
+ const controller = new AbortController();
279
+ const timeout = setTimeout(
280
+ () => controller.abort(new Error(`Swagger download timed out after ${swaggerDownloadTimeoutMs}ms.`)),
281
+ swaggerDownloadTimeoutMs,
282
+ );
283
+ let response;
284
+ try {
285
+ response = await fetch(url, { signal: controller.signal });
286
+ } finally {
287
+ clearTimeout(timeout);
288
+ }
289
+
290
+ if (!response.ok) {
291
+ throw new Error(
292
+ `Swagger URL returned ${response.status} ${response.statusText}: ${url}`,
293
+ );
294
+ }
295
+
296
+ const content = await response.text();
297
+ JSON.parse(content);
298
+ writeSwaggerBackup(content, url);
299
+ }
300
+
301
+ function useBackupSwaggerInput(reason) {
302
+ if (!fileExists(swaggerBackupPath)) {
303
+ return undefined;
304
+ }
305
+
306
+ console.warn(
307
+ `${reason} Using Swagger backup: ${displayPath(swaggerBackupPath)}`,
308
+ );
309
+
310
+ return {
311
+ input: swaggerBackupPath,
312
+ label: swaggerBackupPath,
313
+ };
314
+ }
315
+
316
+ async function resolveSwaggerInput() {
317
+ if (offlineMode) {
318
+ if (!fileExists(swaggerBackupPath)) {
319
+ throw new Error(`Offline mode requires an existing Swagger backup: ${swaggerBackupPath}`);
320
+ }
321
+ return { input: swaggerBackupPath, label: swaggerBackupPath };
322
+ }
323
+
324
+ if (swaggerFile) {
325
+ const swaggerFilePath = path.resolve(cwd, swaggerFile);
326
+
327
+ if (fileExists(swaggerFilePath)) {
328
+ copySwaggerBackupFromFile(swaggerFilePath);
329
+
330
+ return {
331
+ input: swaggerFilePath,
332
+ label: swaggerFilePath,
333
+ };
334
+ }
335
+
336
+ const backupInput = strictMode
337
+ ? undefined
338
+ : useBackupSwaggerInput(`Swagger file was not found: ${swaggerFilePath}.`);
339
+
340
+ if (backupInput) return backupInput;
341
+
342
+ throw new Error(
343
+ `Swagger file was not found and no backup exists: ${swaggerFilePath}`,
344
+ );
345
+ }
346
+
347
+ if (swaggerUrl) {
348
+ try {
349
+ await downloadSwaggerBackupFromUrl(swaggerUrl);
350
+
351
+ return {
352
+ input: swaggerBackupPath,
353
+ label: swaggerUrl,
354
+ };
355
+ } catch (error) {
356
+ const backupInput = strictMode
357
+ ? undefined
358
+ : useBackupSwaggerInput(`Swagger URL is not available: ${swaggerUrl}.`);
359
+
360
+ if (backupInput) return backupInput;
361
+
362
+ throw error;
363
+ }
364
+ }
365
+
366
+ const defaultSwaggerFile = path.resolve(cwd, "swagger/swagger.json");
367
+
368
+ if (fileExists(defaultSwaggerFile)) {
369
+ copySwaggerBackupFromFile(defaultSwaggerFile);
370
+
371
+ return {
372
+ input: defaultSwaggerFile,
373
+ label: defaultSwaggerFile,
374
+ };
375
+ }
376
+
377
+ try {
378
+ await downloadSwaggerBackupFromUrl(defaultSwaggerUrl);
379
+
380
+ return {
381
+ input: swaggerBackupPath,
382
+ label: defaultSwaggerUrl,
383
+ };
384
+ } catch {
385
+ const backupInput = strictMode
386
+ ? undefined
387
+ : useBackupSwaggerInput(
388
+ "Default Swagger file was not found and default Swagger URL is not available.",
389
+ );
390
+
391
+ if (backupInput) return backupInput;
392
+
393
+ throw new Error(
394
+ `No Swagger input found. Add swagger/swagger.json, configure swaggerFile, or configure swaggerUrl.`,
395
+ );
396
+ }
397
+ }
398
+
399
+ function createImportStatement({ names, from, typeOnly = false }) {
400
+ const cleanNames = [...new Set(names.filter(Boolean))];
401
+ if (cleanNames.length === 0) return "";
402
+ const importKeyword = typeOnly ? "import type" : "import";
403
+ return `${importKeyword} { ${cleanNames.join(", ")} } from ${JSON.stringify(from)};`;
404
+ }
405
+
406
+ function createDefaultFunctionsFileIfMissing(filePath) {
407
+ const extension = path.extname(filePath);
408
+ const resolvedFilePath = extension ? filePath : `${filePath}.ts`;
409
+
410
+ if (fs.existsSync(resolvedFilePath)) {
411
+ return resolvedFilePath;
412
+ }
413
+
414
+ fs.mkdirSync(path.dirname(resolvedFilePath), { recursive: true });
415
+
416
+ fs.writeFileSync(
417
+ resolvedFilePath,
418
+ `${createImportStatement({
419
+ names: ["ApiErrorResult", "ApiSuccessResult"],
420
+ from: runtimePackageName,
421
+ typeOnly: generatorOptions.useTypeOnlyImports !== false,
422
+ })}
423
+
424
+ export function ${defaultSuccessHandlerName}<T>(
425
+ _response: ApiSuccessResult<T>,
426
+ ): void | Promise<void> {
427
+ // Add your default success handling here.
428
+ }
429
+
430
+ export function ${defaultErrorHandlerName}<T>(
431
+ _error: ApiErrorResult<T>,
432
+ ): void | Promise<void> {
433
+ // Add your default error handling here.
434
+ }
435
+ `,
436
+ );
437
+
438
+ console.log(
439
+ `Created default API handler file: ${path.relative(process.cwd(), resolvedFilePath)}`,
440
+ );
441
+
442
+ return resolvedFilePath;
443
+ }
444
+
445
+ function toRelativeImportPath(fromDirectory, filePath) {
446
+ let relativePath = path.relative(fromDirectory, filePath);
447
+
448
+ if (!relativePath.startsWith(".")) {
449
+ relativePath = `./${relativePath}`;
450
+ }
451
+
452
+ return relativePath
453
+ .replaceAll(path.sep, "/")
454
+ .replace(/\.(ts|tsx|js|jsx)$/, "");
455
+ }
456
+
457
+ function escapeRegExp(value) {
458
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
459
+ }
460
+
461
+ function fileExportsName(source, exportName) {
462
+ const escapedName = escapeRegExp(exportName);
463
+
464
+ const directExportPattern = new RegExp(
465
+ `export\\s+(?:async\\s+)?(?:function|const|let|var|class)\\s+${escapedName}\\b`,
466
+ "m",
467
+ );
468
+
469
+ if (directExportPattern.test(source)) {
470
+ return true;
471
+ }
472
+
473
+ const namedExportBlocks = source.matchAll(/export\s*\{([\s\S]*?)\}/g);
474
+
475
+ for (const match of namedExportBlocks) {
476
+ const exportedNames = match[1]
477
+ .split(",")
478
+ .map((part) => part.trim())
479
+ .filter(Boolean);
480
+
481
+ if (
482
+ exportedNames.some((part) => {
483
+ const [localName, aliasName] = part
484
+ .split(/\s+as\s+/)
485
+ .map((name) => name.trim());
486
+
487
+ return (
488
+ aliasName === exportName || (!aliasName && localName === exportName)
489
+ );
490
+ })
491
+ ) {
492
+ return true;
493
+ }
494
+ }
495
+
496
+ return false;
497
+ }
498
+
499
+ function resolveDefaultHandlers() {
500
+ const candidatePath = path.extname(defaultFunctionsFilePath)
501
+ ? defaultFunctionsFilePath
502
+ : `${defaultFunctionsFilePath}.ts`;
503
+ if (checkMode && !fs.existsSync(candidatePath)) {
504
+ if (verboseMode) console.log("Skipping missing default handler file in check mode.");
505
+ return undefined;
506
+ }
507
+ const defaultFunctionsFile = createDefaultFunctionsFileIfMissing(defaultFunctionsFilePath);
508
+ const source = fs.readFileSync(defaultFunctionsFile, "utf8");
509
+ const hasSuccessHandler = fileExportsName(source, defaultSuccessHandlerName);
510
+ const hasErrorHandler = fileExportsName(source, defaultErrorHandlerName);
511
+
512
+ if (!hasSuccessHandler || !hasErrorHandler) {
513
+ const missingNames = [
514
+ ...(!hasSuccessHandler ? [defaultSuccessHandlerName] : []),
515
+ ...(!hasErrorHandler ? [defaultErrorHandlerName] : []),
516
+ ];
517
+
518
+ console.warn(
519
+ `Default API handlers were not imported because ${path.relative(
520
+ process.cwd(),
521
+ defaultFunctionsFile,
522
+ )} does not export: ${missingNames.join(", ")}.`,
523
+ );
524
+
525
+ return undefined;
526
+ }
527
+
528
+ return {
529
+ path: toRelativeImportPath(methodsDir, defaultFunctionsFile),
530
+ success: defaultSuccessHandlerName,
531
+ error: defaultErrorHandlerName,
532
+ };
533
+ }
534
+
535
+ async function main() {
536
+ const swaggerInput = await resolveSwaggerInput();
537
+
538
+ console.log(`Generating API from: ${swaggerInput.label}`);
539
+ console.log(`Output folder: ${apiRoot}`);
540
+ console.log(
541
+ "Generator: local typedapi OpenAPI generator, function-only methods",
542
+ );
543
+
544
+ const result = await generateApi({
545
+ input: swaggerInput.input,
546
+ output: apiRoot,
547
+ ...generatorOptions,
548
+ generatorVersion: runtimePackageJson.version,
549
+ defaultHandlers: resolveDefaultHandlers(),
550
+ });
551
+
552
+ for (const file of result.files) {
553
+ console.log(`Generated ${path.relative(cwd, file)}`);
554
+ }
555
+
556
+ if (verboseMode && result.writeStrategy === "managed-file-sync") {
557
+ console.log(
558
+ "Used Windows-safe managed-file synchronization because the output directory could not be replaced atomically.",
559
+ );
560
+ }
561
+
562
+ console.log(checkMode ? "Generated API is up to date." : "API generation completed.");
563
+ }
564
+
565
+ main().catch((error) => {
566
+ console.error(error);
567
+ process.exit(1);
568
+ });