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
|
@@ -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 {
|
|
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))
|
|
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
|
-
() =>
|
|
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(
|
|
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(
|
|
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)
|
|
564
|
+
if (verboseMode)
|
|
565
|
+
console.log("Skipping missing default handler file in check mode.");
|
|
531
566
|
return undefined;
|
|
532
567
|
}
|
|
533
|
-
const defaultFunctionsFile = createDefaultFunctionsFileIfMissing(
|
|
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(
|
|
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 {
|
|
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(
|
|
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) =>
|
|
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 {
|
|
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)
|
|
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) =>
|
|
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(
|
|
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(
|
|
158
|
-
|
|
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))
|
|
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))
|
|
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))
|
|
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)
|
|
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(
|
|
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) {
|