openapi-typesafe-migrate 1.0.0-rc.1 → 1.0.0-rc.3

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/cli/main.mjs CHANGED
@@ -1,15 +1,96 @@
1
- import { mkdirSync, appendFileSync, readFileSync } from 'node:fs';
1
+ import { mkdtempSync, writeFileSync, mkdirSync, appendFileSync, readFileSync } from 'node:fs';
2
2
  import { tmpdir } from 'node:os';
3
- import { dirname, join, resolve } from 'node:path';
4
- import { readFile, writeFile, readdir } from 'node:fs/promises';
3
+ import { join, dirname, basename, isAbsolute, relative, sep, resolve } from 'node:path';
4
+ import { spawn } from 'node:child_process';
5
+ import { realpath, readFile, lstat, writeFile, chmod, rename, rm, readdir } from 'node:fs/promises';
5
6
  import ts from '@typescript/typescript6';
6
7
  import { parseArgs } from 'node:util';
7
8
 
9
+ const LS_FILES_ARGUMENTS = [
10
+ "ls-files",
11
+ "--cached",
12
+ "--others",
13
+ "--exclude-standard",
14
+ "--deduplicate",
15
+ "-z"
16
+ ];
17
+ const CHECK_IGNORE_ARGUMENTS = ["check-ignore", "--quiet", "."];
18
+ const IGNORED_EXIT_CODE = 0;
19
+ const NOT_IGNORED_EXIT_CODE = 1;
20
+ const IGNORED_REASON = "the git repository around the folder ignores it";
21
+ const GIT_COMMAND = "git";
22
+ const HARDENED_CONFIG = [
23
+ "-c",
24
+ "core.fsmonitor=false",
25
+ "-c",
26
+ "safe.bareRepository=explicit"
27
+ ];
28
+ const TRAILING_SLASH_RE = /\/$/;
29
+ const SELF_PATHS = /* @__PURE__ */ new Set(["", "."]);
30
+ function runGit(directory, gitArguments) {
31
+ return new Promise((resolve) => {
32
+ const child = spawn(GIT_COMMAND, [...HARDENED_CONFIG, ...gitArguments], { cwd: directory });
33
+ const output = [];
34
+ const errors = [];
35
+ child.stdout.on("data", (chunk) => output.push(chunk));
36
+ child.stderr.on("data", (chunk) => errors.push(chunk));
37
+ child.on("error", (error) => resolve({ kind: "failed", reason: error.message }));
38
+ child.on("close", (exitCode) => {
39
+ resolve({
40
+ kind: "exited",
41
+ exitCode,
42
+ output: Buffer.concat(output).toString("utf8"),
43
+ errors: Buffer.concat(errors).toString("utf8").trim()
44
+ });
45
+ });
46
+ });
47
+ }
48
+ function failureReason(run) {
49
+ if (run.kind === "failed") {
50
+ return run.reason;
51
+ }
52
+ return run.errors || `exit code ${run.exitCode}`;
53
+ }
54
+ function listedPaths$1(output) {
55
+ return output.split("\0").map((path) => path.replace(TRAILING_SLASH_RE, "")).filter((path) => !SELF_PATHS.has(path));
56
+ }
57
+ async function listDirectory(directory) {
58
+ const ignored = await runGit(directory, CHECK_IGNORE_ARGUMENTS);
59
+ if (ignored.kind === "exited" && ignored.exitCode === IGNORED_EXIT_CODE) {
60
+ return { kind: "unavailable", reason: IGNORED_REASON };
61
+ }
62
+ if (ignored.kind === "failed" || ignored.exitCode !== NOT_IGNORED_EXIT_CODE) {
63
+ return { kind: "unavailable", reason: failureReason(ignored) };
64
+ }
65
+ const listed = await runGit(directory, LS_FILES_ARGUMENTS);
66
+ if (listed.kind === "failed" || listed.exitCode !== 0) {
67
+ return { kind: "unavailable", reason: failureReason(listed) };
68
+ }
69
+ return { kind: "listed", paths: listedPaths$1(listed.output) };
70
+ }
71
+ class SpawnedGitFileList {
72
+ list(directory) {
73
+ return listDirectory(directory);
74
+ }
75
+ }
76
+ function createGitFileList() {
77
+ return new SpawnedGitFileList();
78
+ }
79
+
80
+ const LOG_FILE_MODE = 384;
81
+ const TEMPORARY_FOLDER_PREFIX = "openapi-typesafe-migrate-";
82
+ const LOG_EXTENSION = ".jsonl";
83
+ const TIME_SEPARATOR = ":";
84
+ const FILE_NAME_TIME_SEPARATOR = "-";
8
85
  class JsonLinesLogger {
9
86
  #options;
10
87
  constructor(options) {
11
88
  this.#options = options;
12
- mkdirSync(dirname(options.path), { recursive: true });
89
+ if (options.isExclusive) {
90
+ writeFileSync(options.path, "", { flag: "wx", mode: LOG_FILE_MODE });
91
+ } else {
92
+ mkdirSync(dirname(options.path), { recursive: true });
93
+ }
13
94
  }
14
95
  info(event, fields = {}) {
15
96
  this.#append("info", event, fields);
@@ -19,13 +100,19 @@ class JsonLinesLogger {
19
100
  }
20
101
  #append(level, event, fields) {
21
102
  const time = this.#options.now().toISOString();
22
- appendFileSync(this.#options.path, `${JSON.stringify({ time, level, event, ...fields })}
23
- `);
103
+ const line = `${JSON.stringify({ time, level, event, ...fields })}
104
+ `;
105
+ appendFileSync(this.#options.path, line, { mode: LOG_FILE_MODE });
24
106
  }
25
107
  }
26
108
  function createJsonLinesLogger(options) {
27
109
  return new JsonLinesLogger(options);
28
110
  }
111
+ function createTemporaryLogPath(parent, startTime) {
112
+ const folder = mkdtempSync(join(parent, TEMPORARY_FOLDER_PREFIX));
113
+ const name = startTime.toISOString().replaceAll(TIME_SEPARATOR, FILE_NAME_TIME_SEPARATOR);
114
+ return join(folder, `${name}${LOG_EXTENSION}`);
115
+ }
29
116
 
30
117
  const SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set([
31
118
  "node_modules",
@@ -38,11 +125,19 @@ const SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set([
38
125
  ".turbo",
39
126
  "coverage"
40
127
  ]);
41
- async function listTree(root, relative) {
42
- const entries = await readdir(join(root, relative), { withFileTypes: true });
128
+ const CURRENT_DIRECTORY = ".";
129
+ const TEMPORARY_SUFFIX = ".openapi-typesafe-migrate.tmp";
130
+ const PERMISSION_BITS = 4095;
131
+ const TEMPORARY_MODE = 384;
132
+ const PARENT_DIRECTORY = "..";
133
+ function isInSkippedDirectory(path) {
134
+ return path.split("/").slice(0, -1).some((directory) => SKIPPED_DIRECTORIES.has(directory));
135
+ }
136
+ async function listTree(root, relative2) {
137
+ const entries = await readdir(join(root, relative2), { withFileTypes: true });
43
138
  const nested = await Promise.all(
44
139
  entries.map((entry) => {
45
- const path = relative === "" ? entry.name : `${relative}/${entry.name}`;
140
+ const path = relative2 === "" ? entry.name : `${relative2}/${entry.name}`;
46
141
  if (entry.isDirectory()) {
47
142
  return SKIPPED_DIRECTORIES.has(entry.name) ? [] : listTree(root, path);
48
143
  }
@@ -51,23 +146,134 @@ async function listTree(root, relative) {
51
146
  );
52
147
  return nested.flat();
53
148
  }
149
+ function isRelativeDownward(path) {
150
+ return !isAbsolute(path) && !path.split("/").includes(PARENT_DIRECTORY);
151
+ }
152
+ function isInFolder(folder, target) {
153
+ const path = relative(folder, target);
154
+ return path !== "" && path !== PARENT_DIRECTORY && !path.startsWith(`${PARENT_DIRECTORY}${sep}`) && !isAbsolute(path);
155
+ }
156
+ async function entryAt(root, realRoot, path) {
157
+ if (!isRelativeDownward(path)) {
158
+ return "outside";
159
+ }
160
+ try {
161
+ const stats = await lstat(join(root, path));
162
+ if (!stats.isFile() && !stats.isDirectory()) {
163
+ return "other";
164
+ }
165
+ if (!isInFolder(realRoot, await realpath(join(root, path)))) {
166
+ return "outside";
167
+ }
168
+ return stats.isFile() ? "file" : "directory";
169
+ } catch (error) {
170
+ if (error.code === "ENOENT") {
171
+ return "other";
172
+ }
173
+ throw error;
174
+ }
175
+ }
176
+ async function listedPaths(root, realRoot, paths) {
177
+ const candidates = paths.filter((path) => !isInSkippedDirectory(path));
178
+ const entries = await Promise.all(candidates.map((path) => entryAt(root, realRoot, path)));
179
+ const withEntry = (entry) => candidates.filter((_path, index) => entries[index] === entry);
180
+ return {
181
+ files: withEntry("file"),
182
+ directories: withEntry("directory").filter((path) => !SKIPPED_DIRECTORIES.has(baseName$1(path))),
183
+ skipped: withEntry("outside").map((path) => ({ path, reason: "outside-root" }))
184
+ };
185
+ }
186
+ function baseName$1(path) {
187
+ return path.slice(path.lastIndexOf("/") + 1);
188
+ }
189
+ function joinRelative(directory, path) {
190
+ return directory === "" ? path : `${directory}/${path}`;
191
+ }
192
+ const NO_FILES = { files: [], skipped: [] };
193
+ function joinLists(lists) {
194
+ return lists.reduce(
195
+ (joined, list) => ({
196
+ files: [...joined.files, ...list.files],
197
+ skipped: [...joined.skipped, ...list.skipped]
198
+ }),
199
+ NO_FILES
200
+ );
201
+ }
54
202
  class NodeProjectFiles {
55
203
  #root;
56
- constructor(root) {
204
+ #options;
205
+ constructor(root, options) {
57
206
  this.#root = root;
207
+ this.#options = options;
58
208
  }
59
- listFiles() {
60
- return listTree(this.#root, "");
61
- }
62
- read(path) {
63
- return readFile(join(this.#root, path), "utf8");
209
+ /** Git's view of the project when it is in a git repository, so ignored files stay out. */
210
+ async listFiles() {
211
+ return this.#listDirectory(await realpath(this.#root), "");
212
+ }
213
+ /**
214
+ * One directory, by the git repository it is in. Git lists a submodule or a nested repository
215
+ * as one entry, so each of those is listed the same way, by its own repository and ignore rules.
216
+ */
217
+ async #listDirectory(realRoot, directory) {
218
+ const { git, logger } = this.#options;
219
+ const listing = await git.list(join(this.#root, directory));
220
+ const logged = { directory: directory === "" ? CURRENT_DIRECTORY : directory };
221
+ if (listing.kind === "unavailable") {
222
+ const files2 = await listTree(this.#root, directory);
223
+ logger.info("files_listed", {
224
+ ...logged,
225
+ source: "tree",
226
+ fileCount: files2.length,
227
+ reason: listing.reason
228
+ });
229
+ return { files: files2, skipped: [] };
230
+ }
231
+ const paths = listing.paths.map((path) => joinRelative(directory, path));
232
+ const { files, directories, skipped } = await listedPaths(this.#root, realRoot, paths);
233
+ logger.info("files_listed", { ...logged, source: "git", fileCount: files.length });
234
+ for (const file of skipped) {
235
+ logger.warn("file_skipped", { ...file });
236
+ }
237
+ const nested = await Promise.all(
238
+ directories.map((path) => this.#listDirectory(realRoot, path))
239
+ );
240
+ return joinLists([{ files, skipped }, ...nested]);
241
+ }
242
+ async read(path) {
243
+ return readFile(await this.#pathInRoot(path), "utf8");
244
+ }
245
+ /**
246
+ * Writes a new file and renames it over the old one, so the write never goes through a link: a
247
+ * hard link to a file outside the root keeps its old text, and is reported in the log. The new
248
+ * file keeps the mode of the old one.
249
+ */
250
+ async write(path, text) {
251
+ const target = await this.#pathInRoot(path);
252
+ const { mode, nlink } = await lstat(target);
253
+ const temporary = join(dirname(target), `.${basename(target)}${TEMPORARY_SUFFIX}`);
254
+ await writeFile(temporary, text, { encoding: "utf8", flag: "wx", mode: TEMPORARY_MODE });
255
+ try {
256
+ await chmod(temporary, mode & PERMISSION_BITS);
257
+ await rename(temporary, target);
258
+ } catch (error) {
259
+ await rm(temporary, { force: true });
260
+ throw error;
261
+ }
262
+ if (nlink > 1) {
263
+ this.#options.logger.warn("hard_link_separated", { path, linkCount: nlink });
264
+ }
64
265
  }
65
- write(path, text) {
66
- return writeFile(join(this.#root, path), text, "utf8");
266
+ /** The real path of a project file; refuses one that resolves outside the root. */
267
+ async #pathInRoot(path) {
268
+ const realPath = isRelativeDownward(path) ? await realpath(join(this.#root, path)) : void 0;
269
+ if (realPath === void 0 || !isInFolder(await realpath(this.#root), realPath)) {
270
+ throw new Error(`${path} is outside the project folder ${this.#root}`);
271
+ }
272
+ return realPath;
67
273
  }
68
274
  }
69
- function createNodeProjectFiles(root) {
70
- return new NodeProjectFiles(root);
275
+ function createNodeProjectFiles(root, options) {
276
+ return new NodeProjectFiles(root, options);
71
277
  }
72
278
 
73
279
  function unchanged(text) {
@@ -157,6 +363,85 @@ function collectFileNames(file) {
157
363
  return names;
158
364
  }
159
365
 
366
+ const SCRIPT_KINDS = /* @__PURE__ */ new Map([
367
+ [".ts", ts.ScriptKind.TS],
368
+ [".mts", ts.ScriptKind.TS],
369
+ [".cts", ts.ScriptKind.TS],
370
+ [".tsx", ts.ScriptKind.TSX],
371
+ [".js", ts.ScriptKind.JS],
372
+ [".mjs", ts.ScriptKind.JS],
373
+ [".cjs", ts.ScriptKind.JS],
374
+ [".jsx", ts.ScriptKind.JSX]
375
+ ]);
376
+ const ASTRO_EXTENSION = ".astro";
377
+ const SCRIPT_OPEN_TAG = "<script";
378
+ const TAG_END = ">";
379
+ const ASTRO_FRONTMATTER_RE = /^(\s*---\r?\n)([\s\S]*?)\r?\n---/;
380
+ const LANG_RE = /\blang\s*=\s*["']?([a-z]+)/i;
381
+ const LANG_KINDS = /* @__PURE__ */ new Map([
382
+ ["ts", ts.ScriptKind.TS],
383
+ ["typescript", ts.ScriptKind.TS],
384
+ ["tsx", ts.ScriptKind.TSX],
385
+ ["jsx", ts.ScriptKind.JSX]
386
+ ]);
387
+ function extensionOf$1(path) {
388
+ return path.slice(path.lastIndexOf("."));
389
+ }
390
+ function plainScriptKind(path) {
391
+ return SCRIPT_KINDS.get(extensionOf$1(path));
392
+ }
393
+ function blockKind(attributes, isAstro) {
394
+ const lang = LANG_RE.exec(attributes)?.[1]?.toLowerCase();
395
+ const fallback = isAstro ? ts.ScriptKind.TS : ts.ScriptKind.JS;
396
+ return (lang === void 0 ? void 0 : LANG_KINDS.get(lang)) ?? fallback;
397
+ }
398
+ function astroFrontmatter(text) {
399
+ const frontmatter = ASTRO_FRONTMATTER_RE.exec(text);
400
+ if (frontmatter === null) {
401
+ return { markupStart: 0 };
402
+ }
403
+ const [whole, fence, content] = frontmatter;
404
+ const region = { offset: fence.length, text: content, kind: ts.ScriptKind.TS };
405
+ return { region, markupStart: whole.length };
406
+ }
407
+ function scriptBlocks(markup) {
408
+ const openTag = /<script\b/gi;
409
+ const closeTag = /<\/script\s*>/gi;
410
+ const blocks = [];
411
+ for (let opening = openTag.exec(markup); opening !== null; opening = openTag.exec(markup)) {
412
+ const tagEnd = markup.indexOf(TAG_END, opening.index);
413
+ closeTag.lastIndex = tagEnd + 1;
414
+ const closing = tagEnd === -1 ? null : closeTag.exec(markup);
415
+ if (closing === null) {
416
+ break;
417
+ }
418
+ const attributes = markup.slice(opening.index + SCRIPT_OPEN_TAG.length, tagEnd);
419
+ blocks.push({
420
+ attributes,
421
+ contentStart: tagEnd + 1,
422
+ content: markup.slice(tagEnd + 1, closing.index)
423
+ });
424
+ openTag.lastIndex = closeTag.lastIndex;
425
+ }
426
+ return blocks;
427
+ }
428
+ function componentRegions(text, isAstro) {
429
+ const { region, markupStart } = isAstro ? astroFrontmatter(text) : { markupStart: 0 };
430
+ const regions = region === void 0 ? [] : [region];
431
+ for (const block of scriptBlocks(text.slice(markupStart))) {
432
+ const offset = markupStart + block.contentStart;
433
+ regions.push({ offset, text: block.content, kind: blockKind(block.attributes, isAstro) });
434
+ }
435
+ return regions;
436
+ }
437
+ function scriptRegions(text, path) {
438
+ const kind = plainScriptKind(path);
439
+ if (kind !== void 0) {
440
+ return [{ offset: 0, text, kind }];
441
+ }
442
+ return componentRegions(text, extensionOf$1(path) === ASTRO_EXTENSION);
443
+ }
444
+
160
445
  const KEYWORDS = /* @__PURE__ */ new Map([
161
446
  ["AnyKeyword", "any"],
162
447
  ["BigIntKeyword", "bigint"],
@@ -362,19 +647,6 @@ function returnEdits(expression, names) {
362
647
  const TRANSFORM = "transform";
363
648
  const LEGACY_ONLY_CALLBACKS = ["postTransform", "transformProperty"];
364
649
  const IR_CALLBACKS_OPTION = "irCallbacks";
365
- const SCRIPT_KINDS = /* @__PURE__ */ new Map([
366
- [".ts", ts.ScriptKind.TS],
367
- [".mts", ts.ScriptKind.TS],
368
- [".cts", ts.ScriptKind.TS],
369
- [".tsx", ts.ScriptKind.TSX],
370
- [".js", ts.ScriptKind.JS],
371
- [".mjs", ts.ScriptKind.JS],
372
- [".cjs", ts.ScriptKind.JS],
373
- [".jsx", ts.ScriptKind.JSX]
374
- ]);
375
- function scriptKindOf(path) {
376
- return SCRIPT_KINDS.get(path.slice(path.lastIndexOf(".")));
377
- }
378
650
  function propertyNamed(options, name) {
379
651
  return options.properties.find((property) => {
380
652
  const key = property.name;
@@ -500,7 +772,7 @@ function findCallFindings(file, text, names) {
500
772
  }
501
773
  class TypescriptCallbackRewriter {
502
774
  rewrite(text, path) {
503
- const scriptKind = scriptKindOf(path);
775
+ const scriptKind = plainScriptKind(path);
504
776
  if (scriptKind === void 0) {
505
777
  return unchanged(text);
506
778
  }
@@ -517,6 +789,149 @@ function createTypescriptCallbackRewriter() {
517
789
  return new TypescriptCallbackRewriter();
518
790
  }
519
791
 
792
+ const REQUIRE = "require";
793
+ const RESOLVE = "resolve";
794
+ const VITEST_METHODS = /* @__PURE__ */ new Set([
795
+ "mock",
796
+ "doMock",
797
+ "unmock",
798
+ "doUnmock",
799
+ "importActual",
800
+ "importMock"
801
+ ]);
802
+ const JEST_METHODS = /* @__PURE__ */ new Set([
803
+ "mock",
804
+ "doMock",
805
+ "unmock",
806
+ "dontMock",
807
+ "deepUnmock",
808
+ "setMock",
809
+ "requireActual",
810
+ "requireMock",
811
+ "createMockFromModule",
812
+ "genMockFromModule",
813
+ "unstable_mockModule",
814
+ "unstable_unmockModule"
815
+ ]);
816
+ const MODULE_METHODS = /* @__PURE__ */ new Map([
817
+ ["vi", VITEST_METHODS],
818
+ ["jest", JEST_METHODS],
819
+ ["mock", /* @__PURE__ */ new Set(["module"])],
820
+ [REQUIRE, /* @__PURE__ */ new Set([RESOLVE])]
821
+ ]);
822
+ function isIdentifierNamed(node, name) {
823
+ return ts.isIdentifier(node) && node.text === name;
824
+ }
825
+ function isModuleMethod(callee) {
826
+ const object = callee.expression;
827
+ const method = callee.name.text;
828
+ if (ts.isIdentifier(object)) {
829
+ return MODULE_METHODS.get(object.text)?.has(method) ?? false;
830
+ }
831
+ return method === RESOLVE && ts.isMetaProperty(object);
832
+ }
833
+ function isModuleCall(call) {
834
+ const callee = call.expression;
835
+ if (callee.kind === ts.SyntaxKind.ImportKeyword || isIdentifierNamed(callee, REQUIRE)) {
836
+ return true;
837
+ }
838
+ return ts.isPropertyAccessExpression(callee) && isModuleMethod(callee);
839
+ }
840
+ function isModuleSpecifier(literal) {
841
+ const parent = literal.parent;
842
+ if (ts.isImportDeclaration(parent) || ts.isExportDeclaration(parent) || ts.isJSDocImportTag(parent)) {
843
+ return parent.moduleSpecifier === literal;
844
+ }
845
+ if (ts.isCallExpression(parent)) {
846
+ return parent.arguments[0] === literal && isModuleCall(parent);
847
+ }
848
+ if (ts.isModuleDeclaration(parent)) {
849
+ return parent.name === literal;
850
+ }
851
+ if (ts.isLiteralTypeNode(parent)) {
852
+ return ts.isImportTypeNode(parent.parent);
853
+ }
854
+ return ts.isExternalModuleReference(parent);
855
+ }
856
+ function collectStrings(root, file, offset, found) {
857
+ const visit = (node) => {
858
+ if (ts.isStringLiteralLike(node)) {
859
+ const start = offset + node.getStart(file) + 1;
860
+ const end = offset + node.getEnd() - 1;
861
+ found.set(start, {
862
+ start,
863
+ end,
864
+ value: node.text,
865
+ isModuleSpecifier: isModuleSpecifier(node)
866
+ });
867
+ }
868
+ ts.forEachChild(node, visit);
869
+ };
870
+ visit(root);
871
+ }
872
+ function commentRanges(file) {
873
+ const ranges = /* @__PURE__ */ new Map();
874
+ const add = (found) => {
875
+ for (const range of found ?? []) {
876
+ ranges.set(range.pos, range);
877
+ }
878
+ };
879
+ const visit = (node) => {
880
+ if (node.kind === ts.SyntaxKind.JsxText) {
881
+ return;
882
+ }
883
+ const children = node.getChildren(file).filter((child) => !ts.isJSDoc(child));
884
+ if (children.length === 0) {
885
+ add(ts.getTrailingCommentRanges(file.text, node.pos));
886
+ add(ts.getLeadingCommentRanges(file.text, node.pos));
887
+ }
888
+ children.forEach(visit);
889
+ };
890
+ visit(file);
891
+ return [...ranges.values()];
892
+ }
893
+ const JSDOC_START = "/**";
894
+ const EMPTY_BLOCK_COMMENT = "/**/";
895
+ const JSDOC_HOST = "\nvar host;";
896
+ const JSDOC_FILE_NAME = "jsdoc.js";
897
+ function collectJsDocStrings(comment, offset, found) {
898
+ const text = `${comment}${JSDOC_HOST}`;
899
+ const file = ts.createSourceFile(JSDOC_FILE_NAME, text, ts.ScriptTarget.Latest, true);
900
+ const host = file.statements[0];
901
+ for (const doc of ts.getJSDocCommentsAndTags(host)) {
902
+ collectStrings(doc, file, offset, found);
903
+ }
904
+ }
905
+ function literalStrings(file, offset) {
906
+ const found = /* @__PURE__ */ new Map();
907
+ collectStrings(file, file, offset, found);
908
+ for (const range of commentRanges(file)) {
909
+ const comment = file.text.slice(range.pos, range.end);
910
+ if (comment.startsWith(JSDOC_START) && comment !== EMPTY_BLOCK_COMMENT) {
911
+ collectJsDocStrings(comment, offset + range.pos, found);
912
+ }
913
+ }
914
+ return found;
915
+ }
916
+ function regionStrings(region, path) {
917
+ const file = ts.createSourceFile(path, region.text, ts.ScriptTarget.Latest, true, region.kind);
918
+ const found = literalStrings(file, region.offset);
919
+ for (const reference of file.typeReferenceDirectives) {
920
+ const start = region.offset + reference.pos;
921
+ const end = region.offset + reference.end;
922
+ found.set(start, { start, end, value: reference.fileName, isModuleSpecifier: true });
923
+ }
924
+ return [...found.values()];
925
+ }
926
+ class TypescriptSourceStringScanner {
927
+ scan(text, path) {
928
+ return scriptRegions(text, path).flatMap((region) => regionStrings(region, path)).sort((left, right) => left.start - right.start);
929
+ }
930
+ }
931
+ function createTypescriptStringScanner() {
932
+ return new TypescriptSourceStringScanner();
933
+ }
934
+
520
935
  const PACKAGE_RENAMES = /* @__PURE__ */ new Map([
521
936
  ["openapi-typescript", "openapi-typesafe"],
522
937
  ["openapi-fetch", "openapi-typesafe-fetch"],
@@ -539,6 +954,9 @@ const DEPENDENCY_FIELDS = [
539
954
  "optionalDependencies"
540
955
  ];
541
956
  const OVERRIDE_FIELDS = ["overrides", "resolutions", "pnpm"];
957
+ const OLD_PEER_DEPENDENTS = /* @__PURE__ */ new Map([
958
+ ["swr-openapi", { peer: "openapi-fetch", range: ">=0.15" }]
959
+ ]);
542
960
 
543
961
  const COMMAND_RE = new RegExp(`(?<=^|[\\s"'\`;&|(])${GENERATOR_COMMAND}(?=$|[\\s"'\`;&|)])`, "g");
544
962
  const VERSIONED_COMMAND_RE = new RegExp(`(?<=^|[\\s"'\`;&|(])${GENERATOR_COMMAND}@`);
@@ -602,6 +1020,35 @@ function overriddenPackages(value) {
602
1020
  return [...new Set(targets.filter((target) => PACKAGE_RENAMES.has(target)))];
603
1021
  }
604
1022
 
1023
+ const SCHEME_SEPARATOR = "://";
1024
+ const HIDDEN_USERINFO = "***";
1025
+ const USERINFO_END = "@";
1026
+ const AUTHORITY_END_RE = /[/?#"'`\s]/;
1027
+ function scanAuthority(text, start) {
1028
+ let userinfoEnd = -1;
1029
+ let end = start;
1030
+ for (; end < text.length && !AUTHORITY_END_RE.test(text.charAt(end)); end++) {
1031
+ if (text.charAt(end) === USERINFO_END) {
1032
+ userinfoEnd = end;
1033
+ }
1034
+ }
1035
+ return { end, userinfoEnd };
1036
+ }
1037
+ function hideUrlCredentials(text) {
1038
+ let hidden = "";
1039
+ let copiedUpTo = 0;
1040
+ for (let found = text.indexOf(SCHEME_SEPARATOR); found !== -1; ) {
1041
+ const start = found + SCHEME_SEPARATOR.length;
1042
+ const { end, userinfoEnd } = scanAuthority(text, start);
1043
+ if (userinfoEnd !== -1) {
1044
+ hidden += `${text.slice(copiedUpTo, start)}${HIDDEN_USERINFO}`;
1045
+ copiedUpTo = userinfoEnd;
1046
+ }
1047
+ found = text.indexOf(SCHEME_SEPARATOR, end);
1048
+ }
1049
+ return `${hidden}${text.slice(copiedUpTo)}`;
1050
+ }
1051
+
605
1052
  const VERSION_STYLE_RE = /^([\^~]|>=)?\d/;
606
1053
  const KEPT_PROTOCOLS = ["catalog:", "workspace:"];
607
1054
  function migrateVersionSpec(spec, version) {
@@ -618,13 +1065,16 @@ function migrateVersionSpec(spec, version) {
618
1065
 
619
1066
  const INDENT_RE = /^[ \t]+(?=")/m;
620
1067
  const DEFAULT_INDENT = " ";
1068
+ const LF = "\n";
1069
+ const CRLF = "\r\n";
621
1070
  function isObject(value) {
622
1071
  return typeof value === "object" && value !== null && !Array.isArray(value);
623
1072
  }
624
1073
  function formatLike(original, manifest) {
625
1074
  const indent = INDENT_RE.exec(original)?.[0] ?? DEFAULT_INDENT;
626
- const ending = original.endsWith("\n") ? "\n" : "";
627
- return `${JSON.stringify(manifest, null, indent)}${ending}`;
1075
+ const lineEnding = original.includes(CRLF) ? CRLF : LF;
1076
+ const finalNewline = original.endsWith(LF) ? lineEnding : "";
1077
+ return `${JSON.stringify(manifest, null, indent).replaceAll(LF, lineEnding)}${finalNewline}`;
628
1078
  }
629
1079
  function renameDependencies(field, dependencies, version, findings) {
630
1080
  return Object.fromEntries(
@@ -639,10 +1089,14 @@ function renameDependencies(field, dependencies, version, findings) {
639
1089
  field,
640
1090
  from: name,
641
1091
  to: renamed,
642
- spec: decision.spec
1092
+ spec: hideUrlCredentials(decision.spec)
643
1093
  });
644
1094
  if (decision.kind === "kept" && decision.isReported) {
645
- findings.manualSteps.push({ kind: "spec-kept", name: renamed, spec });
1095
+ findings.manualSteps.push({
1096
+ kind: "spec-kept",
1097
+ name: renamed,
1098
+ spec: hideUrlCredentials(spec)
1099
+ });
646
1100
  }
647
1101
  if (field === "peerDependencies") {
648
1102
  findings.manualSteps.push({ kind: "peer-dependency-renamed", name: renamed });
@@ -671,6 +1125,17 @@ function reportOverrides(manifest, findings) {
671
1125
  }
672
1126
  }
673
1127
  }
1128
+ function reportOldPeers(manifest, findings) {
1129
+ for (const [dependent, { peer, range }] of OLD_PEER_DEPENDENTS) {
1130
+ const isDependency = DEPENDENCY_FIELDS.some((field) => {
1131
+ const dependencies = manifest[field];
1132
+ return isObject(dependencies) && Object.hasOwn(dependencies, dependent);
1133
+ });
1134
+ if (isDependency) {
1135
+ findings.manualSteps.push({ kind: "old-peer-needed", dependent, peer, range });
1136
+ }
1137
+ }
1138
+ }
674
1139
  function migrateFields(manifest, version, findings) {
675
1140
  const migrated = { ...manifest };
676
1141
  for (const field of DEPENDENCY_FIELDS) {
@@ -683,6 +1148,7 @@ function migrateFields(manifest, version, findings) {
683
1148
  migrated.scripts = migrateScripts(manifest.scripts, findings);
684
1149
  }
685
1150
  reportOverrides(manifest, findings);
1151
+ reportOldPeers(manifest, findings);
686
1152
  return migrated;
687
1153
  }
688
1154
  function migratePackageManifest(text, version) {
@@ -690,7 +1156,7 @@ function migratePackageManifest(text, version) {
690
1156
  try {
691
1157
  manifest = JSON.parse(text);
692
1158
  } catch (error) {
693
- const reason = error instanceof Error ? error.message : String(error);
1159
+ const reason = hideUrlCredentials(error instanceof Error ? error.message : String(error));
694
1160
  return { ...unchanged(text), manualSteps: [{ kind: "manifest-unreadable", reason }] };
695
1161
  }
696
1162
  if (!isObject(manifest)) {
@@ -702,10 +1168,6 @@ function migratePackageManifest(text, version) {
702
1168
  return { text: isChanged ? formatLike(text, migrated) : text, ...findings };
703
1169
  }
704
1170
 
705
- const SPECIFIER_RE = new RegExp(
706
- `(["'\`])(${[...PACKAGE_RENAMES.keys()].join("|")})(/[^"'\`\\s]*)?\\1`,
707
- "g"
708
- );
709
1171
  const SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
710
1172
  ".ts",
711
1173
  ".tsx",
@@ -719,28 +1181,60 @@ const SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
719
1181
  ".svelte",
720
1182
  ".astro"
721
1183
  ]);
722
- function migrateModuleSpecifiers(text) {
1184
+ function renamedValue(value) {
1185
+ const slash = value.indexOf("/");
1186
+ const name = slash === -1 ? value : value.slice(0, slash);
1187
+ const renamed = PACKAGE_RENAMES.get(name);
1188
+ if (renamed === void 0) {
1189
+ return void 0;
1190
+ }
1191
+ return { to: `${renamed}${value.slice(name.length)}`, isDeep: slash !== -1 };
1192
+ }
1193
+ function distinctStrings(strings) {
1194
+ const distinct = [];
1195
+ for (const found of [...strings].sort((left, right) => left.start - right.start)) {
1196
+ const previous = distinct.at(-1);
1197
+ if (previous === void 0 || found.start >= previous.end) {
1198
+ distinct.push(found);
1199
+ } else if (found.start === previous.start && found.end === previous.end) {
1200
+ const isModuleSpecifier = previous.isModuleSpecifier || found.isModuleSpecifier;
1201
+ distinct[distinct.length - 1] = { ...previous, isModuleSpecifier };
1202
+ } else {
1203
+ throw new Error(
1204
+ `Source strings overlap at offsets ${previous.start}-${previous.end} and ${found.start}-${found.end}; the file was not changed.`
1205
+ );
1206
+ }
1207
+ }
1208
+ return distinct;
1209
+ }
1210
+ function migrateModuleSpecifiers(text, strings) {
723
1211
  const changes = [];
724
1212
  const manualSteps = [];
725
1213
  let migrated = "";
726
1214
  let copiedUpTo = 0;
727
- for (const match of text.matchAll(SPECIFIER_RE)) {
728
- const [whole, quote, name, subpath] = match;
729
- const line = lineAt(text, match.index);
730
- const to = `${PACKAGE_RENAMES.get(name)}${subpath ?? ""}`;
731
- changes.push({ kind: "specifier-renamed", line, from: whole.slice(1, -1), to });
732
- if (subpath !== void 0) {
733
- manualSteps.push({ kind: "deep-import", line, specifier: to });
1215
+ for (const found of distinctStrings(strings)) {
1216
+ const renamed = renamedValue(found.value);
1217
+ if (renamed === void 0) {
1218
+ continue;
1219
+ }
1220
+ const line = lineAt(text, found.start);
1221
+ if (!found.isModuleSpecifier) {
1222
+ manualSteps.push({ kind: "string-names-package", line, value: found.value, to: renamed.to });
1223
+ continue;
1224
+ }
1225
+ changes.push({ kind: "specifier-renamed", line, from: found.value, to: renamed.to });
1226
+ if (renamed.isDeep) {
1227
+ manualSteps.push({ kind: "deep-import", line, specifier: renamed.to });
734
1228
  }
735
- migrated += `${text.slice(copiedUpTo, match.index)}${quote}${to}${quote}`;
736
- copiedUpTo = match.index + whole.length;
1229
+ migrated += `${text.slice(copiedUpTo, found.start)}${renamed.to}`;
1230
+ copiedUpTo = found.end;
737
1231
  }
738
1232
  return { text: migrated + text.slice(copiedUpTo), changes, manualSteps };
739
1233
  }
740
1234
 
741
- const ENTRY_RE = /^(\s+)(["']?)([\w@./-]+)\2:\s*(["']?)([^"'#\s]+)\4(.*)$/;
1235
+ const ENTRY_RE = /^(\s+)(["']?)([\w@./-]+)\2:\s*(["']?)([^"'#\s]+)\4/;
742
1236
  const TOP_LEVEL_KEY_RE = /^([\w-]+):/;
743
- const KEY_RE = /^\s+(["']?)(.+?)\1:(?:\s|$)/;
1237
+ const KEY_RE = /^\s+(["']?)(\S.*?)\1:(?:\s|$)/;
744
1238
  const CATALOG_SECTIONS = /* @__PURE__ */ new Set(["catalog", "catalogs"]);
745
1239
  const REPORTED_SECTIONS = /* @__PURE__ */ new Set(["overrides", "patchedDependencies"]);
746
1240
  function migrateEntry(line, version) {
@@ -749,7 +1243,8 @@ function migrateEntry(line, version) {
749
1243
  if (entry === null || renamed === void 0) {
750
1244
  return { line };
751
1245
  }
752
- const [, indent, keyQuote, name, specQuote, spec, rest] = entry;
1246
+ const [whole, indent, keyQuote, name, specQuote, spec] = entry;
1247
+ const rest = line.slice(whole.length);
753
1248
  const decision = migrateVersionSpec(spec, version);
754
1249
  const key = `${keyQuote}${renamed}${keyQuote}`;
755
1250
  const migrated = `${indent}${key}: ${specQuote}${decision.spec}${specQuote}${rest}`;
@@ -758,7 +1253,7 @@ function migrateEntry(line, version) {
758
1253
  return {
759
1254
  line: migrated,
760
1255
  change,
761
- manualStep: { kind: "spec-kept", name: renamed, spec }
1256
+ manualStep: { kind: "spec-kept", name: renamed, spec: hideUrlCredentials(spec) }
762
1257
  };
763
1258
  }
764
1259
  return { line: migrated, change };
@@ -771,29 +1266,41 @@ function reportEntry(line, field) {
771
1266
  name
772
1267
  }));
773
1268
  }
1269
+ const LINE_FEED = "\n";
1270
+ const CARRIAGE_RETURN = "\r";
1271
+ function withoutCarriageReturn(line) {
1272
+ if (!line.endsWith(CARRIAGE_RETURN)) {
1273
+ return { body: line, restore: (body) => body };
1274
+ }
1275
+ return {
1276
+ body: line.slice(0, -CARRIAGE_RETURN.length),
1277
+ restore: (body) => `${body}${CARRIAGE_RETURN}`
1278
+ };
1279
+ }
774
1280
  function migrateWorkspaceCatalogs(text, version) {
775
1281
  const changes = [];
776
1282
  const manualSteps = [];
777
1283
  let section;
778
- const lines = text.split("\n").map((line) => {
1284
+ const lines = text.split(LINE_FEED).map((rawLine) => {
1285
+ const { body: line, restore } = withoutCarriageReturn(rawLine);
779
1286
  const topLevelKey = TOP_LEVEL_KEY_RE.exec(line)?.[1];
780
1287
  if (topLevelKey !== void 0) {
781
1288
  section = topLevelKey;
782
- return line;
1289
+ return rawLine;
783
1290
  }
784
1291
  if (section !== void 0 && REPORTED_SECTIONS.has(section)) {
785
1292
  manualSteps.push(...reportEntry(line, section));
786
- return line;
1293
+ return rawLine;
787
1294
  }
788
1295
  if (section === void 0 || !CATALOG_SECTIONS.has(section)) {
789
- return line;
1296
+ return rawLine;
790
1297
  }
791
1298
  const result = migrateEntry(line, version);
792
1299
  changes.push(...result.change ? [result.change] : []);
793
1300
  manualSteps.push(...result.manualStep ? [result.manualStep] : []);
794
- return result.line;
1301
+ return restore(result.line);
795
1302
  });
796
- return changes.length === 0 ? { ...unchanged(text), manualSteps } : { text: lines.join("\n"), changes, manualSteps };
1303
+ return changes.length === 0 ? { ...unchanged(text), manualSteps } : { text: lines.join(LINE_FEED), changes, manualSteps };
797
1304
  }
798
1305
 
799
1306
  const SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([".sh", ".bash", ".zsh", ".yml", ".yaml"]);
@@ -826,7 +1333,10 @@ const FILE_KINDS = [
826
1333
  },
827
1334
  {
828
1335
  matches: (path) => SOURCE_EXTENSIONS.has(extensionOf(path)),
829
- migrate: (text, path, context) => chain(migrateModuleSpecifiers(text), (renamed) => context.rewriter.rewrite(renamed, path))
1336
+ migrate: (text, path, context) => chain(
1337
+ migrateModuleSpecifiers(text, context.scanner.scan(text, path)),
1338
+ (renamed) => context.rewriter.rewrite(renamed, path)
1339
+ )
830
1340
  },
831
1341
  {
832
1342
  matches: (path) => SCRIPT_EXTENSIONS.has(extensionOf(path)) || SCRIPT_NAMES.has(baseName(path)),
@@ -846,7 +1356,8 @@ async function migrateFile(path, dependencies, input) {
846
1356
  return void 0;
847
1357
  }
848
1358
  const text = await dependencies.files.read(path);
849
- const context = { version: input.version, rewriter: dependencies.rewriter };
1359
+ const { rewriter, scanner } = dependencies;
1360
+ const context = { version: input.version, rewriter, scanner };
850
1361
  const migration = migrate(text, path, context);
851
1362
  const isChanged = migration.text !== text;
852
1363
  if (isChanged && input.isWrite) {
@@ -859,7 +1370,8 @@ async function migrateFile(path, dependencies, input) {
859
1370
  return { path, isChanged, changes, manualSteps };
860
1371
  }
861
1372
  async function migrateProject(dependencies, input) {
862
- const paths = (await dependencies.files.listFiles()).sort();
1373
+ const { files, skipped } = await dependencies.files.listFiles();
1374
+ const paths = [...files].sort();
863
1375
  dependencies.logger.info("migration_started", { fileCount: paths.length, ...input });
864
1376
  const reports = [];
865
1377
  for (const path of paths) {
@@ -868,8 +1380,11 @@ async function migrateProject(dependencies, input) {
868
1380
  reports.push(report);
869
1381
  }
870
1382
  }
871
- dependencies.logger.info("migration_finished", { changedFiles: reports.length });
872
- return reports;
1383
+ dependencies.logger.info("migration_finished", {
1384
+ changedFiles: reports.length,
1385
+ skippedFiles: skipped.length
1386
+ });
1387
+ return { files: reports, skipped };
873
1388
  }
874
1389
 
875
1390
  function assertNever(value) {
@@ -877,6 +1392,7 @@ function assertNever(value) {
877
1392
  }
878
1393
 
879
1394
  const IR_CALLBACKS_GUIDE = "https://github.com/openapi-typesafe/openapi-typesafe/blob/main/docs/node.md#ir-callbacks";
1395
+ const OPENAPI_FETCH_ALIAS_GUIDE = "https://github.com/openapi-typesafe/openapi-typesafe/blob/main/docs/fetch/index.md#libraries-built-on-openapi-fetch";
880
1396
  function changeMessage(change) {
881
1397
  switch (change.kind) {
882
1398
  case "dependency-renamed":
@@ -904,8 +1420,15 @@ const KEEP_REASONS = {
904
1420
  const PEER_NOTE = "users of this package must switch too (a breaking change for them)";
905
1421
  const PINNED_NOTE = "change it to openapi-typesafe@<version>";
906
1422
  const DEEP_IMPORT_NOTE = "imports a file inside the package; check that it exists";
1423
+ const STRING_NOTE = "is a string, not a module specifier, so it was kept";
1424
+ const STRING_FIX_NOTE = "if it names the package, change it to";
907
1425
  const KEPT_NOTE = `it works, with a deprecation warning. See ${IR_CALLBACKS_GUIDE}`;
908
1426
  const LEGACY_CALLBACK_NOTE = `receives TypeScript nodes (deprecated). See ${IR_CALLBACKS_GUIDE}`;
1427
+ function oldPeerMessage(step) {
1428
+ const { dependent, peer, range } = step;
1429
+ const successor = PACKAGE_RENAMES.get(peer) ?? peer;
1430
+ return `${dependent} takes its client type from ${peer}@${range}, its peer dependency: alias ${peer} to ${successor} with overrides or resolutions, or passing it the ${successor} client fails to type-check. See ${OPENAPI_FETCH_ALIAS_GUIDE}`;
1431
+ }
909
1432
  function manualStepMessage(step) {
910
1433
  switch (step.kind) {
911
1434
  case "manifest-unreadable":
@@ -920,6 +1443,10 @@ function manualStepMessage(step) {
920
1443
  return `${step.location} runs a pinned openapi-typescript@<version>: ${PINNED_NOTE}`;
921
1444
  case "deep-import":
922
1445
  return `line ${step.line}: "${step.specifier}" ${DEEP_IMPORT_NOTE}`;
1446
+ case "string-names-package":
1447
+ return `line ${step.line}: "${step.value}" ${STRING_NOTE}; ${STRING_FIX_NOTE} "${step.to}"`;
1448
+ case "old-peer-needed":
1449
+ return oldPeerMessage(step);
923
1450
  case "transform-not-rewritten":
924
1451
  return `line ${step.line}: transform kept (${KEEP_REASONS[step.reason]}); ${KEPT_NOTE}`;
925
1452
  case "legacy-callback":
@@ -928,6 +1455,13 @@ function manualStepMessage(step) {
928
1455
  return assertNever(step);
929
1456
  }
930
1457
  }
1458
+ const SKIPPED_HEADING = "Skipped (not read, not written):";
1459
+ const SKIP_REASONS = {
1460
+ "outside-root": "git listed it, but it is outside the project folder; check where this git index came from"
1461
+ };
1462
+ function skipReasonMessage(reason) {
1463
+ return SKIP_REASONS[reason];
1464
+ }
931
1465
  const NEXT_STEPS = [
932
1466
  "Install: run your package manager's install, so the lockfile has the new packages.",
933
1467
  "Regenerate your types and commit them once: the file header names openapi-typesafe now, so `--check` fails until you do.",
@@ -957,14 +1491,24 @@ function manualStepsSection(reports) {
957
1491
  );
958
1492
  return steps.length === 0 ? [] : ["Manual steps:", ...steps, ""];
959
1493
  }
960
- function renderReport(reports, isWrite) {
1494
+ function skippedSection(skipped) {
1495
+ if (skipped.length === 0) {
1496
+ return [];
1497
+ }
1498
+ const lines = skipped.map((file) => ` ${file.path}: ${skipReasonMessage(file.reason)}`);
1499
+ return [SKIPPED_HEADING, ...lines, ""];
1500
+ }
1501
+ function renderReport(migration, isWrite) {
1502
+ const reports = migration.files;
1503
+ const skipped = skippedSection(migration.skipped);
961
1504
  if (reports.length === 0) {
962
- return `${NOTHING_FOUND}
1505
+ return `${[...skipped, NOTHING_FOUND].join("\n")}
963
1506
  `;
964
1507
  }
965
1508
  const lines = [
966
1509
  ...changesSection(reports),
967
1510
  ...manualStepsSection(reports),
1511
+ ...skipped,
968
1512
  "Next steps:",
969
1513
  ...NEXT_STEPS.map((step, index) => ` ${index + 1}. ${step}`),
970
1514
  "",
@@ -1012,7 +1556,7 @@ Options
1012
1556
 
1013
1557
  Exit codes
1014
1558
  0 Done
1015
- 1 The migration failed (the message and the log say why)
1559
+ 1 The migration failed (the message and the log say why), or the log could not be created
1016
1560
  2 Unknown option or argument
1017
1561
  `;
1018
1562
  const usageError = (message) => `${message}
@@ -1020,6 +1564,8 @@ Run openapi-typesafe-migrate --help for the options.
1020
1564
  `;
1021
1565
  const logNote = (path) => `Log: ${path}
1022
1566
  `;
1567
+ const logError = (path, message) => `Cannot create the log ${path ?? "in the temporary directory"}: ${message}
1568
+ `;
1023
1569
 
1024
1570
  const EXIT_CODES = { success: 0, failure: 1, usage: 2 };
1025
1571
  function parseRequest(argv, dependencies) {
@@ -1033,17 +1579,31 @@ function parseRequest(argv, dependencies) {
1033
1579
  throw error;
1034
1580
  }
1035
1581
  }
1582
+ const errorMessage = (error) => error instanceof Error ? error.message : String(error);
1583
+ function openLog(request, dependencies) {
1584
+ try {
1585
+ return dependencies.createLogger(request.logPath);
1586
+ } catch (error) {
1587
+ dependencies.writeStderr(logError(request.logPath, errorMessage(error)));
1588
+ return void 0;
1589
+ }
1590
+ }
1036
1591
  async function runMigration(request, dependencies) {
1592
+ const log = openLog(request, dependencies);
1593
+ if (log === void 0) {
1594
+ return EXIT_CODES.failure;
1595
+ }
1596
+ const { logger, path } = log;
1037
1597
  const root = dependencies.resolvePath(dependencies.cwd, request.root ?? ".");
1038
- const { logger, path } = dependencies.createLogger(request.logPath);
1039
1598
  try {
1040
- const files = dependencies.createFiles(root);
1599
+ const files = dependencies.createFiles(root, logger);
1600
+ const { rewriter, scanner } = dependencies;
1041
1601
  const input = { version: dependencies.version, isWrite: request.isWrite };
1042
- const reports = await migrateProject({ files, rewriter: dependencies.rewriter, logger }, input);
1043
- dependencies.writeStdout(renderReport(reports, request.isWrite));
1602
+ const migration = await migrateProject({ files, rewriter, scanner, logger }, input);
1603
+ dependencies.writeStdout(renderReport(migration, request.isWrite));
1044
1604
  return EXIT_CODES.success;
1045
1605
  } catch (error) {
1046
- const message = error instanceof Error ? error.message : String(error);
1606
+ const message = errorMessage(error);
1047
1607
  logger.warn("migration_failed", { root, message });
1048
1608
  dependencies.writeStderr(`${message}
1049
1609
  `);
@@ -1070,24 +1630,25 @@ async function runCli(argv, dependencies) {
1070
1630
  }
1071
1631
 
1072
1632
  const PACKAGE_JSON = new URL("../../package.json", import.meta.url);
1073
- const LOG_DIRECTORY = "openapi-typesafe-migrate";
1074
1633
  function readVersion() {
1075
1634
  const packageJson = JSON.parse(readFileSync(PACKAGE_JSON, "utf8"));
1076
1635
  return packageJson.version;
1077
1636
  }
1078
1637
  const now = () => /* @__PURE__ */ new Date();
1079
1638
  function createLogger(path) {
1080
- const logPath = path === void 0 ? join(tmpdir(), LOG_DIRECTORY, `${now().toISOString().replaceAll(":", "-")}.jsonl`) : resolve(path);
1081
- return { logger: createJsonLinesLogger({ path: logPath, now }), path: logPath };
1639
+ const isExclusive = path === void 0;
1640
+ const logPath = isExclusive ? createTemporaryLogPath(tmpdir(), now()) : resolve(path);
1641
+ return { logger: createJsonLinesLogger({ path: logPath, now, isExclusive }), path: logPath };
1082
1642
  }
1083
1643
  const dependencies = {
1084
1644
  version: readVersion(),
1085
1645
  cwd: process.cwd(),
1086
1646
  writeStdout: (text) => process.stdout.write(text),
1087
1647
  writeStderr: (text) => process.stderr.write(text),
1088
- createFiles: createNodeProjectFiles,
1648
+ createFiles: (root, logger) => createNodeProjectFiles(root, { git: createGitFileList(), logger }),
1089
1649
  createLogger,
1090
1650
  rewriter: createTypescriptCallbackRewriter(),
1651
+ scanner: createTypescriptStringScanner(),
1091
1652
  resolvePath: resolve
1092
1653
  };
1093
1654
  runCli(process.argv.slice(2), dependencies).then((exitCode) => {