typedapi-client-helpers 0.3.5 → 0.3.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "typedapi-client-helpers",
3
- "version": "0.3.5",
3
+ "version": "0.3.6",
4
4
  "description": "Reusable typed OpenAPI client helpers.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -2,7 +2,10 @@
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- import { generateApi } from "./openapi-generator/index.mjs";
5
+ import {
6
+ cleanupGenerationArtifacts,
7
+ generateApi,
8
+ } from "./openapi-generator/index.mjs";
6
9
 
7
10
  const __filename = fileURLToPath(import.meta.url);
8
11
  const __dirname = path.dirname(__filename);
@@ -93,6 +96,28 @@ const apiRoot = path.resolve(
93
96
  ),
94
97
  );
95
98
 
99
+ let terminationSignalHandled = false;
100
+
101
+ function handleTerminationSignal(signal) {
102
+ if (terminationSignalHandled) return;
103
+
104
+ terminationSignalHandled = true;
105
+
106
+ try {
107
+ cleanupGenerationArtifacts(apiRoot);
108
+ } catch (error) {
109
+ console.error(
110
+ `Failed to clean TypedApi temporary files after ${signal}:`,
111
+ error,
112
+ );
113
+ }
114
+
115
+ process.exit(signal === "SIGINT" ? 130 : 143);
116
+ }
117
+
118
+ process.once("SIGINT", () => handleTerminationSignal("SIGINT"));
119
+ process.once("SIGTERM", () => handleTerminationSignal("SIGTERM"));
120
+
96
121
  const methodsDir = path.join(apiRoot, "methods");
97
122
 
