toolcraft-openapi 0.0.57 → 0.0.58

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.
@@ -38,6 +38,7 @@ interface GenerateCliOptions {
38
38
  diff: boolean;
39
39
  input: string;
40
40
  inspect: boolean;
41
+ lockPath: string;
41
42
  outputFormat: OutputFormat;
42
43
  outputDir: string;
43
44
  }
@@ -19,6 +19,7 @@ const DEFAULT_OPTIONS = {
19
19
  diff: false,
20
20
  input: "openapi.json",
21
21
  inspect: false,
22
+ lockPath: "openapi.lock",
22
23
  outputFormat: "terminal",
23
24
  outputDir: "src/generated"
24
25
  };
@@ -27,6 +28,7 @@ const HELP_TEXT = `Usage: toolcraft-openapi-generate [options]
27
28
  Options:
28
29
  --input <path-or-url> OpenAPI document to read (default: openapi.json)
29
30
  --output <dir> Directory for generated command files (default: src/generated)
31
+ --lock <path> Lock file path (default: openapi.lock)
30
32
  --check Exit non-zero if generated output would change
31
33
  --diff Print a diff of generated changes without writing files
32
34
  --inspect Inspect route compatibility without writing files
@@ -113,18 +115,27 @@ export async function syncGeneratedClient(options, services) {
113
115
  const effectiveConfig = hasErrorDiagnostics(diagnostics) ? undefined : configResult.config;
114
116
  const generatedFiles = generate(document, { specSha, config: effectiveConfig });
115
117
  const outputDir = path.resolve(services.cwd, options.outputDir);
118
+ const lockPath = path.resolve(services.cwd, options.lockPath);
119
+ const currentLockContents = await readOpenApiLockText(services.fs, lockPath);
120
+ const desiredLockContents = stringifyOpenApiLock({ specSha });
116
121
  const currentFiles = await readGeneratedFiles(services.fs, outputDir);
117
122
  const desiredFiles = new Map([
118
123
  ...generatedFiles.map((file) => [path.resolve(outputDir, file.path), file.contents]),
119
124
  ...createDownloadedSpecFiles(options.input, sourceText).map((file) => [path.resolve(outputDir, file.path), file.contents])
120
125
  ]);
121
126
  const updatedFiles = collectUpdatedFiles(currentFiles, desiredFiles);
127
+ const updatedLockFile = currentLockContents === desiredLockContents
128
+ ? undefined
129
+ : { path: lockPath, contents: desiredLockContents, previousContents: currentLockContents };
122
130
  const deletedFiles = collectDeletedFiles(currentFiles, desiredFiles);
123
- const drifted = updatedFiles.length > 0 || deletedFiles.length > 0;
131
+ const drifted = updatedFiles.length > 0 || updatedLockFile !== undefined || deletedFiles.length > 0;
124
132
  if (!options.check && !options.diff && drifted && !hasErrorDiagnostics(diagnostics)) {
125
133
  try {
126
134
  await writeGeneratedFiles(services.fs, outputDir, updatedFiles);
127
135
  await deleteGeneratedFiles(services.fs, outputDir, deletedFiles);
136
+ if (updatedLockFile !== undefined) {
137
+ await writeOpenApiLock(services.fs, lockPath, { specSha });
138
+ }
128
139
  }
129
140
  catch (error) {
130
141
  await restoreGeneratedFiles(services.fs, outputDir, currentFiles, updatedFiles, deletedFiles);
@@ -137,8 +148,8 @@ export async function syncGeneratedClient(options, services) {
137
148
  diagnostics,
138
149
  drifted,
139
150
  specSha,
140
- updatedFiles,
141
- updatedFileCount: updatedFiles.length
151
+ updatedFiles: updatedLockFile === undefined ? updatedFiles : [...updatedFiles, updatedLockFile],
152
+ updatedFileCount: updatedFiles.length + (updatedLockFile === undefined ? 0 : 1)
142
153
  };
143
154
  }
144
155
  async function readAdjacentToolcraftConfig(input, services) {
@@ -243,7 +254,10 @@ function parseGenerateCliArgs(argv) {
243
254
  options.inspect = true;
244
255
  continue;
245
256
  }
246
- if (argument === "--input" || argument === "--output" || argument === "--output-format") {
257
+ if (argument === "--input" ||
258
+ argument === "--output" ||
259
+ argument === "--lock" ||
260
+ argument === "--output-format") {
247
261
  const value = argv[index + 1];
248
262
  if (value === undefined) {
249
263
  throw new UserError(`Missing value for ${JSON.stringify(argument)}.`);
@@ -260,6 +274,10 @@ function parseGenerateCliArgs(argv) {
260
274
  assignOptionValue(options, "--output", argument.slice("--output=".length));
261
275
  continue;
262
276
  }
277
+ if (argument.startsWith("--lock=")) {
278
+ assignOptionValue(options, "--lock", argument.slice("--lock=".length));
279
+ continue;
280
+ }
263
281
  if (argument.startsWith("--output-format=")) {
264
282
  assignOptionValue(options, "--output-format", argument.slice("--output-format=".length));
265
283
  continue;
@@ -283,8 +301,57 @@ function assignOptionValue(options, argument, value) {
283
301
  options.outputFormat = value;
284
302
  return;
285
303
  }
304
+ if (argument === "--lock") {
305
+ options.lockPath = value;
306
+ return;
307
+ }
286
308
  options.outputDir = value;
287
309
  }
310
+ async function readOpenApiLockText(fs, lockPath) {
311
+ try {
312
+ const contents = await fs.readFile(lockPath, "utf8");
313
+ parseOpenApiLock(contents, lockPath);
314
+ return contents;
315
+ }
316
+ catch (error) {
317
+ if (isNotFoundError(error)) {
318
+ return undefined;
319
+ }
320
+ throw error;
321
+ }
322
+ }
323
+ function parseOpenApiLock(contents, lockPath) {
324
+ let parsed;
325
+ try {
326
+ parsed = JSON.parse(contents);
327
+ }
328
+ catch (error) {
329
+ throw new UserError(`Lock file ${JSON.stringify(lockPath)} is not valid JSON: ${getErrorMessage(error)}.`, { cause: error });
330
+ }
331
+ if (typeof parsed !== "object" ||
332
+ parsed === null ||
333
+ !("version" in parsed) ||
334
+ parsed.version !== 1 ||
335
+ !("specSha" in parsed) ||
336
+ typeof parsed.specSha !== "string" ||
337
+ parsed.specSha.length === 0) {
338
+ return null;
339
+ }
340
+ return { specSha: parsed.specSha };
341
+ }
342
+ function stringifyOpenApiLock(lock) {
343
+ return `${JSON.stringify({ version: 1, specSha: lock.specSha }, null, 2)}\n`;
344
+ }
345
+ async function writeOpenApiLock(fs, lockPath, lock) {
346
+ await fs.mkdir(path.dirname(lockPath), { recursive: true });
347
+ try {
348
+ await fs.writeFile(lockPath, stringifyOpenApiLock(lock), "utf8");
349
+ }
350
+ catch (error) {
351
+ const code = getErrorCode(error);
352
+ throw new UserError(`Failed to write lock file ${JSON.stringify(lockPath)}${code === undefined ? "" : ` (${code})`}: ${getErrorMessage(error)}`, { cause: error });
353
+ }
354
+ }
288
355
  function createSpecSha(sourceText) {
289
356
  return `sha256:${createHash("sha256").update(sourceText).digest("hex")}`;
290
357
  }
@@ -446,6 +513,15 @@ function isNotFoundError(error) {
446
513
  function isAlreadyExistsError(error) {
447
514
  return hasOwnErrorCode(error, "EEXIST");
448
515
  }
516
+ function getErrorCode(error) {
517
+ if (typeof error !== "object" || error === null || !("code" in error)) {
518
+ return undefined;
519
+ }
520
+ return typeof error.code === "string" ? error.code : undefined;
521
+ }
522
+ function getErrorMessage(error) {
523
+ return error instanceof Error ? error.message : String(error);
524
+ }
449
525
  function isDirectExecution(moduleUrl, argv) {
450
526
  const entryPoint = argv[1];
451
527
  if (entryPoint === undefined) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolcraft-openapi",
3
- "version": "0.0.57",
3
+ "version": "0.0.58",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -28,7 +28,7 @@
28
28
  "toolcraft-openapi-generate": "dist/bin/generate.js"
29
29
  },
30
30
  "dependencies": {
31
- "toolcraft": "0.0.57",
31
+ "toolcraft": "0.0.58",
32
32
  "auth-store": "^0.0.1",
33
33
  "yaml": "^2.8.2"
34
34
  },