98
123
  const defaultFunctionsPathSetting = getStringSetting(
@@ -222,7 +247,8 @@ function getMethodNameStyle(...values) {
222
247
  const normalized = value.replace(/[\s_-]/g, "").toLowerCase();
223
248
 
224
249
  if (["operationid", "operation"].includes(normalized)) return "operationId";
225
- if (["action", "actionname", "controllermethod"].includes(normalized)) return "action";
250
+ if (["action", "actionname", "controllermethod"].includes(normalized))
251
+ return "action";
226
252
 
227
253
  throw new Error(
228
254
  `Invalid typedApiMethodNameStyle ${JSON.stringify(value)}. Use "operationId" or "action".`,
@@ -284,7 +310,12 @@ function copySwaggerBackupFromFile(filePath) {
284
310
  async function downloadSwaggerBackupFromUrl(url) {
285
311
  const controller = new AbortController();
286
312
  const timeout = setTimeout(
287
- () => controller.abort(new Error(`Swagger download timed out after ${swaggerDownloadTimeoutMs}ms.`)),
313
+ () =>
314
+ controller.abort(
315
+ new Error(
316
+ `Swagger download timed out after ${swaggerDownloadTimeoutMs}ms.`,
317
+ ),
318
+ ),
288
319
  swaggerDownloadTimeoutMs,
289
320
  );
290
321
  let response;
@@ -323,7 +354,9 @@ function useBackupSwaggerInput(reason) {
323
354
  async function resolveSwaggerInput() {
324
355
  if (offlineMode) {
325
356
  if (!fileExists(swaggerBackupPath)) {
326
- throw new Error(`Offline mode requires an existing Swagger backup: ${swaggerBackupPath}`);
357
+ throw new Error(
358
+ `Offline mode requires an existing Swagger backup: ${swaggerBackupPath}`,
359
+ );
327
360
  }
328
361
  return { input: swaggerBackupPath, label: swaggerBackupPath };
329
362
  }
@@ -342,7 +375,9 @@ async function resolveSwaggerInput() {
342
375
 
343
376
  const backupInput = strictMode
344
377
  ? undefined
345
- : useBackupSwaggerInput(`Swagger file was not found: ${swaggerFilePath}.`);
378
+ : useBackupSwaggerInput(
379
+ `Swagger file was not found: ${swaggerFilePath}.`,
380
+ );
346
381
 
347
382
  if (backupInput) return backupInput;
348
383
 
@@ -452,7 +487,6 @@ export const ${defaultErrorMessageName} = "An unknown error occurred.";
452
487
  return resolvedFilePath;
453
488
  }
454
489
 
455
-
456
490
  function addDefaultErrorMessageIfMissing(filePath) {
457
491
  const source = fs.readFileSync(filePath, "utf8");
458
492
  if (fileExportsName(source, defaultErrorMessageName)) return;
@@ -527,10 +561,13 @@ function resolveDefaultHandlers() {
527
561
  ? defaultFunctionsFilePath
528
562
  : `${defaultFunctionsFilePath}.ts`;
529
563
  if (checkMode && !fs.existsSync(candidatePath)) {
530
- if (verboseMode) console.log("Skipping missing default handler file in check mode.");
564
+ if (verboseMode)
565
+ console.log("Skipping missing default handler file in check mode.");
531
566
  return undefined;
532
567
  }
533
- const defaultFunctionsFile = createDefaultFunctionsFileIfMissing(defaultFunctionsFilePath);
568
+ const defaultFunctionsFile = createDefaultFunctionsFileIfMissing(
569
+ defaultFunctionsFilePath,
570
+ );
534
571
  const source = fs.readFileSync(defaultFunctionsFile, "utf8");
535
572
  const hasSuccessHandler = fileExportsName(source, defaultSuccessHandlerName);
536
573
  const hasErrorHandler = fileExportsName(source, defaultErrorHandlerName);
@@ -562,6 +599,8 @@ function resolveDefaultHandlers() {
562
599
  }
563
600
 
564
601
  async function main() {
602
+ cleanupGenerationArtifacts(apiRoot);
603
+
565
604
  const swaggerInput = await resolveSwaggerInput();
566
605
 
567
606
  console.log(`Generating API from: ${swaggerInput.label}`);
@@ -588,7 +627,9 @@ async function main() {
588
627
  );
589
628
  }
590
629
 
591
- console.log(checkMode ? "Generated API is up to date." : "API generation completed.");
630
+ console.log(
631
+ checkMode ? "Generated API is up to date." : "API generation completed.",
632
+ );
592
633
  }
593
634
 
594
635
  main().catch((error) => {
@@ -1,7 +1,10 @@
1
1
  import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
- import { collectOperations, validateOpenApiDocument } from "./openapi-utils.mjs";
4
+ import {
5
+ collectOperations,
6
+ validateOpenApiDocument,
7
+ } from "./openapi-utils.mjs";
5
8
  import { generateDataContracts } from "./schema.mjs";
6
9
  import { generateHttpClient } from "./http-client-template.mjs";
7
10
  import {
@@ -54,13 +57,18 @@ export async function generateApi(options) {
54
57
 
55
58
  const controllers = new Map();
56
59
  for (const operationInfo of operations) {
57
- const controllerName = controllerNameForOperation(operationInfo, normalizedOptions);
60
+ const controllerName = controllerNameForOperation(
61
+ operationInfo,
62
+ normalizedOptions,
63
+ );
58
64
  const controllerOperations = controllers.get(controllerName) ?? [];
59
65
  controllerOperations.push(operationInfo);
60
66
  controllers.set(controllerName, controllerOperations);
61
67
  }
62
68
 
63
- const controllerNames = [...controllers.keys()].sort((a, b) => a.localeCompare(b));
69
+ const controllerNames = [...controllers.keys()].sort((a, b) =>
70
+ a.localeCompare(b),
71
+ );
64
72
  for (const controllerName of controllerNames) {
65
73
  files.set(
66
74
  `methods/${methodFileNameForController(controllerName)}`,
@@ -73,7 +81,6 @@ export async function generateApi(options) {
73
81
  );
74
82
  }
75
83
 
76
-
77
84
  const changes = compareOutput(output, files);
78
85
  if (options.check) {
79
86
  if (changes.length > 0) {
@@ -81,7 +88,10 @@ export async function generateApi(options) {
81
88
  `Generated API is out of date:\n${changes.map((item) => `- ${item}`).join("\n")}`,
82
89
  );
83
90
  }
84
- return { files: [...files.keys()].map((file) => path.join(output, file)), changed: false };
91
+ return {
92
+ files: [...files.keys()].map((file) => path.join(output, file)),
93
+ changed: false,
94
+ };
85
95
  }
86
96
 
87
97
  const writeStrategy = writeOutputTransactionally(
@@ -105,7 +115,8 @@ function compareOutput(output, files) {
105
115
  changes.push(`${relativePath} is missing`);
106
116
  continue;
107
117
  }
108
- if (fs.readFileSync(target, "utf8") !== expected) changes.push(`${relativePath} differs`);
118
+ if (fs.readFileSync(target, "utf8") !== expected)
119
+ changes.push(`${relativePath} differs`);
109
120
  }
110
121
 
111
122
  for (const relativePath of listManagedFiles(output)) {
@@ -123,7 +134,9 @@ function listManagedFiles(output) {
123
134
  result.push(topLevel);
124
135
  continue;
125
136
  }
126
- walk(target, (file) => result.push(path.relative(output, file).replaceAll(path.sep, "/")));
137
+ walk(target, (file) =>
138
+ result.push(path.relative(output, file).replaceAll(path.sep, "/")),
139
+ );
127
140
  }
128
141
  return result;
129
142
  }
@@ -142,6 +155,61 @@ function removeManagedOutput(root) {
142
155
  }
143
156
  }
144
157
 
158
+ export function cleanupGenerationArtifacts(output) {
159
+ const resolvedOutput = path.resolve(output);
160
+ const parent = path.dirname(resolvedOutput);
161
+
162
+ if (!fs.existsSync(parent)) return;
163
+
164
+ const outputName = path.basename(resolvedOutput);
165
+ const stagingPrefix = `.${outputName}.typedapi-staging-`;
166
+ const backupPrefix = `.${outputName}.typedapi-backup-`;
167
+ const managedBackupPrefix = `.${outputName}.typedapi-managed-backup-`;
168
+ const backups = [];
169
+
170
+ for (const entry of fs.readdirSync(parent, { withFileTypes: true })) {
171
+ if (!entry.isDirectory()) continue;
172
+
173
+ const entryPath = path.join(parent, entry.name);
174
+
175
+ if (
176
+ entry.name.startsWith(stagingPrefix) ||
177
+ entry.name.startsWith(managedBackupPrefix)
178
+ ) {
179
+ removePathWithRetry(entryPath);
180
+ continue;
181
+ }
182
+
183
+ if (entry.name.startsWith(backupPrefix)) {
184
+ backups.push(entryPath);
185
+ }
186
+ }
187
+
188
+ if (backups.length === 0) return;
189
+
190
+ if (!fs.existsSync(resolvedOutput)) {
191
+ backups.sort(
192
+ (left, right) => fs.statSync(right).mtimeMs - fs.statSync(left).mtimeMs,
193
+ );
194
+
195
+ const [backupToRestore, ...staleBackups] = backups;
196
+
197
+ retryFileSystemOperation(() =>
198
+ fs.renameSync(backupToRestore, resolvedOutput),
199
+ );
200
+
201
+ for (const backup of staleBackups) {
202
+ removePathWithRetry(backup);
203
+ }
204
+
205
+ return;
206
+ }
207
+
208
+ for (const backup of backups) {
209
+ removePathWithRetry(backup);
210
+ }
211
+ }
212
+
145
213
  const RETRYABLE_FS_ERROR_CODES = new Set([
146
214
  "EACCES",
147
215
  "EBUSY",
@@ -151,14 +219,25 @@ const RETRYABLE_FS_ERROR_CODES = new Set([
151
219
  "EPERM",
152
220
  ]);
153
221
 
154
- function writeOutputTransactionally(output, files, cleanOutput, forceInPlaceOutput = false) {
222
+ function writeOutputTransactionally(
223
+ output,
224
+ files,
225
+ cleanOutput,
226
+ forceInPlaceOutput = false,
227
+ ) {
155
228
  const parent = path.dirname(output);
156
229
  fs.mkdirSync(parent, { recursive: true });
157
- const staging = fs.mkdtempSync(path.join(parent, `.${path.basename(output)}.typedapi-staging-`));
158
- const backup = path.join(parent, `.${path.basename(output)}.typedapi-backup-${process.pid}-${Date.now()}`);
230
+ const staging = fs.mkdtempSync(
231
+ path.join(parent, `.${path.basename(output)}.typedapi-staging-`),
232
+ );
233
+ const backup = path.join(
234
+ parent,
235
+ `.${path.basename(output)}.typedapi-backup-${process.pid}-${Date.now()}`,
236
+ );
159
237
 
160
238
  try {
161
- if (fs.existsSync(output)) fs.cpSync(output, staging, { recursive: true, force: true });
239
+ if (fs.existsSync(output))
240
+ fs.cpSync(output, staging, { recursive: true, force: true });
162
241
  if (cleanOutput) removeManagedOutput(staging);
163
242
 
164
243
  for (const [relativePath, content] of files) {
@@ -212,7 +291,8 @@ function writeOutputTransactionally(output, files, cleanOutput, forceInPlaceOutp
212
291
  return "directory-swap";
213
292
  } finally {
214
293
  removePathWithRetry(staging);
215
- if (fs.existsSync(backup) && fs.existsSync(output)) removePathWithRetry(backup);
294
+ if (fs.existsSync(backup) && fs.existsSync(output))
295
+ removePathWithRetry(backup);
216
296
  }
217
297
  }
218
298
 
@@ -272,7 +352,8 @@ function syncPath(source, target) {
272
352
  syncPath(path.join(source, entry), path.join(target, entry));
273
353
  }
274
354
  for (const entry of fs.readdirSync(target)) {
275
- if (!sourceEntries.has(entry)) removePathWithRetry(path.join(target, entry));
355
+ if (!sourceEntries.has(entry))
356
+ removePathWithRetry(path.join(target, entry));
276
357
  }
277
358
  return;
278
359
  }
@@ -312,7 +393,8 @@ function retryFileSystemOperation(operation, attempts = 12) {
312
393
  return operation();
313
394
  } catch (error) {
314
395
  lastError = error;
315
- if (!isRetryableFileSystemError(error) || attempt === attempts - 1) throw error;
396
+ if (!isRetryableFileSystemError(error) || attempt === attempts - 1)
397
+ throw error;
316
398
  sleepSynchronously(Math.min(25 * 2 ** attempt, 250));
317
399
  }
318
400
  }
@@ -320,7 +402,11 @@ function retryFileSystemOperation(operation, attempts = 12) {
320
402
  }
321
403
 
322
404
  function isRetryableFileSystemError(error) {
323
- return Boolean(error && typeof error === "object" && RETRYABLE_FS_ERROR_CODES.has(error.code));
405
+ return Boolean(
406
+ error &&
407
+ typeof error === "object" &&
408
+ RETRYABLE_FS_ERROR_CODES.has(error.code),
409
+ );
324
410
  }
325
411
 
326
412
  function sleepSynchronously(milliseconds) {