openapi-typesafe-migrate 1.0.0-rc.2 → 1.0.0-rc.4

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,11 +1,157 @@
1
- import { mkdtempSync, writeFileSync, mkdirSync, appendFileSync, readFileSync } from 'node:fs';
1
+ import { constants, mkdtempSync, mkdirSync, appendFileSync, openSync, fstatSync, closeSync, readFileSync } from 'node:fs';
2
2
  import { tmpdir } from 'node:os';
3
- import { join, dirname, basename, isAbsolute, relative, sep, resolve } from 'node:path';
3
+ import { win32, posix, join, dirname, isAbsolute, relative, sep, basename, resolve } from 'node:path';
4
+ import { realpath, lstat, stat, access, readFile, writeFile, chmod, rename, rm, readdir } from 'node:fs/promises';
4
5
  import { spawn } from 'node:child_process';
5
- import { realpath, readFile, lstat, writeFile, chmod, rename, rm, readdir } from 'node:fs/promises';
6
6
  import ts from '@typescript/typescript6';
7
+ import { Scalar, parseAllDocuments, isScalar as isScalar$1, isSeq, isMap as isMap$1, isNode, CST } from 'yaml';
7
8
  import { parseArgs } from 'node:util';
8
9
 
10
+ const WINDOWS = "win32";
11
+ const PATH_VARIABLE = "PATH";
12
+ const GIT_NAME = "git";
13
+ const WINDOWS_EXECUTABLE_EXTENSION = ".exe";
14
+ const GIT_FOLDER = ".git";
15
+ const PARENT_FOLDER = "..";
16
+ const PACKAGES_FOLDER = "node_modules";
17
+ const ABSENT_CODES = /* @__PURE__ */ new Set(["ENOENT", "ENOTDIR", "EACCES", "EPERM"]);
18
+ function isAbsent(error) {
19
+ return ABSENT_CODES.has(error?.code ?? "");
20
+ }
21
+ async function unlessAbsent(check, absent) {
22
+ try {
23
+ return await check();
24
+ } catch (error) {
25
+ if (isAbsent(error)) {
26
+ return absent;
27
+ }
28
+ throw error;
29
+ }
30
+ }
31
+ const pathsOf = (platform) => platform === WINDOWS ? win32 : posix;
32
+ function pathVariable({ environment, platform }) {
33
+ const name = platform === WINDOWS ? Object.keys(environment).find((key) => key.toUpperCase() === PATH_VARIABLE) : PATH_VARIABLE;
34
+ return name === void 0 ? "" : environment[name] ?? "";
35
+ }
36
+ function isAtOrUnder(paths, folder, target) {
37
+ const path = paths.relative(folder, target);
38
+ return !(path === PARENT_FOLDER || path.startsWith(`${PARENT_FOLDER}${paths.sep}`)) && !paths.isAbsolute(path);
39
+ }
40
+ function isInPackagesFolder(host, path) {
41
+ const separator = host.platform === WINDOWS ? /[\\/]/ : posix.sep;
42
+ return path.split(separator).some((segment) => segment.toLowerCase() === PACKAGES_FOLDER);
43
+ }
44
+ async function isTrustedFolder(host, project, folder) {
45
+ const paths = pathsOf(host.platform);
46
+ if (!paths.isAbsolute(folder) || isInPackagesFolder(host, folder)) {
47
+ return false;
48
+ }
49
+ const real = await host.fileSystem.realPath(folder);
50
+ return real !== void 0 && !isAtOrUnder(paths, project, real) && !isInPackagesFolder(host, real);
51
+ }
52
+ async function findGitExecutable(host) {
53
+ const paths = pathsOf(host.platform);
54
+ const name = host.platform === WINDOWS ? `${GIT_NAME}${WINDOWS_EXECUTABLE_EXTENSION}` : GIT_NAME;
55
+ const root = paths.resolve(host.projectRoot);
56
+ const project = await host.fileSystem.realPath(root) ?? root;
57
+ for (const folder of pathVariable(host).split(paths.delimiter)) {
58
+ const candidate = paths.join(folder, name);
59
+ const isTrusted = await isTrustedFolder(host, project, folder);
60
+ if (isTrusted && await host.fileSystem.isExecutableFile(candidate)) {
61
+ return candidate;
62
+ }
63
+ }
64
+ return void 0;
65
+ }
66
+ async function findWorkTree(directory, { platform, fileSystem }, isRepository) {
67
+ const paths = pathsOf(platform);
68
+ let folder = paths.resolve(directory);
69
+ for (; ; ) {
70
+ const parent = paths.dirname(folder);
71
+ const hasGit = await fileSystem.hasEntry(paths.join(folder, GIT_FOLDER));
72
+ if (hasGit && await isRepository(folder, parent)) {
73
+ return folder;
74
+ }
75
+ if (parent === folder) {
76
+ return void 0;
77
+ }
78
+ folder = parent;
79
+ }
80
+ }
81
+ function createNodeGitFileSystem() {
82
+ return {
83
+ isExecutableFile: (path) => unlessAbsent(async () => {
84
+ const isFile = (await stat(path)).isFile();
85
+ await access(path, constants.X_OK);
86
+ return isFile;
87
+ }, false),
88
+ hasEntry: (path) => unlessAbsent(async () => {
89
+ await lstat(path);
90
+ return true;
91
+ }, false),
92
+ realPath: (path) => unlessAbsent(() => realpath(path), void 0)
93
+ };
94
+ }
95
+
96
+ const spawnGitProcess = ({ executable, directory, gitArguments, environment }) => new Promise((resolve) => {
97
+ const child = spawn(executable, [...gitArguments], { cwd: directory, env: environment });
98
+ const output = [];
99
+ const errors = [];
100
+ child.stdout.on("data", (chunk) => output.push(chunk));
101
+ child.stderr.on("data", (chunk) => errors.push(chunk));
102
+ child.on("error", (error) => resolve({ kind: "failed", reason: error.message }));
103
+ child.on("close", (exitCode) => {
104
+ resolve({
105
+ kind: "exited",
106
+ exitCode,
107
+ output: Buffer.concat(output).toString("utf8"),
108
+ errors: Buffer.concat(errors).toString("utf8").trim()
109
+ });
110
+ });
111
+ });
112
+ const HARDENED_CONFIG = [
113
+ "-c",
114
+ "core.fsmonitor=false",
115
+ "-c",
116
+ "protocol.allow=never",
117
+ "-c",
118
+ "safe.bareRepository=explicit"
119
+ ];
120
+ const HARDENED_ENVIRONMENT = {
121
+ GIT_ALLOW_PROTOCOL: "",
122
+ GIT_NO_LAZY_FETCH: "1",
123
+ GIT_TERMINAL_PROMPT: "0"
124
+ };
125
+ const REPOSITORY_VARIABLES = [
126
+ "GIT_DIR",
127
+ "GIT_WORK_TREE",
128
+ "GIT_COMMON_DIR",
129
+ "GIT_INDEX_FILE",
130
+ "GIT_OBJECT_DIRECTORY",
131
+ "GIT_ALTERNATE_OBJECT_DIRECTORIES"
132
+ ];
133
+ const WORK_TREE_VARIABLE = "GIT_WORK_TREE";
134
+ const CEILING_VARIABLE = "GIT_CEILING_DIRECTORIES";
135
+ function hardenedEnvironment(command) {
136
+ const kept = Object.entries(command.environment).filter(
137
+ ([name]) => !REPOSITORY_VARIABLES.includes(name.toUpperCase())
138
+ );
139
+ const { workTree, ceiling } = command;
140
+ const forced = {
141
+ ...workTree === void 0 ? {} : { [WORK_TREE_VARIABLE]: workTree },
142
+ ...ceiling === void 0 ? {} : { [CEILING_VARIABLE]: ceiling }
143
+ };
144
+ return { ...Object.fromEntries(kept), ...forced, ...HARDENED_ENVIRONMENT };
145
+ }
146
+ function hardenedInvocation(command, gitArguments) {
147
+ return {
148
+ executable: command.executable,
149
+ directory: command.directory,
150
+ gitArguments: [...HARDENED_CONFIG, ...gitArguments],
151
+ environment: hardenedEnvironment(command)
152
+ };
153
+ }
154
+
9
155
  const LS_FILES_ARGUMENTS = [
10
156
  "ls-files",
11
157
  "--cached",
@@ -15,36 +161,13 @@ const LS_FILES_ARGUMENTS = [
15
161
  "-z"
16
162
  ];
17
163
  const CHECK_IGNORE_ARGUMENTS = ["check-ignore", "--quiet", "."];
164
+ const REPOSITORY_CHECK_ARGUMENTS = ["rev-parse", "--git-dir"];
18
165
  const IGNORED_EXIT_CODE = 0;
19
166
  const NOT_IGNORED_EXIT_CODE = 1;
20
167
  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
- ];
168
+ const NO_GIT_REASON = "no git executable in an absolute folder of PATH";
28
169
  const TRAILING_SLASH_RE = /\/$/;
29
170
  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
171
  function failureReason(run) {
49
172
  if (run.kind === "failed") {
50
173
  return run.reason;
@@ -54,27 +177,47 @@ function failureReason(run) {
54
177
  function listedPaths$1(output) {
55
178
  return output.split("\0").map((path) => path.replace(TRAILING_SLASH_RE, "")).filter((path) => !SELF_PATHS.has(path));
56
179
  }
57
- async function listDirectory(directory) {
58
- const ignored = await runGit(directory, CHECK_IGNORE_ARGUMENTS);
180
+ async function listWith(command, run) {
181
+ const ignored = await run(hardenedInvocation(command, CHECK_IGNORE_ARGUMENTS));
59
182
  if (ignored.kind === "exited" && ignored.exitCode === IGNORED_EXIT_CODE) {
60
183
  return { kind: "unavailable", reason: IGNORED_REASON };
61
184
  }
62
185
  if (ignored.kind === "failed" || ignored.exitCode !== NOT_IGNORED_EXIT_CODE) {
63
186
  return { kind: "unavailable", reason: failureReason(ignored) };
64
187
  }
65
- const listed = await runGit(directory, LS_FILES_ARGUMENTS);
188
+ const listed = await run(hardenedInvocation(command, LS_FILES_ARGUMENTS));
66
189
  if (listed.kind === "failed" || listed.exitCode !== 0) {
67
190
  return { kind: "unavailable", reason: failureReason(listed) };
68
191
  }
69
192
  return { kind: "listed", paths: listedPaths$1(listed.output) };
70
193
  }
71
- class SpawnedGitFileList {
72
- list(directory) {
73
- return listDirectory(directory);
194
+ class HardenedGitFileList {
195
+ #host;
196
+ #process;
197
+ /** PATH is searched once, on the first listing. */
198
+ #executable;
199
+ constructor(options) {
200
+ this.#host = options;
201
+ this.#process = options.process ?? spawnGitProcess;
202
+ }
203
+ async list(directory) {
204
+ this.#executable ??= findGitExecutable(this.#host);
205
+ const executable = await this.#executable;
206
+ if (executable === void 0) {
207
+ return { kind: "unavailable", reason: NO_GIT_REASON };
208
+ }
209
+ const { environment } = this.#host;
210
+ const isRepository = async (folder, ceiling) => {
211
+ const command = { executable, directory: folder, environment, workTree: void 0, ceiling };
212
+ const run = await this.#process(hardenedInvocation(command, REPOSITORY_CHECK_ARGUMENTS));
213
+ return run.kind === "exited" && run.exitCode === 0;
214
+ };
215
+ const workTree = await findWorkTree(directory, this.#host, isRepository);
216
+ return listWith({ executable, directory, environment, workTree }, this.#process);
74
217
  }
75
218
  }
76
- function createGitFileList() {
77
- return new SpawnedGitFileList();
219
+ function createGitFileList(options) {
220
+ return new HardenedGitFileList(options);
78
221
  }
79
222
 
80
223
  const LOG_FILE_MODE = 384;
@@ -82,15 +225,39 @@ const TEMPORARY_FOLDER_PREFIX = "openapi-typesafe-migrate-";
82
225
  const LOG_EXTENSION = ".jsonl";
83
226
  const TIME_SEPARATOR = ":";
84
227
  const FILE_NAME_TIME_SEPARATOR = "-";
228
+ class LogPathIsFolderError extends Error {
229
+ name = "LogPathIsFolderError";
230
+ constructor(path) {
231
+ super(`${path} is a folder; --log takes a file path`);
232
+ }
233
+ }
234
+ const EXCLUSIVE_CREATE_FLAG = "wx";
235
+ const APPEND_FLAG = "a";
236
+ const IS_A_DIRECTORY_CODE = "EISDIR";
237
+ function openLogFile(path, isExclusive) {
238
+ let descriptor;
239
+ try {
240
+ descriptor = openSync(path, isExclusive ? EXCLUSIVE_CREATE_FLAG : APPEND_FLAG, LOG_FILE_MODE);
241
+ } catch (error) {
242
+ throw isDirectoryError(error) ? new LogPathIsFolderError(path) : error;
243
+ }
244
+ try {
245
+ if (fstatSync(descriptor).isDirectory()) {
246
+ throw new LogPathIsFolderError(path);
247
+ }
248
+ } finally {
249
+ closeSync(descriptor);
250
+ }
251
+ }
252
+ const isDirectoryError = (error) => error instanceof Error && "code" in error && error.code === IS_A_DIRECTORY_CODE;
85
253
  class JsonLinesLogger {
86
254
  #options;
87
255
  constructor(options) {
88
256
  this.#options = options;
89
- if (options.isExclusive) {
90
- writeFileSync(options.path, "", { flag: "wx", mode: LOG_FILE_MODE });
91
- } else {
257
+ if (!options.isExclusive) {
92
258
  mkdirSync(dirname(options.path), { recursive: true });
93
259
  }
260
+ openLogFile(options.path, options.isExclusive);
94
261
  }
95
262
  info(event, fields = {}) {
96
263
  this.#append("info", event, fields);
@@ -114,6 +281,101 @@ function createTemporaryLogPath(parent, startTime) {
114
281
  return join(folder, `${name}${LOG_EXTENSION}`);
115
282
  }
116
283
 
284
+ const FILE_NAME = "document.json";
285
+ const QUOTE_LENGTH = 1;
286
+ const encode = (value) => JSON.stringify(value);
287
+ function spanOf$1(node, file) {
288
+ return { start: node.getStart(file), end: node.end };
289
+ }
290
+ function convertString(node, file) {
291
+ const span = spanOf$1(node, file);
292
+ const content = { start: span.start + QUOTE_LENGTH, end: span.end - QUOTE_LENGTH };
293
+ return { kind: "scalar", value: node.text, content, isFolded: false, encode, ...span };
294
+ }
295
+ function convertObject(node, file) {
296
+ const entries = node.properties.filter(ts.isPropertyAssignment).map(
297
+ (property) => ({
298
+ key: ts.isStringLiteral(property.name) ? convertString(property.name, file) : { kind: "other", ...spanOf$1(property.name, file) },
299
+ value: convert$1(property.initializer, file)
300
+ })
301
+ );
302
+ return { kind: "map", entries, ...spanOf$1(node, file) };
303
+ }
304
+ function convert$1(node, file) {
305
+ if (ts.isObjectLiteralExpression(node)) {
306
+ return convertObject(node, file);
307
+ }
308
+ if (ts.isArrayLiteralExpression(node)) {
309
+ const items = node.elements.map((item) => convert$1(item, file));
310
+ return { kind: "list", items, ...spanOf$1(node, file) };
311
+ }
312
+ if (ts.isStringLiteral(node)) {
313
+ return convertString(node, file);
314
+ }
315
+ return { kind: "other", ...spanOf$1(node, file) };
316
+ }
317
+ function firstError(text) {
318
+ const { error } = ts.parseConfigFileTextToJson(FILE_NAME, text);
319
+ return error === void 0 ? void 0 : error.start ?? 0;
320
+ }
321
+ function readTree(text) {
322
+ const errorAt = firstError(text);
323
+ if (errorAt !== void 0) {
324
+ return { kind: "unreadable", offset: errorAt };
325
+ }
326
+ const file = ts.parseJsonText(FILE_NAME, text);
327
+ const documents = file.statements.map((statement) => convert$1(statement.expression, file));
328
+ return { kind: "read", documents };
329
+ }
330
+ function read$1(text) {
331
+ try {
332
+ return readTree(text);
333
+ } catch (error) {
334
+ if (error instanceof RangeError) {
335
+ return { kind: "unreadable", offset: void 0 };
336
+ }
337
+ throw error;
338
+ }
339
+ }
340
+ function createJsonTextReader() {
341
+ return { read: read$1 };
342
+ }
343
+
344
+ const PARENT_DIRECTORY = "..";
345
+ const UNRESOLVED_PATH_CODES = /* @__PURE__ */ new Set(["ENOENT", "ENOTDIR", "ELOOP"]);
346
+ function isRelativeDownward(path) {
347
+ return !isAbsolute(path) && !path.split("/").includes(PARENT_DIRECTORY);
348
+ }
349
+ function isInFolder(folder, target) {
350
+ const path = relative(folder, target);
351
+ return path !== "" && path !== PARENT_DIRECTORY && !path.startsWith(`${PARENT_DIRECTORY}${sep}`) && !isAbsolute(path);
352
+ }
353
+ async function entryAt(root, realRoot, path) {
354
+ if (!isRelativeDownward(path)) {
355
+ return "outside";
356
+ }
357
+ try {
358
+ const stats = await lstat(join(root, path));
359
+ const isListable = stats.isFile() || stats.isDirectory();
360
+ if (!isListable && !stats.isSymbolicLink()) {
361
+ return "other";
362
+ }
363
+ const realPath = await realpath(join(root, path));
364
+ if (realPath !== realRoot && !isInFolder(realRoot, realPath)) {
365
+ return "outside";
366
+ }
367
+ if (!isListable) {
368
+ return "other";
369
+ }
370
+ return stats.isFile() ? "file" : "directory";
371
+ } catch (error) {
372
+ if (UNRESOLVED_PATH_CODES.has(error.code ?? "")) {
373
+ return "other";
374
+ }
375
+ throw error;
376
+ }
377
+ }
378
+
117
379
  const SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set([
118
380
  "node_modules",
119
381
  ".git",
@@ -129,15 +391,35 @@ const CURRENT_DIRECTORY = ".";
129
391
  const TEMPORARY_SUFFIX = ".openapi-typesafe-migrate.tmp";
130
392
  const PERMISSION_BITS = 4095;
131
393
  const TEMPORARY_MODE = 384;
132
- const PARENT_DIRECTORY = "..";
394
+ const OUTSIDE_ROOT = { kind: "outside-root" };
395
+ const NOT_UTF_8 = { kind: "not-utf-8" };
396
+ const UTF_8 = "utf-8";
397
+ function decodeUtf8(bytes) {
398
+ try {
399
+ const text = new TextDecoder(UTF_8, { fatal: true, ignoreBOM: true }).decode(bytes);
400
+ return { kind: "read", text };
401
+ } catch (error) {
402
+ if (error instanceof TypeError) {
403
+ return { kind: "skipped", reason: NOT_UTF_8 };
404
+ }
405
+ throw error;
406
+ }
407
+ }
408
+ function ioProblem(error) {
409
+ const code = error?.code;
410
+ if (typeof code !== "string") {
411
+ throw error;
412
+ }
413
+ return { kind: "io-error", code };
414
+ }
133
415
  function isInSkippedDirectory(path) {
134
416
  return path.split("/").slice(0, -1).some((directory) => SKIPPED_DIRECTORIES.has(directory));
135
417
  }
136
- async function listTree(root, relative2) {
137
- const entries = await readdir(join(root, relative2), { withFileTypes: true });
418
+ async function listTree(root, relative) {
419
+ const entries = await readdir(join(root, relative), { withFileTypes: true });
138
420
  const nested = await Promise.all(
139
421
  entries.map((entry) => {
140
- const path = relative2 === "" ? entry.name : `${relative2}/${entry.name}`;
422
+ const path = relative === "" ? entry.name : `${relative}/${entry.name}`;
141
423
  if (entry.isDirectory()) {
142
424
  return SKIPPED_DIRECTORIES.has(entry.name) ? [] : listTree(root, path);
143
425
  }
@@ -146,33 +428,6 @@ async function listTree(root, relative2) {
146
428
  );
147
429
  return nested.flat();
148
430
  }
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
431
  async function listedPaths(root, realRoot, paths) {
177
432
  const candidates = paths.filter((path) => !isInSkippedDirectory(path));
178
433
  const entries = await Promise.all(candidates.map((path) => entryAt(root, realRoot, path)));
@@ -180,7 +435,7 @@ async function listedPaths(root, realRoot, paths) {
180
435
  return {
181
436
  files: withEntry("file"),
182
437
  directories: withEntry("directory").filter((path) => !SKIPPED_DIRECTORIES.has(baseName$1(path))),
183
- skipped: withEntry("outside").map((path) => ({ path, reason: "outside-root" }))
438
+ skipped: withEntry("outside").map((path) => ({ path, reason: OUTSIDE_ROOT }))
184
439
  };
185
440
  }
186
441
  function baseName$1(path) {
@@ -240,15 +495,31 @@ class NodeProjectFiles {
240
495
  return joinLists([{ files, skipped }, ...nested]);
241
496
  }
242
497
  async read(path) {
243
- return readFile(await this.#pathInRoot(path), "utf8");
498
+ try {
499
+ const target = await this.#pathInRoot(path);
500
+ return target === void 0 ? { kind: "skipped", reason: OUTSIDE_ROOT } : decodeUtf8(await readFile(target));
501
+ } catch (error) {
502
+ return { kind: "skipped", reason: ioProblem(error) };
503
+ }
504
+ }
505
+ async write(path, text) {
506
+ try {
507
+ const target = await this.#pathInRoot(path);
508
+ if (target === void 0) {
509
+ return { kind: "failed", reason: OUTSIDE_ROOT };
510
+ }
511
+ await this.#replace(target, text, path);
512
+ return { kind: "written" };
513
+ } catch (error) {
514
+ return { kind: "failed", reason: ioProblem(error) };
515
+ }
244
516
  }
245
517
  /**
246
518
  * Writes a new file and renames it over the old one, so the write never goes through a link: a
247
519
  * hard link to a file outside the root keeps its old text, and is reported in the log. The new
248
520
  * file keeps the mode of the old one.
249
521
  */
250
- async write(path, text) {
251
- const target = await this.#pathInRoot(path);
522
+ async #replace(target, text, path) {
252
523
  const { mode, nlink } = await lstat(target);
253
524
  const temporary = join(dirname(target), `.${basename(target)}${TEMPORARY_SUFFIX}`);
254
525
  await writeFile(temporary, text, { encoding: "utf8", flag: "wx", mode: TEMPORARY_MODE });
@@ -263,11 +534,11 @@ class NodeProjectFiles {
263
534
  this.#options.logger.warn("hard_link_separated", { path, linkCount: nlink });
264
535
  }
265
536
  }
266
- /** The real path of a project file; refuses one that resolves outside the root. */
537
+ /** The real path of a project file, or undefined for one that resolves outside the root. */
267
538
  async #pathInRoot(path) {
268
539
  const realPath = isRelativeDownward(path) ? await realpath(join(this.#root, path)) : void 0;
269
540
  if (realPath === void 0 || !isInFolder(await realpath(this.#root), realPath)) {
270
- throw new Error(`${path} is outside the project folder ${this.#root}`);
541
+ return void 0;
271
542
  }
272
543
  return realPath;
273
544
  }
@@ -275,6 +546,17 @@ class NodeProjectFiles {
275
546
  function createNodeProjectFiles(root, options) {
276
547
  return new NodeProjectFiles(root, options);
277
548
  }
549
+ const MISSING_PATH_CODES = /* @__PURE__ */ new Set(["ENOENT", "ENOTDIR"]);
550
+ async function inspectProjectRoot(root) {
551
+ try {
552
+ return (await stat(root)).isDirectory() ? "directory" : "not-a-directory";
553
+ } catch (error) {
554
+ if (MISSING_PATH_CODES.has(error.code ?? "")) {
555
+ return "missing";
556
+ }
557
+ throw error;
558
+ }
559
+ }
278
560
 
279
561
  function unchanged(text) {
280
562
  return { text, changes: [], manualSteps: [] };
@@ -287,80 +569,213 @@ function lineAt(text, offset) {
287
569
  }
288
570
  return line;
289
571
  }
572
+ function lineLocator(text) {
573
+ const lineStarts = [0];
574
+ for (let index = text.indexOf("\n"); index !== -1; index = text.indexOf("\n", index + 1)) {
575
+ lineStarts.push(index + 1);
576
+ }
577
+ return (offset) => {
578
+ let low = 0;
579
+ let high = lineStarts.length - 1;
580
+ while (low < high) {
581
+ const middle = Math.ceil((low + high) / 2);
582
+ if (lineStarts[middle] <= offset) {
583
+ low = middle;
584
+ } else {
585
+ high = middle - 1;
586
+ }
587
+ }
588
+ return low + 1;
589
+ };
590
+ }
591
+ function columnAt(text, offset) {
592
+ return offset - text.lastIndexOf("\n", offset - 1);
593
+ }
594
+
595
+ function applyTextEdits(text, edits) {
596
+ const sorted = [...edits].sort((left, right) => left.start - right.start);
597
+ let edited = "";
598
+ let copiedUpTo = 0;
599
+ for (const edit of sorted) {
600
+ if (edit.start < copiedUpTo) {
601
+ throw new Error(
602
+ `Edits overlap at offset ${edit.start}; the file was not changed (a migration bug).`
603
+ );
604
+ }
605
+ edited += `${text.slice(copiedUpTo, edit.start)}${edit.text}`;
606
+ copiedUpTo = edit.end;
607
+ }
608
+ return edited + text.slice(copiedUpTo);
609
+ }
290
610
 
291
611
  const GENERATOR_MODULES = /* @__PURE__ */ new Set(["openapi-typesafe", "openapi-typescript"]);
292
612
  const TYPESCRIPT_MODULES = /* @__PURE__ */ new Set(["typescript", "@typescript/typescript6"]);
293
- function namedImports(clause, into) {
613
+ const DEFAULT_EXPORT$1 = "default";
614
+ function addNamedImports(clause, isGenerator, into) {
294
615
  const bindings = clause.namedBindings;
295
616
  if (bindings === void 0 || !ts.isNamedImports(bindings)) {
296
617
  return;
297
618
  }
298
619
  for (const element of bindings.elements) {
299
- into.set(element.name.text, (element.propertyName ?? element.name).text);
300
- }
301
- }
302
- function addTypescriptImport(clause, names) {
303
- if (clause.name !== void 0) {
304
- names.typescriptModules.add(clause.name.text);
305
- }
306
- const bindings = clause.namedBindings;
307
- if (bindings !== void 0 && ts.isNamespaceImport(bindings)) {
308
- names.typescriptModules.add(bindings.name.text);
309
- }
310
- namedImports(clause, names.typescriptImports);
311
- }
312
- function addGeneratorImport(clause, names) {
313
- if (clause.name !== void 0) {
314
- names.generatorFunctions.add(clause.name.text);
620
+ const name = (element.propertyName ?? element.name).text;
621
+ const binding = isGenerator ? name === DEFAULT_EXPORT$1 ? { kind: "generator-function" } : { kind: "helper-import", name } : { kind: "typescript-import", name };
622
+ into.set(element, binding);
315
623
  }
316
- namedImports(clause, names.helperImports);
317
624
  }
318
- function addImport(statement, names) {
625
+ function addImport(statement, into) {
319
626
  const clause = statement.importClause;
320
- if (clause === void 0 || !ts.isStringLiteral(statement.moduleSpecifier)) {
627
+ const specifier = ts.isStringLiteral(statement.moduleSpecifier) ? statement.moduleSpecifier.text : "";
628
+ const isGenerator = GENERATOR_MODULES.has(specifier);
629
+ if (clause === void 0 || !isGenerator && !TYPESCRIPT_MODULES.has(specifier)) {
321
630
  return;
322
631
  }
323
- const specifier = statement.moduleSpecifier.text;
324
- if (TYPESCRIPT_MODULES.has(specifier)) {
325
- addTypescriptImport(clause, names);
326
- } else if (GENERATOR_MODULES.has(specifier)) {
327
- addGeneratorImport(clause, names);
632
+ if (clause.name !== void 0) {
633
+ into.set(clause, isGenerator ? { kind: "generator-function" } : { kind: "typescript-module" });
634
+ }
635
+ const namespace = clause.namedBindings;
636
+ if (namespace !== void 0 && ts.isNamespaceImport(namespace)) {
637
+ const kind = isGenerator ? "generator-module" : "typescript-module";
638
+ into.set(namespace, { kind });
328
639
  }
640
+ addNamedImports(clause, isGenerator, into);
329
641
  }
330
- function addConstants(statement, names) {
642
+ function addConstants(statement, into) {
331
643
  if ((statement.declarationList.flags & ts.NodeFlags.Const) === 0) {
332
644
  return;
333
645
  }
334
646
  for (const declaration of statement.declarationList.declarations) {
335
- const initializer = declaration.initializer;
336
- if (!ts.isIdentifier(declaration.name) || initializer === void 0) {
337
- continue;
338
- }
339
- names.constants.set(declaration.name.text, initializer);
340
- if (ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer)) {
341
- names.functions.set(declaration.name.text, initializer);
647
+ if (ts.isIdentifier(declaration.name) && declaration.initializer !== void 0) {
648
+ into.set(declaration, { kind: "constant", initializer: declaration.initializer });
342
649
  }
343
650
  }
344
651
  }
345
- function collectFileNames(file) {
346
- const names = {
347
- typescriptModules: /* @__PURE__ */ new Set(),
348
- typescriptImports: /* @__PURE__ */ new Map(),
349
- helperImports: /* @__PURE__ */ new Map(),
350
- constants: /* @__PURE__ */ new Map(),
351
- generatorFunctions: /* @__PURE__ */ new Set(),
352
- functions: /* @__PURE__ */ new Map()
353
- };
652
+ function collectBindings(file) {
653
+ const bindings = /* @__PURE__ */ new Map();
354
654
  for (const statement of file.statements) {
355
655
  if (ts.isImportDeclaration(statement)) {
356
- addImport(statement, names);
656
+ addImport(statement, bindings);
357
657
  } else if (ts.isVariableStatement(statement)) {
358
- addConstants(statement, names);
658
+ addConstants(statement, bindings);
359
659
  } else if (ts.isFunctionDeclaration(statement) && statement.name !== void 0) {
360
- names.functions.set(statement.name.text, statement);
660
+ bindings.set(statement, { kind: "function", declaration: statement });
661
+ }
662
+ }
663
+ return bindings;
664
+ }
665
+ function singleFileHost(file) {
666
+ return {
667
+ getSourceFile: (name) => name === file.fileName ? file : void 0,
668
+ getDefaultLibFileName: () => "lib.d.ts",
669
+ writeFile: () => void 0,
670
+ getCurrentDirectory: () => "",
671
+ getCanonicalFileName: (name) => name,
672
+ useCaseSensitiveFileNames: () => true,
673
+ getNewLine: () => "\n",
674
+ fileExists: (name) => name === file.fileName,
675
+ readFile: () => void 0
676
+ };
677
+ }
678
+ function createChecker(file) {
679
+ const options = { noLib: true, noResolve: true, allowJs: true, types: [] };
680
+ const host = singleFileHost(file);
681
+ return ts.createProgram({ rootNames: [file.fileName], options, host }).getTypeChecker();
682
+ }
683
+ function symbolOf(checker, identifier) {
684
+ const parent = identifier.parent;
685
+ return ts.isShorthandPropertyAssignment(parent) && parent.name === identifier ? checker.getShorthandAssignmentValueSymbol(parent) : checker.getSymbolAtLocation(identifier);
686
+ }
687
+ function isDeclaredName(identifier, declaration) {
688
+ const declaredName = declaration.name;
689
+ return declaredName === identifier || ts.findAncestor(identifier, ts.isImportDeclaration) !== void 0;
690
+ }
691
+ function indexUses(file, bindings) {
692
+ const uses = /* @__PURE__ */ new Map();
693
+ const visit = (node) => {
694
+ const declaration = ts.isIdentifier(node) ? bindings.declarationOf(node) : void 0;
695
+ if (declaration !== void 0 && !isDeclaredName(node, declaration)) {
696
+ uses.set(declaration, [...uses.get(declaration) ?? [], node]);
361
697
  }
698
+ ts.forEachChild(node, visit);
699
+ };
700
+ visit(file);
701
+ return uses;
702
+ }
703
+ class CheckedFileBindings {
704
+ hasGenerator;
705
+ #file;
706
+ #bindings;
707
+ declarations;
708
+ #checker;
709
+ #uses;
710
+ constructor(file) {
711
+ this.#file = file;
712
+ this.#bindings = collectBindings(file);
713
+ this.declarations = this.#bindings;
714
+ this.hasGenerator = [...this.#bindings.values()].some(
715
+ (binding) => binding.kind === "generator-function" || binding.kind === "generator-module"
716
+ );
717
+ }
718
+ declarationOf(identifier) {
719
+ this.#checker ??= createChecker(this.#file);
720
+ const declaration = symbolOf(this.#checker, identifier)?.declarations?.[0];
721
+ return declaration !== void 0 && this.#bindings.has(declaration) ? declaration : void 0;
722
+ }
723
+ bindingOf(identifier) {
724
+ const declaration = this.declarationOf(identifier);
725
+ return declaration === void 0 ? void 0 : this.#bindings.get(declaration);
726
+ }
727
+ usesOf(declaration) {
728
+ this.#uses ??= indexUses(this.#file, this);
729
+ return this.#uses.get(declaration) ?? [];
730
+ }
731
+ }
732
+ function createFileBindings(file) {
733
+ return new CheckedFileBindings(file);
734
+ }
735
+
736
+ const DEFAULT_EXPORT = "default";
737
+ function isGenerator(callee, bindings) {
738
+ if (ts.isIdentifier(callee)) {
739
+ return bindings.bindingOf(callee)?.kind === "generator-function";
740
+ }
741
+ return ts.isPropertyAccessExpression(callee) && callee.name.text === DEFAULT_EXPORT && ts.isIdentifier(callee.expression) && bindings.bindingOf(callee.expression)?.kind === "generator-module";
742
+ }
743
+ function unwrapped(expression) {
744
+ if (ts.isParenthesizedExpression(expression) || ts.isAsExpression(expression) || ts.isSatisfiesExpression(expression)) {
745
+ return unwrapped(expression.expression);
746
+ }
747
+ return expression;
748
+ }
749
+ const isExported$1 = (declaration) => (ts.getCombinedModifierFlags(declaration) & ts.ModifierFlags.Export) !== 0;
750
+ function constantOptions(name, bindings) {
751
+ const binding = bindings.bindingOf(name);
752
+ const declaration = bindings.declarationOf(name);
753
+ const value = binding?.kind === "constant" ? unwrapped(binding.initializer) : void 0;
754
+ if (value === void 0 || !ts.isObjectLiteralExpression(value)) {
755
+ return { kind: "unknown", expression: name };
756
+ }
757
+ const isOwn = bindings.usesOf(declaration).length === 1;
758
+ return { kind: isOwn && !isExported$1(declaration) ? "own" : "shared", options: value };
759
+ }
760
+ function optionsOf(argument, bindings) {
761
+ const value = unwrapped(argument);
762
+ if (ts.isObjectLiteralExpression(value)) {
763
+ return { kind: "own", options: value };
362
764
  }
363
- return names;
765
+ return ts.isIdentifier(value) ? constantOptions(value, bindings) : { kind: "unknown", expression: argument };
766
+ }
767
+ function generatorCalls(file, bindings) {
768
+ const calls = [];
769
+ const visit = (node) => {
770
+ if (ts.isCallExpression(node) && isGenerator(node.expression, bindings)) {
771
+ const argument = node.arguments[1];
772
+ const options = argument === void 0 ? void 0 : optionsOf(argument, bindings);
773
+ calls.push({ call: node, options });
774
+ }
775
+ ts.forEachChild(node, visit);
776
+ };
777
+ visit(file);
778
+ return calls;
364
779
  }
365
780
 
366
781
  const SCRIPT_KINDS = /* @__PURE__ */ new Map([
@@ -467,22 +882,19 @@ const HELPER_CONSTANTS = /* @__PURE__ */ new Map([
467
882
  ["UNKNOWN", "unknown"]
468
883
  ]);
469
884
  const TYPE_NAME_RE = /^[A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*)*$/;
470
- const plain = (text) => ({ text, operator: null });
471
- function factoryMethod(callee, names) {
472
- if (!ts.isPropertyAccessExpression(callee)) {
473
- return void 0;
885
+ const plain$1 = (text) => ({ text, operator: null });
886
+ function isTypescriptMember(target, member, names) {
887
+ if (ts.isIdentifier(target)) {
888
+ const binding = names.bindings.bindingOf(target);
889
+ return binding?.kind === "typescript-import" && binding.name === member;
474
890
  }
475
- const target = callee.expression;
476
- const isFactory = ts.isIdentifier(target) ? names.typescriptImports.get(target.text) === "factory" : ts.isPropertyAccessExpression(target) && target.name.text === "factory" && ts.isIdentifier(target.expression) && names.typescriptModules.has(target.expression.text);
477
- return isFactory ? callee.name.text : void 0;
891
+ return ts.isPropertyAccessExpression(target) && target.name.text === member && ts.isIdentifier(target.expression) && names.bindings.bindingOf(target.expression)?.kind === "typescript-module";
892
+ }
893
+ function factoryMethod(callee, names) {
894
+ return ts.isPropertyAccessExpression(callee) && isTypescriptMember(callee.expression, "factory", names) ? callee.name.text : void 0;
478
895
  }
479
896
  function syntaxKindMember(expression, names) {
480
- if (!ts.isPropertyAccessExpression(expression)) {
481
- return void 0;
482
- }
483
- const target = expression.expression;
484
- const isSyntaxKind = ts.isIdentifier(target) ? names.typescriptImports.get(target.text) === "SyntaxKind" : ts.isPropertyAccessExpression(target) && target.name.text === "SyntaxKind" && ts.isIdentifier(target.expression) && names.typescriptModules.has(target.expression.text);
485
- return isSyntaxKind ? expression.name.text : void 0;
897
+ return ts.isPropertyAccessExpression(expression) && isTypescriptMember(expression.expression, "SyntaxKind", names) ? expression.name.text : void 0;
486
898
  }
487
899
  function typeName(argument, names) {
488
900
  if (argument === void 0) {
@@ -496,6 +908,18 @@ function typeName(argument, names) {
496
908
  }
497
909
  return void 0;
498
910
  }
911
+ const digits = (digit) => `${digit}(?:_?${digit})*`;
912
+ const DECIMAL_LITERAL = `(?:(?:0|[1-9](?:_?\\d)*)(?:\\.(?:${digits("\\d")})?)?|\\.${digits("\\d")})(?:[eE][+-]?${digits("\\d")})?`;
913
+ const NUMERIC_LITERAL_RE = new RegExp(
914
+ `^(?:0[xX]${digits("[\\da-fA-F]")}|0[oO]${digits("[0-7]")}|0[bB]${digits("[01]")}|${DECIMAL_LITERAL})$`
915
+ );
916
+ function numericLiteralText(value) {
917
+ if (ts.isNumericLiteral(value)) {
918
+ const number = Number(value.text);
919
+ return Number.isFinite(number) ? String(number) : void 0;
920
+ }
921
+ return ts.isStringLiteralLike(value) && NUMERIC_LITERAL_RE.test(value.text) ? value.text : void 0;
922
+ }
499
923
  function literalText(argument, names) {
500
924
  if (argument === void 0 || !ts.isCallExpression(argument)) {
501
925
  return void 0;
@@ -512,7 +936,7 @@ function literalText(argument, names) {
512
936
  case "createStringLiteral":
513
937
  return value !== void 0 && ts.isStringLiteralLike(value) ? JSON.stringify(value.text) : void 0;
514
938
  case "createNumericLiteral":
515
- return value !== void 0 && (ts.isNumericLiteral(value) || ts.isStringLiteralLike(value)) ? String(Number(value.text)) : void 0;
939
+ return value === void 0 ? void 0 : numericLiteralText(value);
516
940
  default:
517
941
  return void 0;
518
942
  }
@@ -535,15 +959,15 @@ function factoryCallText(call, method, names) {
535
959
  switch (method) {
536
960
  case "createTypeReferenceNode": {
537
961
  const name = second === void 0 ? typeName(first, names) : void 0;
538
- return name === void 0 ? void 0 : plain(name);
962
+ return name === void 0 ? void 0 : plain$1(name);
539
963
  }
540
964
  case "createKeywordTypeNode": {
541
965
  const keyword = first === void 0 ? void 0 : KEYWORDS.get(syntaxKindMember(first, names) ?? "");
542
- return keyword === void 0 ? void 0 : plain(keyword);
966
+ return keyword === void 0 ? void 0 : plain$1(keyword);
543
967
  }
544
968
  case "createLiteralTypeNode": {
545
969
  const literal = literalText(first, names);
546
- return literal === void 0 ? void 0 : plain(literal);
970
+ return literal === void 0 ? void 0 : plain$1(literal);
547
971
  }
548
972
  case "createUnionTypeNode":
549
973
  return joined(first, "|", names);
@@ -551,7 +975,7 @@ function factoryCallText(call, method, names) {
551
975
  return joined(first, "&", names);
552
976
  case "createArrayTypeNode": {
553
977
  const element = first === void 0 ? void 0 : typeTextOf(first, names);
554
- return element === void 0 ? void 0 : plain(element.operator ? `(${element.text})[]` : `${element.text}[]`);
978
+ return element === void 0 ? void 0 : plain$1(element.operator ? `(${element.text})[]` : `${element.text}[]`);
555
979
  }
556
980
  default:
557
981
  return void 0;
@@ -561,20 +985,25 @@ function helperCallText(call, names) {
561
985
  if (!ts.isIdentifier(call.expression)) {
562
986
  return void 0;
563
987
  }
564
- const helper = names.helperImports.get(call.expression.text);
988
+ const binding = names.bindings.bindingOf(call.expression);
989
+ const helper = binding?.kind === "helper-import" ? binding.name : void 0;
565
990
  if (helper === "tsUnion") {
566
991
  return joined(call.arguments[0], "|", names);
567
992
  }
568
993
  return helper === "tsIntersection" ? joined(call.arguments[0], "&", names) : void 0;
569
994
  }
570
995
  function identifierText(identifier, names) {
571
- const helper = names.helperImports.get(identifier.text);
572
- const constant = helper === void 0 ? void 0 : HELPER_CONSTANTS.get(helper);
573
- if (constant !== void 0) {
574
- return plain(constant);
996
+ const declaration = names.bindings.declarationOf(identifier);
997
+ const binding = names.bindings.bindingOf(identifier);
998
+ if (binding?.kind === "helper-import") {
999
+ const constant = HELPER_CONSTANTS.get(binding.name);
1000
+ return constant === void 0 ? void 0 : plain$1(constant);
575
1001
  }
576
- const initializer = names.constants.get(identifier.text);
577
- return initializer === void 0 ? void 0 : typeTextOf(initializer, names);
1002
+ if (binding?.kind !== "constant" || names.following.has(declaration)) {
1003
+ return void 0;
1004
+ }
1005
+ const following = /* @__PURE__ */ new Set([...names.following, declaration]);
1006
+ return typeTextOf(binding.initializer, { ...names, following });
578
1007
  }
579
1008
  function typeTextOf(expression, names) {
580
1009
  if (ts.isParenthesizedExpression(expression)) {
@@ -644,6 +1073,112 @@ function returnEdits(expression, names) {
644
1073
  return typeTextEdit(expression, names);
645
1074
  }
646
1075
 
1076
+ const isExported = (node) => (ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Export) !== 0;
1077
+ function isRemovable(node, binding, names) {
1078
+ switch (binding.kind) {
1079
+ case "typescript-module":
1080
+ case "typescript-import":
1081
+ case "helper-import":
1082
+ return true;
1083
+ case "constant":
1084
+ return !isExported(node) && typeTextOf(binding.initializer, names) !== void 0;
1085
+ default:
1086
+ return false;
1087
+ }
1088
+ }
1089
+ const isInside = (node, ranges) => ranges.some((range) => node.getStart() >= range.start && node.getEnd() <= range.end);
1090
+ function unusedDeclarations(names, replaced) {
1091
+ const ranges = replaced.filter((edit) => edit.end > edit.start);
1092
+ const removed = [];
1093
+ for (let isChanged = true; isChanged; ) {
1094
+ isChanged = false;
1095
+ for (const [node, binding] of names.bindings.declarations) {
1096
+ const uses = names.bindings.usesOf(node);
1097
+ const isUnused = uses.length > 0 && uses.every((use) => isInside(use, ranges));
1098
+ const isNew = removed.every((found) => found.node !== node);
1099
+ if (isNew && isUnused && isRemovable(node, binding, names)) {
1100
+ const name = node.name?.getText() ?? "";
1101
+ removed.push({ node, name });
1102
+ ranges.push({ start: node.getStart(), end: node.getEnd() });
1103
+ isChanged = true;
1104
+ }
1105
+ }
1106
+ }
1107
+ return removed;
1108
+ }
1109
+ function statementRemoval(statement, text) {
1110
+ const start = statement.getStart(void 0, true);
1111
+ const end = statement.getEnd();
1112
+ const lineStart = text.lastIndexOf("\n", start - 1) + 1;
1113
+ const lineBreak = text.indexOf("\n", end);
1114
+ const lineEnd = lineBreak === -1 ? text.length : lineBreak + 1;
1115
+ const isAlone = text.slice(lineStart, start).trim() === "" && text.slice(end, lineEnd).trim() === "";
1116
+ return isAlone ? { start: lineStart, end: lineEnd, text: "" } : { start, end, text: "" };
1117
+ }
1118
+ function listRemovals(elements, isRemoved) {
1119
+ const lastKept = elements.findLastIndex((element) => !isRemoved(element));
1120
+ const edits = [];
1121
+ elements.forEach((element, index) => {
1122
+ const next = elements[index + 1];
1123
+ if (index < lastKept && isRemoved(element) && next !== void 0) {
1124
+ edits.push({ start: element.getStart(), end: next.getStart(), text: "" });
1125
+ }
1126
+ });
1127
+ const last = elements.at(-1);
1128
+ if (lastKept !== -1 && last !== void 0 && lastKept < elements.length - 1) {
1129
+ edits.push({ start: elements[lastKept].getEnd(), end: last.getEnd(), text: "" });
1130
+ }
1131
+ return edits;
1132
+ }
1133
+ function importEdits(statement, isRemoved, text) {
1134
+ const clause = statement.importClause;
1135
+ const bindings = clause.namedBindings;
1136
+ const named = bindings !== void 0 && ts.isNamedImports(bindings) ? bindings : void 0;
1137
+ const namespace = bindings !== void 0 && ts.isNamespaceImport(bindings) ? bindings : void 0;
1138
+ const parts = [
1139
+ ...clause.name === void 0 ? [] : [clause],
1140
+ ...namespace === void 0 ? [] : [namespace],
1141
+ ...named?.elements ?? []
1142
+ ];
1143
+ if (parts.every(isRemoved)) {
1144
+ return [statementRemoval(statement, text)];
1145
+ }
1146
+ const defaultEnd = clause.name?.getEnd() ?? 0;
1147
+ const edits = [];
1148
+ if (clause.name !== void 0 && isRemoved(clause)) {
1149
+ edits.push({ start: clause.name.getStart(), end: bindings.getStart(), text: "" });
1150
+ }
1151
+ if (namespace !== void 0 && isRemoved(namespace)) {
1152
+ edits.push({ start: defaultEnd, end: namespace.getEnd(), text: "" });
1153
+ }
1154
+ if (named !== void 0 && named.elements.length > 0 && named.elements.every(isRemoved)) {
1155
+ edits.push({ start: defaultEnd, end: named.getEnd(), text: "" });
1156
+ } else if (named !== void 0) {
1157
+ edits.push(...listRemovals(named.elements, isRemoved));
1158
+ }
1159
+ return edits;
1160
+ }
1161
+ function statementEdits(statement, isRemoved, text) {
1162
+ if (ts.isImportDeclaration(statement)) {
1163
+ return importEdits(statement, isRemoved, text);
1164
+ }
1165
+ const declarations = statement.declarationList.declarations;
1166
+ return declarations.every(isRemoved) ? [statementRemoval(statement, text)] : listRemovals(declarations, isRemoved);
1167
+ }
1168
+ function removalEdits(removed, text) {
1169
+ const nodes = new Set(removed.map((found) => found.node));
1170
+ const isRemoved = (node) => nodes.has(node);
1171
+ const statements = new Set(
1172
+ removed.map(
1173
+ (found) => ts.findAncestor(
1174
+ found.node,
1175
+ (node) => ts.isVariableStatement(node) || ts.isImportDeclaration(node)
1176
+ )
1177
+ )
1178
+ );
1179
+ return [...statements].flatMap((statement) => statementEdits(statement, isRemoved, text));
1180
+ }
1181
+
647
1182
  const TRANSFORM = "transform";
648
1183
  const LEGACY_ONLY_CALLBACKS = ["postTransform", "transformProperty"];
649
1184
  const IR_CALLBACKS_OPTION = "irCallbacks";
@@ -653,6 +1188,14 @@ function propertyNamed(options, name) {
653
1188
  return key !== void 0 && (ts.isIdentifier(key) || ts.isStringLiteral(key)) && key.text === name;
654
1189
  });
655
1190
  }
1191
+ function namedFunction(name, names) {
1192
+ const binding = names.bindings.bindingOf(name);
1193
+ if (binding?.kind === "function") {
1194
+ return binding.declaration;
1195
+ }
1196
+ const value = binding?.kind === "constant" ? binding.initializer : void 0;
1197
+ return value !== void 0 && (ts.isArrowFunction(value) || ts.isFunctionExpression(value)) ? value : void 0;
1198
+ }
656
1199
  function callbackFunction(property, names) {
657
1200
  if (ts.isMethodDeclaration(property)) {
658
1201
  return property;
@@ -662,17 +1205,16 @@ function callbackFunction(property, names) {
662
1205
  }
663
1206
  const value = property.initializer;
664
1207
  if (ts.isIdentifier(value)) {
665
- return names.functions.get(value.text);
1208
+ return namedFunction(value, names);
666
1209
  }
667
1210
  return ts.isArrowFunction(value) || ts.isFunctionExpression(value) ? value : void 0;
668
1211
  }
669
- function isSharedFunction(property, text) {
1212
+ function isSharedFunction(property, names) {
670
1213
  if (!ts.isPropertyAssignment(property) || !ts.isIdentifier(property.initializer)) {
671
1214
  return false;
672
1215
  }
673
- const name = property.initializer.text.replaceAll("$", "\\$");
674
- const uses = text.match(new RegExp(`(?<![\\w$])${name}(?![\\w$])`, "g")) ?? [];
675
- return uses.length > 2;
1216
+ const declaration = names.bindings.declarationOf(property.initializer);
1217
+ return declaration !== void 0 && names.bindings.usesOf(declaration).length > 1;
676
1218
  }
677
1219
  function returnTypeEdits(callback, text) {
678
1220
  const type = callback.type;
@@ -687,7 +1229,7 @@ function transformEdits(property, names, text) {
687
1229
  if (callback?.body === void 0) {
688
1230
  return "not-a-function";
689
1231
  }
690
- if (isSharedFunction(property, text)) {
1232
+ if (isSharedFunction(property, names)) {
691
1233
  return "shared-function";
692
1234
  }
693
1235
  const edits = returnTypeEdits(callback, text);
@@ -741,35 +1283,66 @@ function migrateOptions(options, text, names) {
741
1283
  findings.changes.push({ kind: "transform-rewritten", line });
742
1284
  return findings;
743
1285
  }
744
- function generatorOptions(node, names) {
745
- if (!ts.isCallExpression(node) || !ts.isIdentifier(node.expression)) {
746
- return void 0;
1286
+ function sharedOptionsFindings(options, text) {
1287
+ const findings = { edits: [], changes: [], manualSteps: [] };
1288
+ if (propertyNamed(options, IR_CALLBACKS_OPTION) !== void 0) {
1289
+ return findings;
1290
+ }
1291
+ findings.manualSteps.push(...legacyCallbackSteps(options, text));
1292
+ const transform = propertyNamed(options, TRANSFORM);
1293
+ if (transform !== void 0) {
1294
+ const line = lineAt(text, transform.getStart());
1295
+ findings.manualSteps.push({ kind: "transform-not-rewritten", line, reason: "shared-options" });
747
1296
  }
748
- const options = node.arguments[1];
749
- const isGenerator = names.generatorFunctions.has(node.expression.text);
750
- return isGenerator && options !== void 0 && ts.isObjectLiteralExpression(options) ? options : void 0;
1297
+ return findings;
751
1298
  }
752
- function applyEdits(text, edits) {
753
- return [...edits].sort((left, right) => right.start - left.start).reduce(
754
- (result, edit) => result.slice(0, edit.start) + edit.text + result.slice(edit.end),
755
- text
756
- );
1299
+ function callFindings(found, text, names) {
1300
+ const options = found.options;
1301
+ switch (options?.kind) {
1302
+ case void 0:
1303
+ return { edits: [], changes: [], manualSteps: [] };
1304
+ case "own":
1305
+ return migrateOptions(options.options, text, names);
1306
+ case "shared":
1307
+ return sharedOptionsFindings(options.options, text);
1308
+ case "unknown": {
1309
+ const line = lineAt(text, options.expression.getStart());
1310
+ return { edits: [], changes: [], manualSteps: [{ kind: "options-not-inspected", line }] };
1311
+ }
1312
+ }
757
1313
  }
758
1314
  function findCallFindings(file, text, names) {
759
1315
  const all = { edits: [], changes: [], manualSteps: [] };
760
- const visit = (node) => {
761
- const options = generatorOptions(node, names);
1316
+ const seen = /* @__PURE__ */ new Set();
1317
+ for (const found of generatorCalls(file, names.bindings)) {
1318
+ const options = found.options?.kind === "unknown" ? void 0 : found.options?.options;
1319
+ if (options !== void 0 && seen.has(options)) {
1320
+ continue;
1321
+ }
762
1322
  if (options !== void 0) {
763
- const found = migrateOptions(options, text, names);
764
- all.edits.push(...found.edits);
765
- all.changes.push(...found.changes);
766
- all.manualSteps.push(...found.manualSteps);
1323
+ seen.add(options);
767
1324
  }
768
- ts.forEachChild(node, visit);
769
- };
770
- visit(file);
1325
+ const findings = callFindings(found, text, names);
1326
+ all.edits.push(...findings.edits);
1327
+ all.changes.push(...findings.changes);
1328
+ all.manualSteps.push(...findings.manualSteps);
1329
+ }
771
1330
  return all;
772
1331
  }
1332
+ function rewriteFile(file, names) {
1333
+ const text = file.text;
1334
+ const { edits, changes, manualSteps } = findCallFindings(file, text, names);
1335
+ const removed = unusedDeclarations(names, edits);
1336
+ for (const found of removed) {
1337
+ changes.push({
1338
+ kind: "declaration-removed",
1339
+ line: lineAt(text, found.node.getStart()),
1340
+ name: found.name
1341
+ });
1342
+ }
1343
+ const allEdits = [...edits, ...removalEdits(removed, text)];
1344
+ return { text: applyTextEdits(text, allEdits), changes, manualSteps };
1345
+ }
773
1346
  class TypescriptCallbackRewriter {
774
1347
  rewrite(text, path) {
775
1348
  const scriptKind = plainScriptKind(path);
@@ -777,12 +1350,11 @@ class TypescriptCallbackRewriter {
777
1350
  return unchanged(text);
778
1351
  }
779
1352
  const file = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true, scriptKind);
780
- const names = collectFileNames(file);
781
- if (names.generatorFunctions.size === 0) {
1353
+ const bindings = createFileBindings(file);
1354
+ if (!bindings.hasGenerator) {
782
1355
  return unchanged(text);
783
1356
  }
784
- const { edits, changes, manualSteps } = findCallFindings(file, text, names);
785
- return { text: applyEdits(text, edits), changes, manualSteps };
1357
+ return rewriteFile(file, { bindings, following: /* @__PURE__ */ new Set() });
786
1358
  }
787
1359
  }
788
1360
  function createTypescriptCallbackRewriter() {
@@ -853,15 +1425,18 @@ function isModuleSpecifier(literal) {
853
1425
  }
854
1426
  return ts.isExternalModuleReference(parent);
855
1427
  }
1428
+ function stringValue(node, file) {
1429
+ return ts.isTemplateExpression(node) ? file.text.slice(node.getStart(file) + 1, node.getEnd() - 1) : node.text;
1430
+ }
856
1431
  function collectStrings(root, file, offset, found) {
857
1432
  const visit = (node) => {
858
- if (ts.isStringLiteralLike(node)) {
1433
+ if (ts.isStringLiteralLike(node) || ts.isTemplateExpression(node)) {
859
1434
  const start = offset + node.getStart(file) + 1;
860
1435
  const end = offset + node.getEnd() - 1;
861
1436
  found.set(start, {
862
1437
  start,
863
1438
  end,
864
- value: node.text,
1439
+ value: stringValue(node, file),
865
1440
  isModuleSpecifier: isModuleSpecifier(node)
866
1441
  });
867
1442
  }
@@ -932,12 +1507,166 @@ function createTypescriptStringScanner() {
932
1507
  return new TypescriptSourceStringScanner();
933
1508
  }
934
1509
 
1510
+ const plain = (value) => value;
1511
+ const singleQuoted$1 = (value) => `'${value.replaceAll("'", "''")}'`;
1512
+ const doubleQuoted$1 = (value) => JSON.stringify(value);
1513
+ const QUOTINGS = {
1514
+ [Scalar.PLAIN]: [plain, singleQuoted$1, doubleQuoted$1],
1515
+ [Scalar.QUOTE_SINGLE]: [singleQuoted$1, doubleQuoted$1],
1516
+ [Scalar.QUOTE_DOUBLE]: [doubleQuoted$1]
1517
+ };
1518
+ function readsBack(written, value, isFlow) {
1519
+ const [document] = parseAllDocuments(isFlow ? `[${written}]` : `- ${written}`);
1520
+ const list = document?.contents;
1521
+ const item = isSeq(list) && list.items.length === 1 ? list.items[0] : void 0;
1522
+ return document?.errors.length === 0 && isScalar$1(item) && item.value === value;
1523
+ }
1524
+ function encoderFor(quotings, isFlow) {
1525
+ return (value) => quotings.map((quoting) => quoting(value)).find((text) => readsBack(text, value, isFlow)) ?? doubleQuoted$1(value);
1526
+ }
1527
+ function blockContent(token) {
1528
+ const headerLength = token.props.reduce((length, prop) => length + CST.stringify(prop).length, 0);
1529
+ const start = token.offset + headerLength;
1530
+ return { start, end: start + token.source.trimEnd().length };
1531
+ }
1532
+ function contentRange(token, node) {
1533
+ switch (token?.type) {
1534
+ case "scalar":
1535
+ return { start: token.offset, end: token.offset + token.source.length };
1536
+ case "single-quoted-scalar":
1537
+ case "double-quoted-scalar":
1538
+ return { start: token.offset + 1, end: token.offset + token.source.length - 1 };
1539
+ case "block-scalar":
1540
+ return blockContent(token);
1541
+ default:
1542
+ return node;
1543
+ }
1544
+ }
1545
+ function spanOf(node) {
1546
+ const [start, end] = node.range ?? [0, 0];
1547
+ return { start, end };
1548
+ }
1549
+ function scalarText(node) {
1550
+ if (typeof node.value === "string") {
1551
+ return node.value;
1552
+ }
1553
+ const isWritten = node.type === Scalar.PLAIN && node.value !== null;
1554
+ return isWritten ? node.source : void 0;
1555
+ }
1556
+ function convertScalar(node, isFlow) {
1557
+ const span = spanOf(node);
1558
+ const value = scalarText(node);
1559
+ if (value === void 0) {
1560
+ return { kind: "other", ...span };
1561
+ }
1562
+ const quotings = node.type === void 0 ? void 0 : QUOTINGS[node.type];
1563
+ const isReplaceable = quotings !== void 0 && node.anchor === void 0 && node.tag === void 0;
1564
+ return {
1565
+ kind: "scalar",
1566
+ value,
1567
+ content: contentRange(node.srcToken, span),
1568
+ isFolded: node.type !== Scalar.BLOCK_LITERAL,
1569
+ encode: isReplaceable ? encoderFor(quotings, isFlow) : void 0,
1570
+ ...span
1571
+ };
1572
+ }
1573
+ const flowOf = (conversion, node) => ({
1574
+ isFlow: conversion.isFlow || node.flow === true
1575
+ });
1576
+ function convert(node, conversion) {
1577
+ if (isScalar$1(node)) {
1578
+ return convertScalar(node, conversion.isFlow);
1579
+ }
1580
+ if (isSeq(node)) {
1581
+ const inner = flowOf(conversion, node);
1582
+ return { kind: "list", items: node.items.map((item) => convert(item, inner)), ...spanOf(node) };
1583
+ }
1584
+ if (isMap$1(node)) {
1585
+ const inner = flowOf(conversion, node);
1586
+ const entries = node.items.map((pair) => ({
1587
+ key: convert(pair.key, inner),
1588
+ value: convert(pair.value, inner)
1589
+ }));
1590
+ return { kind: "map", entries, ...spanOf(node) };
1591
+ }
1592
+ return { kind: "other", ...spanOf(isNode(node) ? node : {}) };
1593
+ }
1594
+ function duplicateKeyOffset(node) {
1595
+ if (isSeq(node)) {
1596
+ return firstOffset(node.items);
1597
+ }
1598
+ if (!isMap$1(node)) {
1599
+ return void 0;
1600
+ }
1601
+ const keys = /* @__PURE__ */ new Set();
1602
+ for (const pair of node.items) {
1603
+ if (isScalar$1(pair.key) && keys.has(pair.key.value)) {
1604
+ return spanOf(pair.key).start;
1605
+ }
1606
+ keys.add(isScalar$1(pair.key) ? pair.key.value : pair.key);
1607
+ }
1608
+ return firstOffset(node.items.flatMap((pair) => [pair.key, pair.value]));
1609
+ }
1610
+ function firstOffset(nodes) {
1611
+ for (const node of nodes) {
1612
+ const offset = duplicateKeyOffset(node);
1613
+ if (offset !== void 0) {
1614
+ return offset;
1615
+ }
1616
+ }
1617
+ return void 0;
1618
+ }
1619
+ const YAML_PARSE_OPTIONS = { keepSourceTokens: true, uniqueKeys: false };
1620
+ function read(text) {
1621
+ const documents = parseAllDocuments(text, YAML_PARSE_OPTIONS);
1622
+ const error = documents.flatMap((document) => document.errors)[0];
1623
+ if (error !== void 0) {
1624
+ return { kind: "unreadable", offset: error.pos[0] };
1625
+ }
1626
+ const duplicate = firstOffset(documents.map((document) => document.contents));
1627
+ if (duplicate !== void 0) {
1628
+ return { kind: "unreadable", offset: duplicate };
1629
+ }
1630
+ const roots = documents.flatMap(
1631
+ (document) => document.contents === null ? [] : [convert(document.contents, { isFlow: false })]
1632
+ );
1633
+ return { kind: "read", documents: roots };
1634
+ }
1635
+ function createYamlTextReader() {
1636
+ return { read };
1637
+ }
1638
+
1639
+ const CATALOG_PROTOCOL = "catalog:";
1640
+ const DEFAULT_CATALOG = "default";
1641
+ function catalogOfSpec(spec) {
1642
+ if (!spec.startsWith(CATALOG_PROTOCOL)) {
1643
+ return void 0;
1644
+ }
1645
+ return spec.slice(CATALOG_PROTOCOL.length) || DEFAULT_CATALOG;
1646
+ }
1647
+ class CatalogEntries {
1648
+ #keys = /* @__PURE__ */ new Set();
1649
+ add(catalog, name) {
1650
+ this.#keys.add(JSON.stringify([catalog, name]));
1651
+ }
1652
+ has(catalog, name) {
1653
+ return this.#keys.has(JSON.stringify([catalog, name]));
1654
+ }
1655
+ }
1656
+ const NO_CATALOG_PLAN = {
1657
+ inUse: new CatalogEntries(),
1658
+ kept: new CatalogEntries()
1659
+ };
1660
+
935
1661
  const PACKAGE_RENAMES = /* @__PURE__ */ new Map([
936
1662
  ["openapi-typescript", "openapi-typesafe"],
937
1663
  ["openapi-fetch", "openapi-typesafe-fetch"],
938
1664
  ["openapi-react-query", "openapi-typesafe-react-query"],
939
1665
  ["openapi-typescript-helpers", "openapi-typesafe-helpers"]
940
1666
  ]);
1667
+ function namesOriginalPackage(text) {
1668
+ return [...PACKAGE_RENAMES.keys()].some((name) => text.includes(name));
1669
+ }
941
1670
  const GENERATOR_COMMAND = "openapi-typescript";
942
1671
  const MIGRATED_GENERATOR_COMMAND = "openapi-typesafe";
943
1672
  const FLAG_RENAMES = /* @__PURE__ */ new Map([
@@ -953,71 +1682,328 @@ const DEPENDENCY_FIELDS = [
953
1682
  "peerDependencies",
954
1683
  "optionalDependencies"
955
1684
  ];
1685
+ const NAME_KEYED_FIELDS = ["peerDependenciesMeta", "dependenciesMeta"];
1686
+ const NAME_LIST_FIELDS = ["bundleDependencies", "bundledDependencies"];
1687
+ const CATALOG_FIELD = "catalog";
1688
+ const NAMED_CATALOGS_FIELD = "catalogs";
1689
+ const WORKSPACES_FIELD = "workspaces";
956
1690
  const OVERRIDE_FIELDS = ["overrides", "resolutions", "pnpm"];
957
1691
  const OLD_PEER_DEPENDENTS = /* @__PURE__ */ new Map([
958
1692
  ["swr-openapi", { peer: "openapi-fetch", range: ">=0.15" }]
959
1693
  ]);
960
-
961
- const COMMAND_RE = new RegExp(`(?<=^|[\\s"'\`;&|(])${GENERATOR_COMMAND}(?=$|[\\s"'\`;&|)])`, "g");
962
- const VERSIONED_COMMAND_RE = new RegExp(`(?<=^|[\\s"'\`;&|(])${GENERATOR_COMMAND}@`);
963
- const MIGRATED_COMMAND_RE = new RegExp(
964
- `(?<=^|[\\s"'\`;&|(])${MIGRATED_GENERATOR_COMMAND}(?=$|[\\s"'\`;&|)])`
965
- );
966
- const FLAG_RE = new RegExp(`(?<=\\s)(${[...FLAG_RENAMES.keys()].join("|")})(?=$|[\\s=])`, "g");
967
- function renameFlags(text, location, changes) {
968
- if (!MIGRATED_COMMAND_RE.test(text)) {
969
- return text;
970
- }
971
- return text.replace(FLAG_RE, (flag) => {
972
- const renamed = FLAG_RENAMES.get(flag);
973
- changes.push({ kind: "flag-renamed", location, from: flag, to: renamed });
974
- return renamed;
975
- });
1694
+ const PATH_SEPARATOR$7 = "/";
1695
+ function renamedSpecifier(value) {
1696
+ const slash = value.indexOf(PATH_SEPARATOR$7);
1697
+ const name = slash === -1 ? value : value.slice(0, slash);
1698
+ const renamed = PACKAGE_RENAMES.get(name);
1699
+ if (renamed === void 0) {
1700
+ return void 0;
1701
+ }
1702
+ const subpath = value.slice(name.length);
1703
+ return { to: `${renamed}${subpath}`, isDeep: subpath !== "" && subpath !== PATH_SEPARATOR$7 };
976
1704
  }
977
- function migrateCommand(text, location) {
978
- const changes = [];
979
- const manualSteps = [];
980
- if (VERSIONED_COMMAND_RE.test(text)) {
981
- manualSteps.push({ kind: "versioned-command", location });
1705
+
1706
+ const isMap = (node) => node?.kind === "map";
1707
+ const isScalar = (node) => node.kind === "scalar";
1708
+ function textEntries(map) {
1709
+ return map.entries.flatMap(({ key, value }) => isScalar(key) ? [{ key, value }] : []);
1710
+ }
1711
+ function valuesAt(map, key) {
1712
+ return textEntries(map).filter((entry) => entry.key.value === key).map((entry) => entry.value);
1713
+ }
1714
+ function mapsAt(map, key) {
1715
+ return valuesAt(map, key).filter(isMap);
1716
+ }
1717
+ function stringsBelow(node, path) {
1718
+ switch (node.kind) {
1719
+ case "scalar":
1720
+ return [{ ...node, path }];
1721
+ case "list":
1722
+ return node.items.flatMap((item, index) => stringsBelow(item, [...path, index]));
1723
+ case "map":
1724
+ return textEntries(node).flatMap(
1725
+ ({ key, value }) => stringsBelow(value, [...path, key.value])
1726
+ );
1727
+ default:
1728
+ return [];
982
1729
  }
983
- const renamed = text.replace(COMMAND_RE, () => {
984
- changes.push({ kind: "command-renamed", location });
985
- return MIGRATED_GENERATOR_COMMAND;
986
- });
987
- return { text: renameFlags(renamed, location, changes), changes, manualSteps };
988
1730
  }
989
- function migrateCommandLines(text, file) {
990
- const changes = [];
991
- const manualSteps = [];
992
- const lines = text.split("\n").map((line, index) => {
993
- const migrated = migrateCommand(line, `${file}:${index + 1}`);
994
- changes.push(...migrated.changes);
995
- manualSteps.push(...migrated.manualSteps);
996
- return migrated.text;
997
- });
998
- return { text: lines.join("\n"), changes, manualSteps };
1731
+ function documentStrings(documents) {
1732
+ return documents.flatMap((document) => stringsBelow(document, []));
1733
+ }
1734
+ function ownerKey(path) {
1735
+ return path.findLast((part) => typeof part === "string");
1736
+ }
1737
+ function unreadableConfig(text, offset) {
1738
+ if (!namesOriginalPackage(text)) {
1739
+ return unchanged(text);
1740
+ }
1741
+ const position = offset === void 0 ? void 0 : { line: lineAt(text, offset), column: columnAt(text, offset) };
1742
+ return { ...unchanged(text), manualSteps: [{ kind: "config-unreadable", position }] };
999
1743
  }
1000
1744
 
1001
1745
  const PARENT_SEPARATOR = ">";
1002
- const PATH_SEPARATOR = "/";
1003
- const SCOPE_PREFIX = "@";
1746
+ const PATH_SEPARATOR$6 = "/";
1747
+ const SCOPE_PREFIX$2 = "@";
1004
1748
  function overrideTarget(key) {
1005
1749
  const child = key.split(PARENT_SEPARATOR).at(-1) ?? key;
1006
- const segments = child.split(PATH_SEPARATOR);
1007
- const isScoped = segments.length > 1 && segments.at(-2)?.startsWith(SCOPE_PREFIX) === true;
1008
- const name = isScoped ? segments.slice(-2).join(PATH_SEPARATOR) : segments.at(-1) ?? child;
1009
- const versionAt = name.indexOf(SCOPE_PREFIX, 1);
1750
+ const segments = child.split(PATH_SEPARATOR$6);
1751
+ const isScoped = segments.length > 1 && segments.at(-2)?.startsWith(SCOPE_PREFIX$2) === true;
1752
+ const name = isScoped ? segments.slice(-2).join(PATH_SEPARATOR$6) : segments.at(-1) ?? child;
1753
+ const versionAt = name.indexOf(SCOPE_PREFIX$2, 1);
1010
1754
  return versionAt === -1 ? name : name.slice(0, versionAt);
1011
1755
  }
1012
- function keysOf(value) {
1013
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
1756
+ function overriddenPackages(keys) {
1757
+ const targets = keys.map(overrideTarget);
1758
+ return [...new Set(targets.filter((target) => PACKAGE_RENAMES.has(target)))];
1759
+ }
1760
+
1761
+ const isEditable = (node) => node.kind === "scalar" && node.encode !== void 0;
1762
+ const replaceScalar = (scalar, value) => ({
1763
+ start: scalar.start,
1764
+ end: scalar.end,
1765
+ text: scalar.encode(value)
1766
+ });
1767
+ function specEntries(map) {
1768
+ return textEntries(map).flatMap(({ key, value }) => {
1769
+ if (!isEditable(key)) {
1770
+ return [];
1771
+ }
1772
+ const spec = isEditable(value) ? { value: value.value, replace: (to) => replaceScalar(value, to) } : void 0;
1773
+ return [{ name: key.value, spec, renameKey: (name) => replaceScalar(key, name) }];
1774
+ });
1775
+ }
1776
+ function keyEntries(map) {
1777
+ return textEntries(map).map(({ key }) => key).filter(isEditable).map((key) => ({ name: key.value, rename: (name) => replaceScalar(key, name) }));
1778
+ }
1779
+ function itemEntries(list) {
1780
+ return list.items.filter(isEditable).map((item) => ({ name: item.value, rename: (name) => replaceScalar(item, name) }));
1781
+ }
1782
+ function nestedKeys(node) {
1783
+ if (node.kind !== "map") {
1014
1784
  return [];
1015
1785
  }
1016
- return Object.entries(value).flatMap(([key, nested]) => [key, ...keysOf(nested)]);
1786
+ return textEntries(node).flatMap(({ key, value }) => [key.value, ...nestedKeys(value)]);
1017
1787
  }
1018
- function overriddenPackages(value) {
1019
- const targets = keysOf(value).map(overrideTarget);
1020
- return [...new Set(targets.filter((target) => PACKAGE_RENAMES.has(target)))];
1788
+
1789
+ const PATH_SEPARATOR$5 = ".";
1790
+ function catalogMaps(root, prefix = "") {
1791
+ const maps = mapsAt(root, CATALOG_FIELD).map((map) => ({
1792
+ name: DEFAULT_CATALOG,
1793
+ label: `${prefix}${CATALOG_FIELD}`,
1794
+ map
1795
+ }));
1796
+ for (const catalogs of mapsAt(root, NAMED_CATALOGS_FIELD)) {
1797
+ for (const { key, value } of textEntries(catalogs)) {
1798
+ if (isMap(value)) {
1799
+ const label = [prefix + NAMED_CATALOGS_FIELD, key.value].join(PATH_SEPARATOR$5);
1800
+ maps.push({ name: key.value, label, map: value });
1801
+ }
1802
+ }
1803
+ }
1804
+ return maps;
1805
+ }
1806
+ const manifestCatalogMaps = (manifest) => [
1807
+ ...catalogMaps(manifest),
1808
+ ...mapsAt(manifest, WORKSPACES_FIELD).flatMap(
1809
+ (workspaces) => catalogMaps(workspaces, `${WORKSPACES_FIELD}${PATH_SEPARATOR$5}`)
1810
+ )
1811
+ ];
1812
+ function entriesInUse(manifests) {
1813
+ const inUse = new CatalogEntries();
1814
+ const fields = manifests.flatMap(
1815
+ (manifest) => DEPENDENCY_FIELDS.flatMap((field) => mapsAt(manifest, field))
1816
+ );
1817
+ for (const dependencies of fields) {
1818
+ const entries = specEntries(dependencies);
1819
+ const names = new Set(entries.map((entry) => entry.name));
1820
+ for (const { name, spec } of entries) {
1821
+ const catalog = spec === void 0 ? void 0 : catalogOfSpec(spec.value);
1822
+ const to = PACKAGE_RENAMES.get(name);
1823
+ if (catalog !== void 0 && to !== void 0 && names.has(to)) {
1824
+ inUse.add(catalog, name);
1825
+ }
1826
+ }
1827
+ }
1828
+ return inUse;
1829
+ }
1830
+ function nestedTexts(node) {
1831
+ if (node.kind !== "map") {
1832
+ return [];
1833
+ }
1834
+ return textEntries(node).flatMap(
1835
+ ({ key, value }) => value.kind === "scalar" ? [{ key: key.value, value: value.value }] : nestedTexts(value)
1836
+ );
1837
+ }
1838
+ function addOverrideUses(documents, inUse) {
1839
+ const sections = documents.flatMap(
1840
+ (document) => OVERRIDE_FIELDS.flatMap((field) => valuesAt(document, field))
1841
+ );
1842
+ for (const { key, value } of sections.flatMap(nestedTexts)) {
1843
+ const catalog = catalogOfSpec(value);
1844
+ const name = overrideTarget(key);
1845
+ if (catalog !== void 0 && PACKAGE_RENAMES.has(name)) {
1846
+ inUse.add(catalog, name);
1847
+ }
1848
+ }
1849
+ }
1850
+ function planCatalogs({ manifests, catalogFiles }) {
1851
+ const inUse = entriesInUse(manifests);
1852
+ addOverrideUses([...manifests, ...catalogFiles], inUse);
1853
+ const kept = new CatalogEntries();
1854
+ const catalogs = [
1855
+ ...catalogFiles.flatMap((file) => catalogMaps(file)),
1856
+ ...manifests.flatMap(manifestCatalogMaps)
1857
+ ];
1858
+ for (const { name: catalog, map } of catalogs) {
1859
+ const entries = specEntries(map);
1860
+ const names = new Set(entries.map((entry) => entry.name));
1861
+ for (const { name, spec } of entries) {
1862
+ const to = PACKAGE_RENAMES.get(name);
1863
+ const stays = spec === void 0 || inUse.has(catalog, name);
1864
+ if (to !== void 0 && !names.has(to) && stays) {
1865
+ kept.add(catalog, name);
1866
+ }
1867
+ }
1868
+ }
1869
+ return { inUse, kept };
1870
+ }
1871
+
1872
+ const NO_LONGER_INSTALLED = /* @__PURE__ */ new Map([
1873
+ ["openapi-typesafe-helpers", ["openapi-fetch", "openapi-react-query"]]
1874
+ ]);
1875
+ const ORIGINAL_NAMES = new Map(
1876
+ [...PACKAGE_RENAMES].map(([original, successor]) => [successor, original])
1877
+ );
1878
+ const PATH_SEPARATOR$4 = "/";
1879
+ function namesUninstalledPackage(text) {
1880
+ return [...NO_LONGER_INSTALLED.keys()].some(
1881
+ (name) => text.includes(name) || text.includes(ORIGINAL_NAMES.get(name) ?? name)
1882
+ );
1883
+ }
1884
+ const directoryOf = (path) => path.slice(0, path.lastIndexOf(PATH_SEPARATOR$4) + 1);
1885
+ function declaredNames(manifest) {
1886
+ return DEPENDENCY_FIELDS.flatMap((field) => mapsAt(manifest, field)).flatMap(textEntries).map(({ key }) => key.value);
1887
+ }
1888
+ function isDeclaredFor(manifests, filePath, name) {
1889
+ const original = ORIGINAL_NAMES.get(name);
1890
+ const containing = manifests.filter(({ path }) => filePath.startsWith(directoryOf(path)));
1891
+ const declares = ({ names }) => names === void 0 || names.has(name) || original !== void 0 && names.has(original);
1892
+ return containing.length === 0 || containing.some(declares);
1893
+ }
1894
+ const packageOfSpecifier = (specifier) => {
1895
+ const slash = specifier.indexOf(PATH_SEPARATOR$4);
1896
+ return slash === -1 ? specifier : specifier.slice(0, slash);
1897
+ };
1898
+ function undeclaredImportSteps(imports, filePath, manifests) {
1899
+ return imports.flatMap(({ specifier, line }) => {
1900
+ const name = packageOfSpecifier(specifier);
1901
+ const installers = NO_LONGER_INSTALLED.get(name);
1902
+ return installers === void 0 || isDeclaredFor(manifests, filePath, name) ? [] : [{ kind: "package-not-declared", line, name, installers }];
1903
+ });
1904
+ }
1905
+
1906
+ function assertNever(value) {
1907
+ throw new Error(`Unexpected variant: ${JSON.stringify(value)}`);
1908
+ }
1909
+
1910
+ const KEPT_PROTOCOLS = ["catalog:", "workspace:"];
1911
+ const RANGE_SEPARATOR = "||";
1912
+ const HYPHEN = "-";
1913
+ const WHITE_SPACE_RE = /\s+/;
1914
+ const SPACED_OPERATOR_RE = /(<=|>=|<|>|=|~>|~|\^)\s+/g;
1915
+ const OPERATOR = String.raw`(<=|>=|<|>|=|~>|~|\^)?`;
1916
+ const VERSION_PART = String.raw`(\d+|[xX*])`;
1917
+ const LABELS = String.raw`(?:-[\w.-]+)?(?:\+[\w.-]+)?`;
1918
+ const COMPARATOR_RE = new RegExp(
1919
+ String.raw`^${OPERATOR}v?${VERSION_PART}(?:\.${VERSION_PART})?(?:\.${VERSION_PART})?${LABELS}$`
1920
+ );
1921
+ const NUMBER_RE = /^\d+$/;
1922
+ const OPERATOR_STYLES = {
1923
+ "^": "^",
1924
+ // `>=` means "this or newer"; with a new package line, a caret range says the same safely.
1925
+ ">=": "^",
1926
+ ">": "^",
1927
+ "~": "~",
1928
+ "~>": "~"
1929
+ };
1930
+ const isNumber = (part) => part !== void 0 && NUMBER_RE.test(part);
1931
+ function versionStyle(major, minor, patch) {
1932
+ if (!isNumber(major)) {
1933
+ return void 0;
1934
+ }
1935
+ if (isNumber(minor) && isNumber(patch)) {
1936
+ return "";
1937
+ }
1938
+ return isNumber(minor) ? "~" : "^";
1939
+ }
1940
+ function rangeStyle(spec) {
1941
+ const sets = spec.replace(SPACED_OPERATOR_RE, "$1").split(RANGE_SEPARATOR);
1942
+ const tokens = sets.map((set) => set.trim().split(WHITE_SPACE_RE));
1943
+ const comparators = tokens.flat().filter((token) => token !== HYPHEN);
1944
+ const matches = comparators.map((comparator) => COMPARATOR_RE.exec(comparator));
1945
+ const first = matches[0];
1946
+ if (first === void 0 || first === null || matches.includes(null)) {
1947
+ return void 0;
1948
+ }
1949
+ const operator = first[1];
1950
+ const isHyphenRange = tokens[0]?.[1] === HYPHEN;
1951
+ if (operator !== void 0 && operator !== "=") {
1952
+ return OPERATOR_STYLES[operator];
1953
+ }
1954
+ return isHyphenRange ? "^" : versionStyle(first[2], first[3], first[4]);
1955
+ }
1956
+ function migrateVersionSpec(spec, version) {
1957
+ if (KEPT_PROTOCOLS.some((protocol) => spec.startsWith(protocol))) {
1958
+ return { kind: "kept", spec, isReported: false };
1959
+ }
1960
+ const style = rangeStyle(spec);
1961
+ if (style === void 0) {
1962
+ return { kind: "kept", spec, isReported: true };
1963
+ }
1964
+ return { kind: "replaced", spec: `${style}${version}` };
1965
+ }
1966
+
1967
+ const NPM_PROTOCOL = "npm:";
1968
+ const SCOPE_PREFIX$1 = "@";
1969
+ const PATH_SEPARATOR$3 = "/";
1970
+ const VERSION_SEPARATOR = "@";
1971
+ function nameEnd(text) {
1972
+ const searchFrom = text.startsWith(SCOPE_PREFIX$1) ? text.indexOf(PATH_SEPARATOR$3) + 1 : 0;
1973
+ const ends = [VERSION_SEPARATOR, PATH_SEPARATOR$3].map((separator) => text.indexOf(separator, Math.max(searchFrom, 1))).filter((index) => index !== -1);
1974
+ return ends.length === 0 ? text.length : Math.min(...ends);
1975
+ }
1976
+ function parseNpmSpec(spec) {
1977
+ if (!spec.startsWith(NPM_PROTOCOL)) {
1978
+ return void 0;
1979
+ }
1980
+ const body = spec.slice(NPM_PROTOCOL.length);
1981
+ const lead = body.startsWith(PATH_SEPARATOR$3) ? PATH_SEPARATOR$3 : "";
1982
+ const text = body.slice(lead.length);
1983
+ const end = nameEnd(text);
1984
+ const name = text.slice(0, end);
1985
+ if (!text.startsWith(VERSION_SEPARATOR, end)) {
1986
+ return { lead, name, range: void 0, rest: text.slice(end) };
1987
+ }
1988
+ const rangeEnd = text.indexOf(PATH_SEPARATOR$3, end);
1989
+ const restStart = rangeEnd === -1 ? text.length : rangeEnd;
1990
+ return { lead, name, range: text.slice(end + 1, restStart), rest: text.slice(restStart) };
1991
+ }
1992
+ function migrateNpmSpec(spec, version) {
1993
+ const parsed = parseNpmSpec(spec);
1994
+ const renamed = parsed === void 0 ? void 0 : PACKAGE_RENAMES.get(parsed.name);
1995
+ if (parsed === void 0 || renamed === void 0) {
1996
+ return void 0;
1997
+ }
1998
+ const head = `${NPM_PROTOCOL}${parsed.lead}${renamed}`;
1999
+ const isDeep = parsed.rest !== "" && parsed.rest !== PATH_SEPARATOR$3;
2000
+ if (parsed.range === void 0) {
2001
+ return { spec: `${head}${parsed.rest}`, isReported: false, isDeep };
2002
+ }
2003
+ const decision = migrateVersionSpec(parsed.range, version);
2004
+ const isReported = decision.kind === "kept" && decision.isReported;
2005
+ const migrated = `${head}${VERSION_SEPARATOR}${decision.spec}${parsed.rest}`;
2006
+ return { spec: migrated, isReported, isDeep };
1021
2007
  }
1022
2008
 
1023
2009
  const SCHEME_SEPARATOR = "://";
@@ -1032,140 +2018,1121 @@ function scanAuthority(text, start) {
1032
2018
  userinfoEnd = end;
1033
2019
  }
1034
2020
  }
1035
- return { end, userinfoEnd };
2021
+ return { end, userinfoEnd };
2022
+ }
2023
+ function hideUrlCredentials(text) {
2024
+ let hidden = "";
2025
+ let copiedUpTo = 0;
2026
+ for (let found = text.indexOf(SCHEME_SEPARATOR); found !== -1; ) {
2027
+ const start = found + SCHEME_SEPARATOR.length;
2028
+ const { end, userinfoEnd } = scanAuthority(text, start);
2029
+ if (userinfoEnd !== -1) {
2030
+ hidden += `${text.slice(copiedUpTo, start)}${HIDDEN_USERINFO}`;
2031
+ copiedUpTo = userinfoEnd;
2032
+ }
2033
+ found = text.indexOf(SCHEME_SEPARATOR, end);
2034
+ }
2035
+ return `${hidden}${text.slice(copiedUpTo)}`;
2036
+ }
2037
+
2038
+ const newFindings = () => ({ edits: [], changes: [], manualSteps: [] });
2039
+ const PEER_FIELD = "peerDependencies";
2040
+ const mapLabel = (map) => map.kind === "catalog" ? map.catalog : map.field;
2041
+ const renamedKey = (name, map) => map.kind === "imports" ? renamedSpecifier(name)?.to : PACKAGE_RENAMES.get(name);
2042
+ function migrateSpec(spec, version) {
2043
+ const npm = migrateNpmSpec(spec, version);
2044
+ if (npm !== void 0) {
2045
+ return { spec: npm.spec, isReported: npm.isReported };
2046
+ }
2047
+ const decision = migrateVersionSpec(spec, version);
2048
+ return { spec: decision.spec, isReported: decision.kind === "kept" && decision.isReported };
2049
+ }
2050
+ function isFree({ from, to, field, names }, findings) {
2051
+ if (names.has(to)) {
2052
+ findings.manualSteps.push({ kind: "name-collision", field, from, to });
2053
+ return false;
2054
+ }
2055
+ return true;
2056
+ }
2057
+ function migrateAlias(entry, { map, version, findings }) {
2058
+ const migrated = entry.spec === void 0 ? void 0 : migrateNpmSpec(entry.spec.value, version);
2059
+ if (entry.spec === void 0 || migrated === void 0) {
2060
+ return;
2061
+ }
2062
+ findings.edits.push(entry.spec.replace(migrated.spec));
2063
+ const spec = hideUrlCredentials(migrated.spec);
2064
+ findings.changes.push({ kind: "alias-renamed", field: mapLabel(map), name: entry.name, spec });
2065
+ if (migrated.isReported) {
2066
+ findings.manualSteps.push({ kind: "spec-kept", name: entry.name, spec });
2067
+ }
2068
+ }
2069
+ function renamedChange(map, from, to, spec) {
2070
+ switch (map.kind) {
2071
+ case "dependencies":
2072
+ return { kind: "dependency-renamed", field: map.field, from, to, spec };
2073
+ case "catalog":
2074
+ return { kind: "catalog-entry-renamed", catalog: map.catalog, from, to, spec };
2075
+ case "imports":
2076
+ return { kind: "import-renamed", field: map.field, from, to, target: spec };
2077
+ default:
2078
+ return assertNever(map);
2079
+ }
2080
+ }
2081
+ function migrateEntry(entry, spec, to, { map, version, findings }) {
2082
+ const old = spec.value;
2083
+ const migrated = migrateSpec(old, version);
2084
+ findings.edits.push(entry.renameKey(to));
2085
+ if (migrated.spec !== old) {
2086
+ findings.edits.push(spec.replace(migrated.spec));
2087
+ }
2088
+ findings.changes.push(renamedChange(map, entry.name, to, hideUrlCredentials(migrated.spec)));
2089
+ if (migrated.isReported) {
2090
+ findings.manualSteps.push({ kind: "spec-kept", name: to, spec: hideUrlCredentials(old) });
2091
+ }
2092
+ if (map.kind === "dependencies" && map.field === PEER_FIELD) {
2093
+ findings.manualSteps.push({ kind: "peer-dependency-renamed", name: to });
2094
+ }
2095
+ }
2096
+ function catalogHold(entry, spec, { map, catalogs }) {
2097
+ if (map.kind === "catalog") {
2098
+ const isInUse = catalogs.inUse.has(map.name, entry.name);
2099
+ return isInUse ? { kind: "catalog-entry-in-use", catalog: map.catalog, name: entry.name } : void 0;
2100
+ }
2101
+ const catalog = map.kind === "dependencies" ? catalogOfSpec(spec.value) : void 0;
2102
+ return catalog !== void 0 && catalogs.kept.has(catalog, entry.name) ? { kind: "catalog-reference-kept", field: map.field, name: entry.name, catalog } : void 0;
2103
+ }
2104
+ function migrateSpecMap(entries, context) {
2105
+ const names = new Set(entries.map((entry) => entry.name));
2106
+ for (const entry of entries) {
2107
+ const to = renamedKey(entry.name, context.map);
2108
+ const field = mapLabel(context.map);
2109
+ if (to === void 0) {
2110
+ migrateAlias(entry, context);
2111
+ continue;
2112
+ }
2113
+ if (!isFree({ from: entry.name, to, field, names }, context.findings)) {
2114
+ continue;
2115
+ }
2116
+ const hold = entry.spec === void 0 ? void 0 : catalogHold(entry, entry.spec, context);
2117
+ if (entry.spec === void 0) {
2118
+ context.findings.manualSteps.push({ kind: "entry-not-edited", field, name: entry.name });
2119
+ } else if (hold !== void 0) {
2120
+ context.findings.manualSteps.push(hold);
2121
+ } else {
2122
+ migrateEntry(entry, entry.spec, to, context);
2123
+ }
2124
+ }
2125
+ }
2126
+ function renameNames(entries, field, findings) {
2127
+ const names = new Set(entries.map((entry) => entry.name));
2128
+ for (const entry of entries) {
2129
+ const to = PACKAGE_RENAMES.get(entry.name);
2130
+ if (to !== void 0 && isFree({ from: entry.name, to, field, names }, findings)) {
2131
+ findings.edits.push(entry.rename(to));
2132
+ findings.changes.push({
2133
+ kind: "package-name-renamed",
2134
+ location: field,
2135
+ from: entry.name,
2136
+ to
2137
+ });
2138
+ }
2139
+ }
2140
+ }
2141
+
2142
+ const IMPORTS_FIELD = "imports";
2143
+ const SCOPES_FIELD = "scopes";
2144
+ const FIELD_SEPARATOR = ".";
2145
+ function migrateImports(config, version, findings) {
2146
+ for (const imports of mapsAt(config, IMPORTS_FIELD)) {
2147
+ const map = { kind: "imports", field: IMPORTS_FIELD };
2148
+ migrateSpecMap(specEntries(imports), { map, version, findings, catalogs: NO_CATALOG_PLAN });
2149
+ }
2150
+ for (const scopes of mapsAt(config, SCOPES_FIELD)) {
2151
+ for (const { key, value } of textEntries(scopes)) {
2152
+ if (isMap(value)) {
2153
+ const field = [SCOPES_FIELD, key.value].join(FIELD_SEPARATOR);
2154
+ const map = { kind: "imports", field };
2155
+ migrateSpecMap(specEntries(value), { map, version, findings, catalogs: NO_CATALOG_PLAN });
2156
+ }
2157
+ }
2158
+ }
2159
+ }
2160
+ const PATH_SEPARATOR$2 = "/";
2161
+ const SCOPE_PREFIX = "@";
2162
+ function importMapKeys(config) {
2163
+ const scoped = mapsAt(config, SCOPES_FIELD).flatMap(textEntries).flatMap(({ value }) => isMap(value) ? [value] : []);
2164
+ return [...mapsAt(config, IMPORTS_FIELD), ...scoped].flatMap(textEntries).map(({ key }) => key.value);
2165
+ }
2166
+ function importMapNames(config) {
2167
+ return importMapKeys(config).map((key) => {
2168
+ const parts = key.split(PATH_SEPARATOR$2);
2169
+ return key.startsWith(SCOPE_PREFIX) ? parts.slice(0, 2).join(PATH_SEPARATOR$2) : parts[0] ?? key;
2170
+ });
2171
+ }
2172
+ function migrateImportMap(text, read, version) {
2173
+ if (read.kind === "unreadable") {
2174
+ return unreadableConfig(text, read.offset);
2175
+ }
2176
+ const findings = newFindings();
2177
+ for (const config of read.documents.filter(isMap)) {
2178
+ migrateImports(config, version, findings);
2179
+ }
2180
+ const { edits, changes, manualSteps } = findings;
2181
+ return { text: applyTextEdits(text, edits), changes, manualSteps };
2182
+ }
2183
+
2184
+ const NONE = /* @__PURE__ */ new Set();
2185
+ const PACKAGE_OPTIONS = /* @__PURE__ */ new Set(["-p", "--package"]);
2186
+ const PACKAGE_MANAGER_OPTIONS = /* @__PURE__ */ new Set([
2187
+ "--filter",
2188
+ "-F",
2189
+ "--dir",
2190
+ "-C",
2191
+ "--prefix",
2192
+ "--cwd",
2193
+ "--workspace"
2194
+ ]);
2195
+ const launcher = (words, packageOptions = NONE, valueOptions = NONE) => {
2196
+ const allValueOptions = /* @__PURE__ */ new Set([...valueOptions, ...packageOptions]);
2197
+ return { words, packageOptions, valueOptions: allValueOptions, operands: 0 };
2198
+ };
2199
+ const CONTAINER_OPTIONS = /* @__PURE__ */ new Set([
2200
+ "-e",
2201
+ "--env",
2202
+ "--env-file",
2203
+ "-v",
2204
+ "--volume",
2205
+ "--mount",
2206
+ "-p",
2207
+ "--publish",
2208
+ "-w",
2209
+ "--workdir",
2210
+ "-u",
2211
+ "--user",
2212
+ "--name",
2213
+ "--network",
2214
+ "--entrypoint",
2215
+ "--platform",
2216
+ "-l",
2217
+ "--label",
2218
+ "-h",
2219
+ "--hostname",
2220
+ "--add-host"
2221
+ ]);
2222
+ const container = (words) => ({
2223
+ ...launcher(words, NONE, CONTAINER_OPTIONS),
2224
+ operands: 1
2225
+ });
2226
+ const LAUNCHERS = [
2227
+ launcher(["npm", "exec"], PACKAGE_OPTIONS),
2228
+ launcher(["npm", "x"], PACKAGE_OPTIONS),
2229
+ launcher(["pnpm", "exec"], NONE, PACKAGE_MANAGER_OPTIONS),
2230
+ launcher(["pnpm", "dlx"], /* @__PURE__ */ new Set(["--package"])),
2231
+ launcher(["yarn", "dlx"], PACKAGE_OPTIONS),
2232
+ launcher(["yarn", "exec"]),
2233
+ launcher(["bun", "x"], PACKAGE_OPTIONS),
2234
+ launcher(["deno", "run"], NONE, /* @__PURE__ */ new Set(["--config", "-c", "--import-map", "--location"])),
2235
+ launcher(["deno", "x"]),
2236
+ launcher(["npx"], PACKAGE_OPTIONS, /* @__PURE__ */ new Set(["-c", "--call"])),
2237
+ launcher(["pnpx"], PACKAGE_OPTIONS),
2238
+ launcher(["bunx"], PACKAGE_OPTIONS),
2239
+ launcher(["pnpm"], NONE, PACKAGE_MANAGER_OPTIONS),
2240
+ launcher(["yarn"], NONE, PACKAGE_MANAGER_OPTIONS),
2241
+ launcher(["node"], NONE, /* @__PURE__ */ new Set(["-r", "--require", "--import", "--loader", "--env-file"])),
2242
+ launcher(["cross-env"]),
2243
+ launcher(["dotenv"], NONE, /* @__PURE__ */ new Set(["-e", "-c", "-v"])),
2244
+ launcher(["env"], NONE, /* @__PURE__ */ new Set(["-u", "--unset", "-C", "--chdir"])),
2245
+ launcher(["exec"]),
2246
+ launcher(["time"]),
2247
+ launcher(["nohup"]),
2248
+ launcher(["sudo"], NONE, /* @__PURE__ */ new Set(["-u", "-g"])),
2249
+ // The shell form of Dockerfile instructions runs the rest of the line in a shell.
2250
+ launcher(["RUN"], NONE, /* @__PURE__ */ new Set(["--mount", "--network", "--security"])),
2251
+ launcher(["CMD"]),
2252
+ launcher(["ENTRYPOINT"]),
2253
+ container(["docker", "run"]),
2254
+ container(["docker", "exec"]),
2255
+ container(["docker", "compose", "run"]),
2256
+ container(["docker", "compose", "exec"]),
2257
+ container(["podman", "run"]),
2258
+ container(["podman", "exec"])
2259
+ ];
2260
+ const installers = (manager, subcommands) => subcommands.map((subcommand) => ({ words: [manager, subcommand] }));
2261
+ const NPM_INSTALL = ["install", "i", "add", "update", "up", "uninstall", "remove", "rm", "un"];
2262
+ const INSTALLERS = [
2263
+ ...installers("npm", NPM_INSTALL),
2264
+ ...installers("pnpm", ["add", "update", "up", "remove", "rm", "uninstall", "un"]),
2265
+ ...installers("yarn", ["add", "upgrade", "up", "remove"]),
2266
+ ...installers("bun", ["add", "a", "update", "remove", "rm", "install", "i"]),
2267
+ ...installers("deno", ["add", "install", "i", "remove", "uninstall"])
2268
+ ];
2269
+ const NPM_SPECIFIER_PREFIX = "npm:";
2270
+
2271
+ const ASSIGNMENT_RE$1 = /^[A-Za-z_][A-Za-z0-9_]*=/;
2272
+ const END_OF_OPTIONS = "--";
2273
+ const isOption = (word) => word.value.startsWith("-") && word.value.length > 1;
2274
+ function skipAssignments(words, start) {
2275
+ let index = start;
2276
+ while (index < words.length && ASSIGNMENT_RE$1.test(words[index].raw)) {
2277
+ index++;
2278
+ }
2279
+ return index;
2280
+ }
2281
+ function matchingPrefix(entries, words, index) {
2282
+ return entries.find(
2283
+ (entry) => entry.words.every((name, offset) => words[index + offset]?.value === name)
2284
+ );
2285
+ }
2286
+ function inlineOptionPackage(word, launcher) {
2287
+ const equals = word.value.indexOf("=");
2288
+ const name = word.value.slice(0, equals);
2289
+ return launcher.packageOptions.has(name) ? { word, value: word.value.slice(equals + 1) } : void 0;
2290
+ }
2291
+ function launchedProgram(words, start, launcher, packages) {
2292
+ let index = start;
2293
+ while (index < words.length && isOption(words[index])) {
2294
+ const option = words[index++];
2295
+ if (option.value === END_OF_OPTIONS) {
2296
+ break;
2297
+ }
2298
+ const inline = option.value.includes("=") ? inlineOptionPackage(option, launcher) : void 0;
2299
+ if (inline !== void 0) {
2300
+ packages.push(inline);
2301
+ }
2302
+ const value = words[index];
2303
+ if (!option.value.includes("=") && launcher.valueOptions.has(option.value) && value) {
2304
+ if (launcher.packageOptions.has(option.value)) {
2305
+ packages.push({ word: value, value: value.value });
2306
+ }
2307
+ index++;
2308
+ }
2309
+ }
2310
+ return index;
2311
+ }
2312
+ const installArguments = (words, start) => words.slice(start).filter((word) => !isOption(word)).map((word) => ({ word, value: word.value }));
2313
+ function commandRoles(words) {
2314
+ const packages = [];
2315
+ let index = skipAssignments(words, 0);
2316
+ while (index < words.length) {
2317
+ const installer = matchingPrefix(INSTALLERS, words, index);
2318
+ if (installer !== void 0) {
2319
+ packages.push(...installArguments(words, index + installer.words.length));
2320
+ return { program: void 0, packages };
2321
+ }
2322
+ const launcher = matchingPrefix(LAUNCHERS, words, index);
2323
+ if (launcher === void 0) {
2324
+ return { program: index, packages };
2325
+ }
2326
+ const start = index + launcher.words.length;
2327
+ const afterOptions = launchedProgram(words, start, launcher, packages);
2328
+ index = skipAssignments(words, afterOptions + launcher.operands);
2329
+ }
2330
+ return { program: void 0, packages };
2331
+ }
2332
+
2333
+ const COMMAND_SEPARATORS = /* @__PURE__ */ new Set([";", "&", "|", "(", ")", "`", "\n"]);
2334
+ const WORD_BREAKS = /* @__PURE__ */ new Set([" ", " ", "\r", "<", ">"]);
2335
+ const BACKSLASH$1 = "\\";
2336
+ const COMMENT = "#";
2337
+ const DOUBLE_QUOTE_ESCAPES = /* @__PURE__ */ new Set(["$", "`", '"', BACKSLASH$1, "\n"]);
2338
+ function singleQuoted(text, start) {
2339
+ const close = text.indexOf("'", start + 1);
2340
+ const end = close === -1 ? text.length : close + 1;
2341
+ return { end, value: text.slice(start + 1, close === -1 ? text.length : close) };
2342
+ }
2343
+ function doubleQuoted(text, start) {
2344
+ let value = "";
2345
+ let index = start + 1;
2346
+ while (index < text.length && text[index] !== '"') {
2347
+ const next = text[index + 1] ?? "";
2348
+ const isEscape = text[index] === BACKSLASH$1 && DOUBLE_QUOTE_ESCAPES.has(next);
2349
+ value += isEscape ? next === "\n" ? "" : next : text[index];
2350
+ index += isEscape ? 2 : 1;
2351
+ }
2352
+ return { end: Math.min(index + 1, text.length), value };
2353
+ }
2354
+ const isLineContinuation = (text, index) => text[index] === BACKSLASH$1 && (text[index + 1] === "\n" || text.startsWith("\r\n", index + 1));
2355
+ function wordPart(text, index) {
2356
+ const char = text[index];
2357
+ if (char === "'") {
2358
+ return singleQuoted(text, index);
2359
+ }
2360
+ if (char === '"') {
2361
+ return doubleQuoted(text, index);
2362
+ }
2363
+ if (char === BACKSLASH$1 && index + 1 < text.length) {
2364
+ return { end: index + 2, value: text[index + 1] };
2365
+ }
2366
+ return { end: index + 1, value: char };
2367
+ }
2368
+ const endsWord = (text, index) => {
2369
+ const char = text[index];
2370
+ return WORD_BREAKS.has(char) || COMMAND_SEPARATORS.has(char) || isLineContinuation(text, index);
2371
+ };
2372
+ function readWord(text, start) {
2373
+ let value = "";
2374
+ let index = start;
2375
+ while (index < text.length && !endsWord(text, index)) {
2376
+ const part = wordPart(text, index);
2377
+ value += part.value;
2378
+ index = part.end;
2379
+ }
2380
+ return { start, end: index, value, raw: text.slice(start, index) };
2381
+ }
2382
+ function skipTrivia(text, index) {
2383
+ const char = text[index];
2384
+ if (WORD_BREAKS.has(char)) {
2385
+ return index + 1;
2386
+ }
2387
+ if (isLineContinuation(text, index)) {
2388
+ return text.indexOf("\n", index) + 1;
2389
+ }
2390
+ if (char === COMMENT) {
2391
+ const lineEnd = text.indexOf("\n", index);
2392
+ return lineEnd === -1 ? text.length : lineEnd;
2393
+ }
2394
+ return index;
2395
+ }
2396
+ function topLevelCommands(text) {
2397
+ const commands = [[]];
2398
+ let index = 0;
2399
+ while (index < text.length) {
2400
+ const afterTrivia = skipTrivia(text, index);
2401
+ if (afterTrivia !== index) {
2402
+ index = afterTrivia;
2403
+ } else if (COMMAND_SEPARATORS.has(text[index])) {
2404
+ commands.push([]);
2405
+ index++;
2406
+ } else {
2407
+ const word = readWord(text, index);
2408
+ commands.at(-1)?.push(word);
2409
+ index = word.end;
2410
+ }
2411
+ }
2412
+ return commands.filter((words) => words.length > 0);
2413
+ }
2414
+ const COMMAND_PREFIX_WORDS = /* @__PURE__ */ new Set([
2415
+ "if",
2416
+ "then",
2417
+ "elif",
2418
+ "else",
2419
+ "fi",
2420
+ "while",
2421
+ "until",
2422
+ "do",
2423
+ "done",
2424
+ "!",
2425
+ "{",
2426
+ "}",
2427
+ "esac"
2428
+ ]);
2429
+ const CLAUSE_WORDS = /* @__PURE__ */ new Set(["for", "select", "case", "[["]);
2430
+ const FUNCTION_WORD = "function";
2431
+ const FUNCTION_HEADER_LENGTH = 2;
2432
+ const isReserved = (word, words) => word !== void 0 && word.raw === word.value && words.has(word.value);
2433
+ function prefixLength(words, index) {
2434
+ if (isReserved(words[index], COMMAND_PREFIX_WORDS)) {
2435
+ return 1;
2436
+ }
2437
+ const word = words[index];
2438
+ return word?.raw === FUNCTION_WORD ? FUNCTION_HEADER_LENGTH : 0;
2439
+ }
2440
+ function simpleCommand(words) {
2441
+ let first = 0;
2442
+ for (let length = prefixLength(words, 0); length > 0; length = prefixLength(words, first)) {
2443
+ first += length;
2444
+ }
2445
+ return isReserved(words[first], CLAUSE_WORDS) ? [] : words.slice(first);
2446
+ }
2447
+ const QUOTES$2 = /* @__PURE__ */ new Set(["'", '"']);
2448
+ const NESTED_COMMAND_RE = /[\s;&|()`]/;
2449
+ function nestedCommandText(word) {
2450
+ const quote = word.raw[0];
2451
+ const isOneQuotedString = QUOTES$2.has(quote) && word.raw.length > 1 && word.raw.endsWith(quote) && wordPart(word.raw, 0).end === word.raw.length;
2452
+ return isOneQuotedString && NESTED_COMMAND_RE.test(word.value) ? word.raw.slice(1, -1) : void 0;
2453
+ }
2454
+ const shifted = (words, offset) => words.map((word) => ({ ...word, start: word.start + offset, end: word.end + offset }));
2455
+ function shellCommandsAt(text, offset) {
2456
+ return shellCommands(text).map((words) => shifted(words, offset));
2457
+ }
2458
+ function shellCommands(text) {
2459
+ const commands = [];
2460
+ for (const command of topLevelCommands(text).map(simpleCommand)) {
2461
+ if (command.length > 0) {
2462
+ commands.push(command);
2463
+ }
2464
+ for (const word of command) {
2465
+ const inner = nestedCommandText(word);
2466
+ if (inner !== void 0) {
2467
+ const nested = shellCommands(inner).map((words) => shifted(words, word.start + 1));
2468
+ commands.push(...nested);
2469
+ }
2470
+ }
2471
+ }
2472
+ return commands;
2473
+ }
2474
+
2475
+ const QUOTES$1 = /* @__PURE__ */ new Set(["'", '"']);
2476
+ const PATH_SEPARATOR$1 = "/";
2477
+ const BIN_FOLDER = "/.bin/";
2478
+ const OLD_PACKAGE_FOLDER = `node_modules/${GENERATOR_COMMAND}/`;
2479
+ function renameAtEnd(word, name, to) {
2480
+ const at = word.raw.lastIndexOf(name);
2481
+ const tail = word.raw.slice(at + name.length);
2482
+ if (at === -1 || tail !== "" && !QUOTES$1.has(tail)) {
2483
+ return void 0;
2484
+ }
2485
+ return { start: word.start + at, end: word.start + at + name.length, text: to };
2486
+ }
2487
+ function packageReference(value) {
2488
+ const bare = value.startsWith(NPM_SPECIFIER_PREFIX) ? value.slice(NPM_SPECIFIER_PREFIX.length) : value;
2489
+ const at = bare.indexOf("@", 1);
2490
+ return { name: at === -1 ? bare : bare.slice(0, at), isPinned: at !== -1 };
2491
+ }
2492
+ function packageFindings(found, locate) {
2493
+ const { name, isPinned } = packageReference(found.value);
2494
+ const to = PACKAGE_RENAMES.get(name);
2495
+ const offset = found.word.start;
2496
+ if (to === void 0) {
2497
+ return [];
2498
+ }
2499
+ const location = locate(offset);
2500
+ if (isPinned) {
2501
+ return [{ offset, manualStep: { kind: "pinned-package", location, name } }];
2502
+ }
2503
+ const edit = renameAtEnd(found.word, name, to);
2504
+ const change = { kind: "package-name-renamed", location, from: name, to };
2505
+ return edit === void 0 ? [] : [{ offset, edit, change }];
2506
+ }
2507
+ function programPathFindings(word, location) {
2508
+ const offset = word.start;
2509
+ if (word.value.includes(OLD_PACKAGE_FOLDER)) {
2510
+ return [{ offset, manualStep: { kind: "old-package-file", location, path: word.value } }];
2511
+ }
2512
+ const isBin = word.value.endsWith(`${BIN_FOLDER}${GENERATOR_COMMAND}`);
2513
+ const edit = isBin ? renameAtEnd(word, GENERATOR_COMMAND, MIGRATED_GENERATOR_COMMAND) : void 0;
2514
+ return edit === void 0 ? [] : [{ offset, edit, change: { kind: "command-renamed", location } }];
2515
+ }
2516
+ function programFindings(word, location) {
2517
+ if (word.value.includes(PATH_SEPARATOR$1)) {
2518
+ return programPathFindings(word, location);
2519
+ }
2520
+ const { name, isPinned } = packageReference(word.value);
2521
+ if (name !== GENERATOR_COMMAND) {
2522
+ return [];
2523
+ }
2524
+ if (isPinned) {
2525
+ return [{ offset: word.start, manualStep: { kind: "versioned-command", location } }];
2526
+ }
2527
+ const edit = renameAtEnd(word, GENERATOR_COMMAND, MIGRATED_GENERATOR_COMMAND);
2528
+ const change = { kind: "command-renamed", location };
2529
+ return edit === void 0 ? [] : [{ offset: word.start, edit, change }];
2530
+ }
2531
+ function flagFindings(word, locate) {
2532
+ const flag = word.raw.split("=", 1)[0];
2533
+ const to = FLAG_RENAMES.get(flag);
2534
+ if (to === void 0) {
2535
+ return [];
2536
+ }
2537
+ const offset = word.start;
2538
+ const edit = { start: offset, end: offset + flag.length, text: to };
2539
+ return [
2540
+ { offset, edit, change: { kind: "flag-renamed", location: locate(offset), from: flag, to } }
2541
+ ];
2542
+ }
2543
+ const RECIPE_PREFIX_RE$1 = /^[@+-]+(?=.)/;
2544
+ function withoutRecipePrefix(words) {
2545
+ const [first, ...rest] = words;
2546
+ if (first === void 0 || !RECIPE_PREFIX_RE$1.test(first.value)) {
2547
+ return words;
2548
+ }
2549
+ return [{ ...first, value: first.value.replace(RECIPE_PREFIX_RE$1, "") }, ...rest];
2550
+ }
2551
+ const NO_VARIABLES = {
2552
+ programAt: () => void 0,
2553
+ referenceAt: () => void 0
2554
+ };
2555
+ function renamedFlags(value) {
2556
+ const flags = shellCommands(value).flat().map((word) => word.raw.split("=", 1)[0]);
2557
+ return [...new Set(flags.filter((flag) => FLAG_RENAMES.has(flag)))];
2558
+ }
2559
+ function argumentFindings(word, locate, variables) {
2560
+ const reference = variables.referenceAt(word);
2561
+ if (reference === void 0) {
2562
+ return flagFindings(word, locate);
2563
+ }
2564
+ const flags = renamedFlags(reference.value);
2565
+ if (flags.length === 0) {
2566
+ return [];
2567
+ }
2568
+ const manualStep = {
2569
+ kind: "flags-in-variable",
2570
+ location: locate(word.start),
2571
+ variable: reference.written,
2572
+ flags
2573
+ };
2574
+ return [{ offset: word.start, manualStep }];
2575
+ }
2576
+ const oldFlagsOf = (findings) => findings.flatMap(({ change, manualStep }) => {
2577
+ if (change?.kind === "flag-renamed") {
2578
+ return [change.from];
2579
+ }
2580
+ return manualStep?.kind === "flags-in-variable" ? [...manualStep.flags] : [];
2581
+ });
2582
+ function uncertainFlagSteps(program, variable, found, locate) {
2583
+ const flags = [...new Set(oldFlagsOf(found))];
2584
+ if (flags.length === 0) {
2585
+ return [];
2586
+ }
2587
+ const manualStep = {
2588
+ kind: "variable-not-only-generator",
2589
+ location: locate(program.start),
2590
+ variable,
2591
+ flags
2592
+ };
2593
+ return [{ offset: program.start, manualStep }];
2594
+ }
2595
+ function commandFindings(command, locate, variables) {
2596
+ const words = withoutRecipePrefix(command);
2597
+ const roles = commandRoles(words);
2598
+ const findings = roles.packages.flatMap((found2) => packageFindings(found2, locate));
2599
+ const program = roles.program === void 0 ? void 0 : words[roles.program];
2600
+ if (program === void 0) {
2601
+ return findings;
2602
+ }
2603
+ const found = programFindings(program, locate(program.start));
2604
+ findings.push(...found);
2605
+ const isRenamed = found.some((finding) => finding.change?.kind === "command-renamed");
2606
+ const variable = variables.programAt(program);
2607
+ if (!isRenamed && (variable === void 0 || variable.program === "other")) {
2608
+ return findings;
2609
+ }
2610
+ const flags = words.slice(roles.program + 1);
2611
+ const flagFound = flags.flatMap((word) => argumentFindings(word, locate, variables));
2612
+ const isUncertain = variable?.program === "generator-or-other";
2613
+ findings.push(
2614
+ ...isUncertain ? uncertainFlagSteps(program, variable.written, flagFound, locate) : flagFound
2615
+ );
2616
+ return findings;
2617
+ }
2618
+ function runsGenerator(command) {
2619
+ const words = withoutRecipePrefix(command);
2620
+ const { program } = commandRoles(words);
2621
+ const word = program === void 0 ? void 0 : words[program];
2622
+ return word !== void 0 && packageReference(word.value).name === GENERATOR_COMMAND;
2623
+ }
2624
+ function shellTextFindings(text, locate) {
2625
+ return commandWordFindings(shellCommands(text), locate);
2626
+ }
2627
+ function commandWordFindings(commands, locate, variables = NO_VARIABLES) {
2628
+ const findings = commands.flatMap((words) => commandFindings(words, locate, variables)).sort((left, right) => left.offset - right.offset);
2629
+ return {
2630
+ edits: findings.flatMap((finding) => finding.edit === void 0 ? [] : [finding.edit]),
2631
+ changes: findings.flatMap((finding) => finding.change === void 0 ? [] : [finding.change]),
2632
+ manualSteps: findings.flatMap(
2633
+ (finding) => finding.manualStep === void 0 ? [] : [finding.manualStep]
2634
+ )
2635
+ };
2636
+ }
2637
+ function migrateShellText(text, locate) {
2638
+ const { edits, changes, manualSteps } = shellTextFindings(text, locate);
2639
+ return { text: applyTextEdits(text, edits), changes, manualSteps };
2640
+ }
2641
+ function migrateCommand(text, location) {
2642
+ return migrateShellText(text, () => location);
2643
+ }
2644
+ function migrateCommandLines(text, file) {
2645
+ const lineOf = lineLocator(text);
2646
+ return migrateShellText(text, (offset) => `${file}:${lineOf(offset)}`);
2647
+ }
2648
+
2649
+ const NO_LOCATION = "";
2650
+ function commandStringSteps(text, strings) {
2651
+ const lineOf = lineLocator(text);
2652
+ return strings.flatMap((found) => {
2653
+ if (found.isModuleSpecifier || PACKAGE_RENAMES.has(found.value)) {
2654
+ return [];
2655
+ }
2656
+ const migrated = migrateCommand(found.value, NO_LOCATION).text;
2657
+ return migrated === found.value ? [] : [{ kind: "command-string", line: lineOf(found.start), to: migrated }];
2658
+ });
2659
+ }
2660
+
2661
+ function canRunGenerator(definition, generators) {
2662
+ switch (definition.kind) {
2663
+ case "command":
2664
+ return definition.runsGenerator;
2665
+ case "reference":
2666
+ return generators.has(definition.target);
2667
+ case "append":
2668
+ return false;
2669
+ default:
2670
+ return assertNever(definition);
2671
+ }
2672
+ }
2673
+ function canRunOther(definition, others, defined) {
2674
+ switch (definition.kind) {
2675
+ case "command":
2676
+ return !definition.runsGenerator;
2677
+ case "reference":
2678
+ return others.has(definition.target) || !defined.has(definition.target);
2679
+ case "append":
2680
+ return false;
2681
+ default:
2682
+ return assertNever(definition);
2683
+ }
2684
+ }
2685
+ function variablePrograms(definitions) {
2686
+ const defined = new Set(definitions.map((definition) => definition.name));
2687
+ const generators = /* @__PURE__ */ new Set();
2688
+ const others = /* @__PURE__ */ new Set();
2689
+ for (let size = -1; size !== generators.size + others.size; ) {
2690
+ size = generators.size + others.size;
2691
+ for (const definition of definitions) {
2692
+ if (canRunGenerator(definition, generators)) {
2693
+ generators.add(definition.name);
2694
+ }
2695
+ if (canRunOther(definition, others, defined)) {
2696
+ others.add(definition.name);
2697
+ }
2698
+ }
2699
+ }
2700
+ const programOf = (name) => {
2701
+ if (!generators.has(name)) {
2702
+ return "other";
2703
+ }
2704
+ return others.has(name) ? "generator-or-other" : "generator";
2705
+ };
2706
+ return new Map([...defined].map((name) => [name, programOf(name)]));
2707
+ }
2708
+
2709
+ const SHELL_VARIABLE_RE = /^"?\$(?:([A-Za-z_][A-Za-z0-9_]*)|\{([A-Za-z_][A-Za-z0-9_]*)\})"?$/;
2710
+ function expandedName(text) {
2711
+ const match = SHELL_VARIABLE_RE.exec(text);
2712
+ return match === null ? void 0 : match[1] ?? match[2];
2713
+ }
2714
+ function programDefinition({ name, value }) {
2715
+ const target = expandedName(value.trim());
2716
+ if (target !== void 0) {
2717
+ return { name, kind: "reference", target };
2718
+ }
2719
+ return { name, kind: "command", runsGenerator: shellCommands(value).some(runsGenerator) };
2720
+ }
2721
+ function shellVariables(definitions) {
2722
+ const values = /* @__PURE__ */ new Map();
2723
+ for (const { name, value } of definitions) {
2724
+ values.set(name, [values.get(name), value].filter((part) => part !== void 0).join(" "));
2725
+ }
2726
+ const programs = variablePrograms(definitions.map(programDefinition));
2727
+ const nameAt = (word) => expandedName(word.raw);
2728
+ return {
2729
+ programAt: (program) => {
2730
+ const name = nameAt(program);
2731
+ const found = name === void 0 ? void 0 : programs.get(name);
2732
+ return found === void 0 ? void 0 : { written: program.raw, program: found };
2733
+ },
2734
+ referenceAt: (word) => {
2735
+ const name = nameAt(word);
2736
+ const value = name === void 0 ? void 0 : values.get(name);
2737
+ return value === void 0 ? void 0 : { written: word.raw, value };
2738
+ }
2739
+ };
2740
+ }
2741
+
2742
+ const EXEC_FORM_START_RE = /^[ \t]*(?:RUN|CMD|ENTRYPOINT)(?:[ \t]+--\S+)*[ \t]+\[/gm;
2743
+ const SHELLS = /* @__PURE__ */ new Set(["sh", "bash", "dash", "ash", "zsh"]);
2744
+ const OPTION_PREFIX = "-";
2745
+ const LONG_OPTION_PREFIX = "--";
2746
+ const COMMAND_OPTION = "c";
2747
+ const SET_OPTION = "-o";
2748
+ const PATH_SEPARATOR = "/";
2749
+ const ARRAY_BLANK_RE = /(?:[ \t\r\n]|\\\r?\n)*/y;
2750
+ const QUOTED_RE = /"(?:[^"\\\n]|\\.)*"/y;
2751
+ const ITEM_SEPARATOR = ",";
2752
+ const NOT_LINE_BREAK_RE$1 = /[^\n]/g;
2753
+ const ARRAY_END = "]";
2754
+ const VARIABLE_INSTRUCTIONS = /* @__PURE__ */ new Set(["ARG", "ENV"]);
2755
+ const LEGACY_ENV_INSTRUCTION = "ENV";
2756
+ const ASSIGNMENT = "=";
2757
+ const QUOTES = /* @__PURE__ */ new Set(['"', "'"]);
2758
+ function matchAt(pattern, text, index) {
2759
+ pattern.lastIndex = index;
2760
+ return pattern.exec(text)?.[0];
2761
+ }
2762
+ function jsonString(raw) {
2763
+ try {
2764
+ return JSON.parse(raw);
2765
+ } catch (error) {
2766
+ if (error instanceof SyntaxError) {
2767
+ return void 0;
2768
+ }
2769
+ throw error;
2770
+ }
2771
+ }
2772
+ function execFormAt(text, start) {
2773
+ const words = [];
2774
+ let index = start + 1;
2775
+ for (; ; ) {
2776
+ index += matchAt(ARRAY_BLANK_RE, text, index)?.length ?? 0;
2777
+ const raw = matchAt(QUOTED_RE, text, index);
2778
+ if (raw === void 0) {
2779
+ const isEmpty = text[index] === ARRAY_END && words.length === 0;
2780
+ return isEmpty ? { start, end: index + 1, words } : void 0;
2781
+ }
2782
+ const value = jsonString(raw);
2783
+ if (value === void 0) {
2784
+ return void 0;
2785
+ }
2786
+ words.push({ start: index + 1, end: index + raw.length - 1, value, raw: raw.slice(1, -1) });
2787
+ index += raw.length;
2788
+ index += matchAt(ARRAY_BLANK_RE, text, index)?.length ?? 0;
2789
+ if (text[index] === ARRAY_END) {
2790
+ return { start, end: index + 1, words };
2791
+ }
2792
+ if (text[index] !== ITEM_SEPARATOR) {
2793
+ return void 0;
2794
+ }
2795
+ index++;
2796
+ }
2797
+ }
2798
+ function execForms(text) {
2799
+ return [...text.matchAll(EXEC_FORM_START_RE)].flatMap((match) => {
2800
+ const form = execFormAt(text, match.index + match[0].length - 1);
2801
+ return form === void 0 ? [] : [form];
2802
+ });
2803
+ }
2804
+ function shellScript(words) {
2805
+ const program = words[0]?.value.split(PATH_SEPARATOR).at(-1) ?? "";
2806
+ if (!SHELLS.has(program)) {
2807
+ return void 0;
2808
+ }
2809
+ let index = 1;
2810
+ for (let option = words[index]?.value; option !== void 0; option = words[index]?.value) {
2811
+ if (!option.startsWith(OPTION_PREFIX)) {
2812
+ return void 0;
2813
+ }
2814
+ if (!option.startsWith(LONG_OPTION_PREFIX) && option.includes(COMMAND_OPTION)) {
2815
+ return words[index + 1];
2816
+ }
2817
+ index += option === SET_OPTION ? 2 : 1;
2818
+ }
2819
+ return void 0;
2820
+ }
2821
+ function execCommands(forms) {
2822
+ const commands = [];
2823
+ const escaped = [];
2824
+ for (const { words } of forms) {
2825
+ commands.push(words);
2826
+ const script = shellScript(words);
2827
+ if (script !== void 0 && script.raw === script.value) {
2828
+ commands.push(...shellCommandsAt(script.value, script.start));
2829
+ } else if (script !== void 0) {
2830
+ escaped.push(script);
2831
+ }
2832
+ }
2833
+ return { commands, escaped };
2834
+ }
2835
+ function withoutExecForms(text, forms) {
2836
+ const parts = [];
2837
+ let copiedUpTo = 0;
2838
+ for (const { start, end } of forms) {
2839
+ parts.push(
2840
+ text.slice(copiedUpTo, start),
2841
+ text.slice(start, end).replace(NOT_LINE_BREAK_RE$1, " ")
2842
+ );
2843
+ copiedUpTo = end;
2844
+ }
2845
+ parts.push(text.slice(copiedUpTo));
2846
+ return parts.join("");
2847
+ }
2848
+ function escapedScriptSteps(scripts, lineOf) {
2849
+ return scripts.flatMap((script) => {
2850
+ const migrated = migrateCommand(script.value, "").text;
2851
+ return migrated === script.value ? [] : [{ kind: "command-string", line: lineOf(script.start), to: migrated }];
2852
+ });
2853
+ }
2854
+ function assignedVariable(word) {
2855
+ const at = word.raw.indexOf(ASSIGNMENT);
2856
+ if (at <= 0) {
2857
+ return void 0;
2858
+ }
2859
+ const raw = word.raw.slice(at + 1);
2860
+ const isQuoted = raw.length >= 2 && QUOTES.has(raw[0]) && raw.at(-1) === raw[0];
2861
+ const value = isQuoted ? raw.slice(1, -1) : raw;
2862
+ const start = word.start + at + 1 + (isQuoted ? 1 : 0);
2863
+ return { name: word.raw.slice(0, at), commands: shellCommandsAt(value, start) };
2864
+ }
2865
+ function dockerVariables(commands) {
2866
+ return commands.flatMap(([instruction, first, ...rest]) => {
2867
+ if (instruction === void 0 || first === void 0) {
2868
+ return [];
2869
+ }
2870
+ if (!VARIABLE_INSTRUCTIONS.has(instruction.value)) {
2871
+ return [];
2872
+ }
2873
+ if (instruction.value === LEGACY_ENV_INSTRUCTION && !first.raw.includes(ASSIGNMENT)) {
2874
+ return [{ name: first.value, commands: [rest] }];
2875
+ }
2876
+ return [first, ...rest].flatMap((word) => assignedVariable(word) ?? []);
2877
+ });
2878
+ }
2879
+ const definitionOf = ({ name, commands }) => ({
2880
+ name,
2881
+ value: commands.flatMap((words) => words.map((word) => word.raw)).join(" ")
2882
+ });
2883
+ function migrateDockerfile(text, file) {
2884
+ const lineOf = lineLocator(text);
2885
+ const locate = (offset) => `${file}:${lineOf(offset)}`;
2886
+ const forms = execForms(text);
2887
+ const exec = execCommands(forms);
2888
+ const shellForm = shellCommands(withoutExecForms(text, forms));
2889
+ const definitions = dockerVariables(shellForm);
2890
+ const values = definitions.flatMap((variable) => variable.commands);
2891
+ const commands = [...shellForm, ...values, ...exec.commands];
2892
+ const variables = shellVariables(definitions.map(definitionOf));
2893
+ const { edits, changes, manualSteps } = commandWordFindings(commands, locate, variables);
2894
+ const steps = [...manualSteps, ...escapedScriptSteps(exec.escaped, lineOf)];
2895
+ return { text: applyTextEdits(text, edits), changes, manualSteps: steps };
2896
+ }
2897
+
2898
+ const ANY = "*";
2899
+ const PACKAGE_SCRIPT_PATHS = [["scripts", ANY]];
2900
+ const NX_COMMAND_PATHS = [
2901
+ ["targets", ANY, "command"],
2902
+ ["targets", ANY, "options", "command"],
2903
+ ["targets", ANY, "options", "commands", ANY],
2904
+ ["targets", ANY, "options", "commands", ANY, "command"]
2905
+ ];
2906
+ const DENO_TASK_PATHS = [
2907
+ ["tasks", ANY],
2908
+ ["tasks", ANY, "command"]
2909
+ ];
2910
+ const matches = (path, pattern) => path.length === pattern.length && pattern.every((part, index) => part === ANY || part === String(path[index]));
2911
+ function migrateJsonCommands(text, strings, patterns) {
2912
+ const changes = [];
2913
+ const manualSteps = [];
2914
+ const edits = [];
2915
+ for (const found of strings.filter((string) => patterns.some((p) => matches(string.path, p)))) {
2916
+ const migrated = migrateCommand(found.value, found.path.join("."));
2917
+ changes.push(...migrated.changes);
2918
+ manualSteps.push(...migrated.manualSteps);
2919
+ if (migrated.text !== found.value) {
2920
+ edits.push({ start: found.start, end: found.end, text: JSON.stringify(migrated.text) });
2921
+ }
2922
+ }
2923
+ return { text: applyTextEdits(text, edits), changes, manualSteps };
2924
+ }
2925
+
2926
+ const ASSIGNMENT_RE = new RegExp(
2927
+ [
2928
+ "^(?:[^\\s:#=][^:#=\\n]*:[ \\t]*)?",
2929
+ "(?:(?:export|override|private)[ \\t]+)*",
2930
+ "([A-Za-z_][A-Za-z0-9_.-]*)",
2931
+ "[ \\t]*(:::=|::=|:=|\\?=|\\+=|!=|=)"
2932
+ ].join(""),
2933
+ "gm"
2934
+ );
2935
+ const VARIABLE_REFERENCE_RE = /^\$(?:\(([A-Za-z_][A-Za-z0-9_.-]*)\)|\{([A-Za-z_][A-Za-z0-9_.-]*)\}|([A-Za-z_]))$/;
2936
+ const RECIPE_PREFIX_RE = /^[@+-]+/;
2937
+ const DOLLAR = "$";
2938
+ const CLOSING = { "(": ")", "{": "}" };
2939
+ const NOT_LINE_BREAK_RE = /[^\n]/g;
2940
+ const MASK = "_";
2941
+ const LINE_BREAK = "\n";
2942
+ const BACKSLASH = "\\";
2943
+ function expansionEnd(text, start) {
2944
+ const open = text[start + 1] ?? "";
2945
+ const close = CLOSING[open];
2946
+ if (close === void 0) {
2947
+ return void 0;
2948
+ }
2949
+ let depth = 0;
2950
+ for (let index = start + 1; index < text.length; index++) {
2951
+ if (text[index] === open) {
2952
+ depth++;
2953
+ } else if (text[index] === close && --depth === 0) {
2954
+ return index + 1;
2955
+ }
2956
+ }
2957
+ return void 0;
1036
2958
  }
1037
- function hideUrlCredentials(text) {
1038
- let hidden = "";
2959
+ function maskExpansions(text) {
2960
+ const parts = [];
1039
2961
  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;
2962
+ for (let index = text.indexOf(DOLLAR); index !== -1; index = text.indexOf(DOLLAR, index + 1)) {
2963
+ if (text[index + 1] === DOLLAR) {
2964
+ index++;
2965
+ continue;
2966
+ }
2967
+ const end = expansionEnd(text, index);
2968
+ if (end !== void 0) {
2969
+ parts.push(text.slice(copiedUpTo, index + 1), MASK.repeat(end - index - 1));
2970
+ copiedUpTo = end;
2971
+ index = end - 1;
1046
2972
  }
1047
- found = text.indexOf(SCHEME_SEPARATOR, end);
1048
2973
  }
1049
- return `${hidden}${text.slice(copiedUpTo)}`;
2974
+ parts.push(text.slice(copiedUpTo));
2975
+ return parts.join("");
1050
2976
  }
1051
-
1052
- const VERSION_STYLE_RE = /^([\^~]|>=)?\d/;
1053
- const KEPT_PROTOCOLS = ["catalog:", "workspace:"];
1054
- function migrateVersionSpec(spec, version) {
1055
- if (KEPT_PROTOCOLS.some((protocol) => spec.startsWith(protocol))) {
1056
- return { kind: "kept", spec, isReported: false };
2977
+ const APPEND_OPERATOR = "+=";
2978
+ function logicalLineEnd(text, index) {
2979
+ let end = text.indexOf(LINE_BREAK, index);
2980
+ while (end !== -1 && text[end - 1] === BACKSLASH) {
2981
+ end = text.indexOf(LINE_BREAK, end + 1);
1057
2982
  }
1058
- const style = VERSION_STYLE_RE.exec(spec);
1059
- if (style === null) {
1060
- return { kind: "kept", spec, isReported: true };
2983
+ return end === -1 ? text.length : end;
2984
+ }
2985
+ const assignmentsOf = (text) => [...text.matchAll(ASSIGNMENT_RE)].map((match) => {
2986
+ const valueStart = match.index + match[0].length;
2987
+ const valueEnd = logicalLineEnd(text, valueStart);
2988
+ const name = match[1];
2989
+ const isAppend = match[2] === APPEND_OPERATOR;
2990
+ return { name, isAppend, start: match.index, valueStart, valueEnd };
2991
+ });
2992
+ function withoutAssignmentHeads(text, assignments) {
2993
+ const parts = [];
2994
+ let copiedUpTo = 0;
2995
+ for (const { start, valueStart } of assignments) {
2996
+ parts.push(
2997
+ text.slice(copiedUpTo, start),
2998
+ text.slice(start, valueStart).replace(NOT_LINE_BREAK_RE, " ")
2999
+ );
3000
+ copiedUpTo = valueStart;
1061
3001
  }
1062
- const prefix = style[1] === ">=" ? "^" : style[1] ?? "";
1063
- return { kind: "replaced", spec: `${prefix}${version}` };
3002
+ parts.push(text.slice(copiedUpTo));
3003
+ return parts.join("");
3004
+ }
3005
+ function referencedName(written) {
3006
+ const match = VARIABLE_REFERENCE_RE.exec(written.trim().replace(RECIPE_PREFIX_RE, ""));
3007
+ return match === null ? void 0 : match[1] ?? match[2] ?? match[3];
3008
+ }
3009
+ function programDefinitions(text, commands, assignments) {
3010
+ const generatorStarts = commands.filter(runsGenerator).map((command) => command[0].start);
3011
+ return assignments.map(({ name, isAppend, valueStart, valueEnd }) => {
3012
+ if (isAppend) {
3013
+ return { name, kind: "append" };
3014
+ }
3015
+ const target = referencedName(text.slice(valueStart, valueEnd));
3016
+ if (target !== void 0) {
3017
+ return { name, kind: "reference", target };
3018
+ }
3019
+ const runs = generatorStarts.some((start) => valueStart <= start && start < valueEnd);
3020
+ return { name, kind: "command", runsGenerator: runs };
3021
+ });
3022
+ }
3023
+ function variableValues(text, assignments) {
3024
+ const values = /* @__PURE__ */ new Map();
3025
+ for (const { name, valueStart, valueEnd } of assignments) {
3026
+ const value = text.slice(valueStart, valueEnd);
3027
+ values.set(name, [values.get(name), value].filter((part) => part !== void 0).join(" "));
3028
+ }
3029
+ return values;
3030
+ }
3031
+ function makefileVariables(text, assignments, programs) {
3032
+ const values = variableValues(text, assignments);
3033
+ const nameAt = (word) => referencedName(text.slice(word.start, word.end));
3034
+ const variables = {
3035
+ programAt: (program) => {
3036
+ const name = nameAt(program);
3037
+ const found = name === void 0 ? void 0 : programs.get(name);
3038
+ const written = text.slice(program.start, program.end);
3039
+ return found === void 0 ? void 0 : { written, program: found };
3040
+ },
3041
+ referenceAt: (word) => {
3042
+ const name = nameAt(word);
3043
+ const value = name === void 0 ? void 0 : values.get(name);
3044
+ const written = text.slice(word.start, word.end);
3045
+ return value === void 0 ? void 0 : { written, value };
3046
+ }
3047
+ };
3048
+ return variables;
3049
+ }
3050
+ function migrateMakefile(text, file) {
3051
+ const lineOf = lineLocator(text);
3052
+ const assignments = assignmentsOf(text);
3053
+ const commands = shellCommands(withoutAssignmentHeads(maskExpansions(text), assignments));
3054
+ const programs = variablePrograms(programDefinitions(text, commands, assignments));
3055
+ const locate = (offset) => `${file}:${lineOf(offset)}`;
3056
+ const variables = makefileVariables(text, assignments, programs);
3057
+ const { edits, changes, manualSteps } = commandWordFindings(commands, locate, variables);
3058
+ return { text: applyTextEdits(text, edits), changes, manualSteps };
1064
3059
  }
1065
3060
 
1066
- const INDENT_RE = /^[ \t]+(?=")/m;
1067
- const DEFAULT_INDENT = " ";
1068
- const LF = "\n";
1069
- const CRLF = "\r\n";
1070
- function isObject(value) {
1071
- return typeof value === "object" && value !== null && !Array.isArray(value);
1072
- }
1073
- function formatLike(original, manifest) {
1074
- const indent = INDENT_RE.exec(original)?.[0] ?? DEFAULT_INDENT;
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}`;
1078
- }
1079
- function renameDependencies(field, dependencies, version, findings) {
1080
- return Object.fromEntries(
1081
- Object.entries(dependencies).map(([name, spec]) => {
1082
- const renamed = PACKAGE_RENAMES.get(name);
1083
- if (renamed === void 0 || typeof spec !== "string") {
1084
- return [name, spec];
1085
- }
1086
- const decision = migrateVersionSpec(spec, version);
1087
- findings.changes.push({
1088
- kind: "dependency-renamed",
1089
- field,
1090
- from: name,
1091
- to: renamed,
1092
- spec: hideUrlCredentials(decision.spec)
1093
- });
1094
- if (decision.kind === "kept" && decision.isReported) {
1095
- findings.manualSteps.push({
1096
- kind: "spec-kept",
1097
- name: renamed,
1098
- spec: hideUrlCredentials(spec)
1099
- });
1100
- }
1101
- if (field === "peerDependencies") {
1102
- findings.manualSteps.push({ kind: "peer-dependency-renamed", name: renamed });
3061
+ function migrateDependencyFields(manifest, target, findings) {
3062
+ for (const field of DEPENDENCY_FIELDS) {
3063
+ for (const dependencies of mapsAt(manifest, field)) {
3064
+ const map = { kind: "dependencies", field };
3065
+ migrateSpecMap(specEntries(dependencies), { ...target, map, findings });
3066
+ }
3067
+ }
3068
+ }
3069
+ function migrateNameKeys(manifest, findings) {
3070
+ for (const field of NAME_KEYED_FIELDS) {
3071
+ for (const meta of mapsAt(manifest, field)) {
3072
+ renameNames(keyEntries(meta), field, findings);
3073
+ const versioned = textEntries(meta).map(({ key }) => key.value).filter((key) => !PACKAGE_RENAMES.has(key));
3074
+ for (const name of overriddenPackages(versioned)) {
3075
+ findings.manualSteps.push({ kind: "override-names-package", field, name });
1103
3076
  }
1104
- return [renamed, decision.spec];
1105
- })
1106
- );
3077
+ }
3078
+ }
1107
3079
  }
1108
- function migrateScripts(scripts, findings) {
1109
- return Object.fromEntries(
1110
- Object.entries(scripts).map(([name, script]) => {
1111
- if (typeof script !== "string") {
1112
- return [name, script];
3080
+ function migrateNameLists(manifest, findings) {
3081
+ for (const field of NAME_LIST_FIELDS) {
3082
+ for (const list of valuesAt(manifest, field)) {
3083
+ if (list.kind === "list") {
3084
+ renameNames(itemEntries(list), field, findings);
1113
3085
  }
1114
- const migrated = migrateCommand(script, `scripts.${name}`);
1115
- findings.changes.push(...migrated.changes);
1116
- findings.manualSteps.push(...migrated.manualSteps);
1117
- return [name, migrated.text];
1118
- })
1119
- );
3086
+ }
3087
+ }
3088
+ }
3089
+ function migrateCatalogs$1(manifest, target, findings) {
3090
+ for (const { name, label, map } of manifestCatalogMaps(manifest)) {
3091
+ const context = {
3092
+ ...target,
3093
+ map: { kind: "catalog", catalog: label, name },
3094
+ findings
3095
+ };
3096
+ migrateSpecMap(specEntries(map), context);
3097
+ }
1120
3098
  }
1121
3099
  function reportOverrides(manifest, findings) {
1122
3100
  for (const field of OVERRIDE_FIELDS) {
1123
- for (const name of overriddenPackages(manifest[field])) {
3101
+ const keys = valuesAt(manifest, field).flatMap(nestedKeys);
3102
+ for (const name of overriddenPackages(keys)) {
1124
3103
  findings.manualSteps.push({ kind: "override-names-package", field, name });
1125
3104
  }
1126
3105
  }
1127
3106
  }
1128
3107
  function reportOldPeers(manifest, findings) {
3108
+ const dependencies = new Set(declaredNames(manifest));
1129
3109
  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) {
3110
+ if (dependencies.has(dependent)) {
1135
3111
  findings.manualSteps.push({ kind: "old-peer-needed", dependent, peer, range });
1136
3112
  }
1137
3113
  }
1138
3114
  }
1139
- function migrateFields(manifest, version, findings) {
1140
- const migrated = { ...manifest };
1141
- for (const field of DEPENDENCY_FIELDS) {
1142
- const dependencies = manifest[field];
1143
- if (isObject(dependencies)) {
1144
- migrated[field] = renameDependencies(field, dependencies, version, findings);
1145
- }
1146
- }
1147
- if (isObject(manifest.scripts)) {
1148
- migrated.scripts = migrateScripts(manifest.scripts, findings);
1149
- }
3115
+ function migrateManifestMap(manifest, target, findings) {
3116
+ migrateDependencyFields(manifest, target, findings);
3117
+ migrateNameKeys(manifest, findings);
3118
+ migrateNameLists(manifest, findings);
3119
+ migrateCatalogs$1(manifest, target, findings);
1150
3120
  reportOverrides(manifest, findings);
1151
3121
  reportOldPeers(manifest, findings);
1152
- return migrated;
1153
3122
  }
1154
- function migratePackageManifest(text, version) {
1155
- let manifest;
1156
- try {
1157
- manifest = JSON.parse(text);
1158
- } catch (error) {
1159
- const reason = hideUrlCredentials(error instanceof Error ? error.message : String(error));
1160
- return { ...unchanged(text), manualSteps: [{ kind: "manifest-unreadable", reason }] };
3123
+ function migratePackageManifest(text, read, target) {
3124
+ if (read.kind === "unreadable") {
3125
+ return unreadableConfig(text, read.offset);
1161
3126
  }
1162
- if (!isObject(manifest)) {
1163
- return unchanged(text);
3127
+ const findings = newFindings();
3128
+ for (const manifest of read.documents.filter(isMap)) {
3129
+ migrateManifestMap(manifest, target, findings);
1164
3130
  }
1165
- const findings = { changes: [], manualSteps: [] };
1166
- const migrated = migrateFields(manifest, version, findings);
1167
- const isChanged = findings.changes.length > 0;
1168
- return { text: isChanged ? formatLike(text, migrated) : text, ...findings };
3131
+ const { edits, changes, manualSteps } = findings;
3132
+ return { text: applyTextEdits(text, edits), changes, manualSteps };
3133
+ }
3134
+ function unreadManifest(text) {
3135
+ return namesOriginalPackage(text) ? { ...unchanged(text), manualSteps: [{ kind: "manifest-not-read" }] } : unchanged(text);
1169
3136
  }
1170
3137
 
1171
3138
  const SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
@@ -1181,44 +3148,66 @@ const SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
1181
3148
  ".svelte",
1182
3149
  ".astro"
1183
3150
  ]);
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;
3151
+ function renamedValue(value, version) {
3152
+ const npm = migrateNpmSpec(value, version);
3153
+ if (npm === void 0) {
3154
+ return renamedSpecifier(value);
1190
3155
  }
1191
- return { to: `${renamed}${value.slice(name.length)}`, isDeep: slash !== -1 };
3156
+ const renamed = { to: npm.spec, isDeep: npm.isDeep };
3157
+ return npm.isReported ? { ...renamed, keptSpec: hideUrlCredentials(value) } : renamed;
1192
3158
  }
3159
+ const TEMPLATE_SUBSTITUTION = "${";
3160
+ function dynamicPackage(value) {
3161
+ const at = value.indexOf(TEMPLATE_SUBSTITUTION);
3162
+ return at === -1 ? void 0 : PACKAGE_RENAMES.get(value.slice(0, at));
3163
+ }
3164
+ const byStartThenOuter = (left, right) => left.start - right.start || right.end - left.end;
1193
3165
  function distinctStrings(strings) {
1194
3166
  const distinct = [];
1195
- for (const found of [...strings].sort((left, right) => left.start - right.start)) {
3167
+ const enclosing = [];
3168
+ for (const found of [...strings].sort(byStartThenOuter)) {
1196
3169
  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) {
3170
+ if (previous?.start === found.start && previous.end === found.end) {
1200
3171
  const isModuleSpecifier = previous.isModuleSpecifier || found.isModuleSpecifier;
1201
3172
  distinct[distinct.length - 1] = { ...previous, isModuleSpecifier };
1202
- } else {
3173
+ continue;
3174
+ }
3175
+ let outer = enclosing.at(-1);
3176
+ while (outer !== void 0 && outer.end <= found.start) {
3177
+ enclosing.pop();
3178
+ outer = enclosing.at(-1);
3179
+ }
3180
+ if (outer !== void 0 && found.end > outer.end) {
1203
3181
  throw new Error(
1204
- `Source strings overlap at offsets ${previous.start}-${previous.end} and ${found.start}-${found.end}; the file was not changed.`
3182
+ `Source strings overlap at offsets ${outer.start}-${outer.end} and ${found.start}-${found.end}; the file was not changed.`
1205
3183
  );
1206
3184
  }
3185
+ enclosing.push(found);
3186
+ distinct.push(found);
1207
3187
  }
1208
3188
  return distinct;
1209
3189
  }
1210
- function migrateModuleSpecifiers(text, strings) {
3190
+ const dynamicStep = (line, value, to) => ({
3191
+ kind: "dynamic-specifier",
3192
+ line,
3193
+ value,
3194
+ to
3195
+ });
3196
+ function migrateModuleSpecifiers(text, strings, version) {
1211
3197
  const changes = [];
1212
3198
  const manualSteps = [];
1213
3199
  let migrated = "";
1214
3200
  let copiedUpTo = 0;
3201
+ const lineOf = lineLocator(text);
1215
3202
  for (const found of distinctStrings(strings)) {
1216
- const renamed = renamedValue(found.value);
3203
+ const renamed = renamedValue(found.value, version);
3204
+ const line = lineOf(found.start);
3205
+ const dynamic = found.isModuleSpecifier ? dynamicPackage(found.value) : void 0;
1217
3206
  if (renamed === void 0) {
3207
+ manualSteps.push(...dynamic === void 0 ? [] : [dynamicStep(line, found.value, dynamic)]);
1218
3208
  continue;
1219
3209
  }
1220
- const line = lineAt(text, found.start);
1221
- if (!found.isModuleSpecifier) {
3210
+ if (!found.isModuleSpecifier || found.start < copiedUpTo) {
1222
3211
  manualSteps.push({ kind: "string-names-package", line, value: found.value, to: renamed.to });
1223
3212
  continue;
1224
3213
  }
@@ -1226,86 +3215,186 @@ function migrateModuleSpecifiers(text, strings) {
1226
3215
  if (renamed.isDeep) {
1227
3216
  manualSteps.push({ kind: "deep-import", line, specifier: renamed.to });
1228
3217
  }
3218
+ if (renamed.keptSpec !== void 0) {
3219
+ manualSteps.push({ kind: "spec-kept", name: renamed.to, spec: renamed.keptSpec });
3220
+ }
1229
3221
  migrated += `${text.slice(copiedUpTo, found.start)}${renamed.to}`;
1230
3222
  copiedUpTo = found.end;
1231
3223
  }
1232
3224
  return { text: migrated + text.slice(copiedUpTo), changes, manualSteps };
1233
3225
  }
3226
+ function importedSpecifiers(text, strings) {
3227
+ const lineOf = lineLocator(text);
3228
+ return strings.filter((found) => found.isModuleSpecifier).map((found) => ({
3229
+ specifier: renamedSpecifier(found.value)?.to ?? found.value,
3230
+ line: lineOf(found.start)
3231
+ }));
3232
+ }
1234
3233
 
1235
- const ENTRY_RE = /^(\s+)(["']?)([\w@./-]+)\2:\s*(["']?)([^"'#\s]+)\4/;
1236
- const TOP_LEVEL_KEY_RE = /^([\w-]+):/;
1237
- const KEY_RE = /^\s+(["']?)(\S.*?)\1:(?:\s|$)/;
1238
- const CATALOG_SECTIONS = /* @__PURE__ */ new Set(["catalog", "catalogs"]);
1239
- const REPORTED_SECTIONS = /* @__PURE__ */ new Set(["overrides", "patchedDependencies"]);
1240
- function migrateEntry(line, version) {
1241
- const entry = ENTRY_RE.exec(line);
1242
- const renamed = entry === null ? void 0 : PACKAGE_RENAMES.get(entry[3]);
1243
- if (entry === null || renamed === void 0) {
1244
- return { line };
1245
- }
1246
- const [whole, indent, keyQuote, name, specQuote, spec] = entry;
1247
- const rest = line.slice(whole.length);
1248
- const decision = migrateVersionSpec(spec, version);
1249
- const key = `${keyQuote}${renamed}${keyQuote}`;
1250
- const migrated = `${indent}${key}: ${specQuote}${decision.spec}${specQuote}${rest}`;
1251
- const change = { kind: "catalog-entry-renamed", from: name, to: renamed };
1252
- if (decision.kind === "kept" && decision.isReported) {
1253
- return {
1254
- line: migrated,
1255
- change,
1256
- manualStep: { kind: "spec-kept", name: renamed, spec: hideUrlCredentials(spec) }
3234
+ const PNPM_WORKSPACE_FILE = {
3235
+ reportedSections: /* @__PURE__ */ new Set(["overrides", "patchedDependencies"])
3236
+ };
3237
+ const YARNRC_FILE = { reportedSections: /* @__PURE__ */ new Set() };
3238
+ function migrateCatalogs(workspace, target, findings) {
3239
+ for (const { name, label, map } of catalogMaps(workspace)) {
3240
+ const context = {
3241
+ ...target,
3242
+ map: { kind: "catalog", catalog: label, name },
3243
+ findings
1257
3244
  };
3245
+ migrateSpecMap(specEntries(map), context);
1258
3246
  }
1259
- return { line: migrated, change };
1260
3247
  }
1261
- function reportEntry(line, field) {
1262
- const key = KEY_RE.exec(line)?.[2];
1263
- return key === void 0 ? [] : overriddenPackages({ [key]: null }).map((name) => ({
1264
- kind: "override-names-package",
1265
- field,
1266
- name
1267
- }));
3248
+ function reportSections(workspace, file, findings) {
3249
+ for (const field of file.reportedSections) {
3250
+ for (const name of overriddenPackages(valuesAt(workspace, field).flatMap(nestedKeys))) {
3251
+ findings.manualSteps.push({ kind: "override-names-package", field, name });
3252
+ }
3253
+ }
3254
+ }
3255
+ function migrateWorkspaceCatalogs(text, read, target, file) {
3256
+ if (read.kind === "unreadable") {
3257
+ return unreadableConfig(text, read.offset);
3258
+ }
3259
+ const findings = newFindings();
3260
+ for (const workspace of read.documents.filter(isMap)) {
3261
+ migrateCatalogs(workspace, target, findings);
3262
+ reportSections(workspace, file, findings);
3263
+ }
3264
+ const { edits, changes, manualSteps } = findings;
3265
+ return { text: applyTextEdits(text, edits), changes, manualSteps };
1268
3266
  }
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 };
3267
+
3268
+ const YAML_COMMAND_KEYS = /* @__PURE__ */ new Set([
3269
+ "run",
3270
+ "script",
3271
+ "before_script",
3272
+ "after_script",
3273
+ "before_install",
3274
+ "install",
3275
+ "bash",
3276
+ "pwsh",
3277
+ "powershell",
3278
+ "command",
3279
+ "commands",
3280
+ "entrypoint"
3281
+ ]);
3282
+ const ENVIRONMENT_KEYS = /* @__PURE__ */ new Set(["env"]);
3283
+ const isEnvironmentValue = (value) => value.path.length >= 2 && ENVIRONMENT_KEYS.has(String(value.path.at(-2)));
3284
+ function environmentVariables(text, strings) {
3285
+ return shellVariables(
3286
+ strings.filter(isEnvironmentValue).map((value) => ({ name: String(value.path.at(-1)), value: shellText(text, value) }))
3287
+ );
3288
+ }
3289
+ const DEPENDABOT_NAME_KEYS = /* @__PURE__ */ new Set([
3290
+ "dependency-name",
3291
+ "patterns",
3292
+ "exclude-patterns"
3293
+ ]);
3294
+ const FOLDED_LINE_BREAK_RE = /(?<=\S[^\S\n]*)\n(?=[^\S\n]*\S)/g;
3295
+ const ENV_EXPRESSION_RE = /\$\{\{[ \t]*env\.([A-Za-z_][A-Za-z0-9_]*)[ \t]*\}\}/g;
3296
+ const asShellVariable = (expression, name) => `$${name}`.padEnd(expression.length, " ");
3297
+ function shellText(text, value) {
3298
+ const slice = text.slice(value.content.start, value.content.end);
3299
+ const unfolded = value.isFolded ? slice.replace(FOLDED_LINE_BREAK_RE, " ") : slice;
3300
+ return unfolded.replace(ENV_EXPRESSION_RE, asShellVariable);
3301
+ }
3302
+ const valuesOf = (strings, keys) => strings.filter((string) => keys.has(ownerKey(string.path) ?? ""));
3303
+ const EXEC_FORM_KEYS = /* @__PURE__ */ new Set(["command", "entrypoint"]);
3304
+ const isExecFormItem = (value) => typeof value.path.at(-1) === "number" && EXEC_FORM_KEYS.has(String(value.path.at(-2)));
3305
+ const listOf = (item) => item.path.slice(0, -1).join(".");
3306
+ const isNextItem = (previous, item) => listOf(previous) === listOf(item) && Number(item.path.at(-1)) > Number(previous.path.at(-1));
3307
+ function execFormCommands(text, items) {
3308
+ const commands = [];
3309
+ let previous;
3310
+ let command = [];
3311
+ for (const item of items) {
3312
+ if (previous === void 0 || !isNextItem(previous, item)) {
3313
+ command = [];
3314
+ commands.push(command);
3315
+ }
3316
+ const { start, end } = item.content;
3317
+ command.push({ start, end, value: item.value, raw: text.slice(start, end) });
3318
+ previous = item;
1274
3319
  }
3320
+ return commands;
3321
+ }
3322
+ function valueFindings(text, value, locate, variables) {
3323
+ const { start } = value.content;
3324
+ const commands = shellCommands(shellText(text, value));
3325
+ const found = commandWordFindings(commands, (offset) => locate(start + offset), variables);
3326
+ return { ...found, edits: shiftedEdits(found.edits, start) };
3327
+ }
3328
+ const shiftedEdits = (edits, offset) => edits.map((edit) => ({ ...edit, start: edit.start + offset, end: edit.end + offset }));
3329
+ function appliedFindings(text, found) {
1275
3330
  return {
1276
- body: line.slice(0, -CARRIAGE_RETURN.length),
1277
- restore: (body) => `${body}${CARRIAGE_RETURN}`
3331
+ text: applyTextEdits(
3332
+ text,
3333
+ found.flatMap((findings) => findings.edits)
3334
+ ),
3335
+ changes: found.flatMap((findings) => findings.changes),
3336
+ manualSteps: found.flatMap((findings) => findings.manualSteps)
1278
3337
  };
1279
3338
  }
1280
- function migrateWorkspaceCatalogs(text, version) {
3339
+ function migrateYamlCommands(text, file, strings) {
3340
+ const values = valuesOf(strings, YAML_COMMAND_KEYS);
3341
+ const lineOf = lineLocator(text);
3342
+ const locate = (offset) => `${file}:${lineOf(offset)}`;
3343
+ const commandValues = new Set(values.filter((value) => !isExecFormItem(value)));
3344
+ const shellValues = strings.filter(
3345
+ (value) => commandValues.has(value) || isEnvironmentValue(value)
3346
+ );
3347
+ const variables = environmentVariables(text, strings);
3348
+ return appliedFindings(text, [
3349
+ ...shellValues.map((value) => valueFindings(text, value, locate, variables)),
3350
+ commandWordFindings(execFormCommands(text, values.filter(isExecFormItem)), locate)
3351
+ ]);
3352
+ }
3353
+ const SCRIPTS_KEY = "scripts";
3354
+ const isScript = (string) => string.path.length === 2 && string.path[0] === SCRIPTS_KEY;
3355
+ function migrateYamlScripts(text, strings) {
3356
+ const scripts = strings.filter(isScript);
3357
+ return appliedFindings(
3358
+ text,
3359
+ scripts.map((script) => valueFindings(text, script, () => script.path.join(".")))
3360
+ );
3361
+ }
3362
+ function migrateDependabotConfig(text, file, strings) {
1281
3363
  const changes = [];
1282
- const manualSteps = [];
1283
- let section;
1284
- const lines = text.split(LINE_FEED).map((rawLine) => {
1285
- const { body: line, restore } = withoutCarriageReturn(rawLine);
1286
- const topLevelKey = TOP_LEVEL_KEY_RE.exec(line)?.[1];
1287
- if (topLevelKey !== void 0) {
1288
- section = topLevelKey;
1289
- return rawLine;
1290
- }
1291
- if (section !== void 0 && REPORTED_SECTIONS.has(section)) {
1292
- manualSteps.push(...reportEntry(line, section));
1293
- return rawLine;
1294
- }
1295
- if (section === void 0 || !CATALOG_SECTIONS.has(section)) {
1296
- return rawLine;
1297
- }
1298
- const result = migrateEntry(line, version);
1299
- changes.push(...result.change ? [result.change] : []);
1300
- manualSteps.push(...result.manualStep ? [result.manualStep] : []);
1301
- return restore(result.line);
1302
- });
1303
- return changes.length === 0 ? { ...unchanged(text), manualSteps } : { text: lines.join(LINE_FEED), changes, manualSteps };
3364
+ const edits = [];
3365
+ const lineOf = lineLocator(text);
3366
+ for (const value of valuesOf(strings, DEPENDABOT_NAME_KEYS)) {
3367
+ const from = value.value;
3368
+ const to = PACKAGE_RENAMES.get(from);
3369
+ if (to !== void 0) {
3370
+ const { start, end } = value.content;
3371
+ const location = `${file}:${lineOf(start)}`;
3372
+ changes.push({ kind: "package-name-renamed", location, from, to });
3373
+ edits.push({ start, end, text: to });
3374
+ }
3375
+ }
3376
+ return { text: applyTextEdits(text, edits), changes, manualSteps: [] };
1304
3377
  }
1305
3378
 
1306
- const SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([".sh", ".bash", ".zsh", ".yml", ".yaml"]);
1307
- const SCRIPT_NAMES = /* @__PURE__ */ new Set(["Makefile", "justfile", "Dockerfile"]);
3379
+ const SHELL_EXTENSIONS = /* @__PURE__ */ new Set([".sh", ".bash", ".zsh"]);
3380
+ const MAKEFILE_EXTENSION = ".mk";
3381
+ const YAML_EXTENSIONS = /* @__PURE__ */ new Set([".yml", ".yaml"]);
3382
+ const MAKEFILE_NAMES = /* @__PURE__ */ new Set(["Makefile", "makefile", "GNUmakefile"]);
3383
+ const SHELL_FILE_NAMES = /* @__PURE__ */ new Set(["justfile", "Justfile", ".justfile"]);
3384
+ const DOCKERFILE_NAMES = /* @__PURE__ */ new Set(["Dockerfile", "Containerfile"]);
3385
+ const HUSKY_FOLDER = ".husky/";
3386
+ const HUSKY_INTERNALS = ".husky/_/";
3387
+ const NX_PROJECT_FILE = "project.json";
3388
+ const DENO_CONFIG_FILES = /* @__PURE__ */ new Set(["deno.json", "deno.jsonc"]);
3389
+ const IMPORT_MAP_FILE = "import_map.json";
3390
+ const DEPENDABOT_FILES = /* @__PURE__ */ new Set([
3391
+ ".github/dependabot.yml",
3392
+ ".github/dependabot.yaml"
3393
+ ]);
1308
3394
  const WORKSPACE_FILE = "pnpm-workspace.yaml";
3395
+ const YARNRC_FILE_NAME = ".yarnrc.yml";
3396
+ const YAML_MANIFEST_FILES = /* @__PURE__ */ new Set(["package.yaml", "package.yml"]);
3397
+ const JSON5_MANIFEST_FILE = "package.json5";
1309
3398
  const MANIFEST_FILE = "package.json";
1310
3399
  const LOCK_FILES = /* @__PURE__ */ new Set(["pnpm-lock.yaml", "yarn.lock", "bun.lock"]);
1311
3400
  function baseName(path) {
@@ -1316,31 +3405,121 @@ function extensionOf(path) {
1316
3405
  const dot = name.lastIndexOf(".");
1317
3406
  return dot <= 0 ? "" : name.slice(dot);
1318
3407
  }
3408
+
3409
+ const isHuskyHook = (path) => (path.startsWith(HUSKY_FOLDER) || path.includes(`/${HUSKY_FOLDER}`)) && !path.includes(HUSKY_INTERNALS);
3410
+ const isDependabotConfig = (path) => [...DEPENDABOT_FILES].some((file) => path === file || path.endsWith(`/${file}`));
3411
+ const withSteps = (migration, steps) => ({
3412
+ ...migration,
3413
+ manualSteps: [...migration.manualSteps, ...steps]
3414
+ });
3415
+ function migrateSource(text, path, context) {
3416
+ const strings = context.scanner.scan(text, path);
3417
+ const migrated = chain(
3418
+ migrateModuleSpecifiers(text, strings, context.version),
3419
+ (renamed) => context.rewriter.rewrite(renamed, path)
3420
+ );
3421
+ const imports = importedSpecifiers(text, strings);
3422
+ const undeclared = undeclaredImportSteps(imports, path, context.manifests);
3423
+ return withSteps(migrated, [...commandStringSteps(text, strings), ...undeclared]);
3424
+ }
1319
3425
  function chain(first, next) {
1320
3426
  const second = next(first.text);
1321
3427
  const changes = [...first.changes, ...second.changes];
1322
3428
  const manualSteps = [...first.manualSteps, ...second.manualSteps];
1323
3429
  return { text: second.text, changes, manualSteps };
1324
3430
  }
3431
+ function migrateConfig(text, read, steps) {
3432
+ const first = read(text);
3433
+ if (first.kind === "unreadable") {
3434
+ return unreadableConfig(text, first.offset);
3435
+ }
3436
+ return steps.reduce(
3437
+ (done, step) => chain(done, (current) => step(current, current === text ? first : read(current))),
3438
+ unchanged(text)
3439
+ );
3440
+ }
3441
+ const stringsStep = (migrate) => (text, read) => read.kind === "unreadable" ? unreadableConfig(text, read.offset) : migrate(text, documentStrings(read.documents));
3442
+ const jsonCommandsStep = (patterns) => stringsStep((text, strings) => migrateJsonCommands(text, strings, patterns));
3443
+ const jsonConfig = (steps) => (text, _path, context) => migrateConfig(text, (current) => context.readers.json.read(current), steps(context));
3444
+ const yamlConfig = (steps) => (text, path, context) => migrateConfig(text, (current) => context.readers.yaml.read(current), steps(context, path));
3445
+ const whenNamed = (migrate) => (text, path, context) => namesOriginalPackage(text) ? migrate(text, path, context) : unchanged(text);
1325
3446
  const FILE_KINDS = [
1326
3447
  {
1327
3448
  matches: (path) => baseName(path) === MANIFEST_FILE,
1328
- migrate: (text, _path, context) => migratePackageManifest(text, context.version)
3449
+ migrate: jsonConfig(({ version, catalogs }) => [
3450
+ (text, read) => migratePackageManifest(text, read, { version, catalogs }),
3451
+ jsonCommandsStep(PACKAGE_SCRIPT_PATHS)
3452
+ ])
3453
+ },
3454
+ {
3455
+ matches: (path) => YAML_MANIFEST_FILES.has(baseName(path)),
3456
+ migrate: yamlConfig(({ version, catalogs }) => [
3457
+ (text, read) => migratePackageManifest(text, read, { version, catalogs }),
3458
+ stringsStep(migrateYamlScripts)
3459
+ ])
3460
+ },
3461
+ {
3462
+ matches: (path) => baseName(path) === JSON5_MANIFEST_FILE,
3463
+ migrate: unreadManifest
1329
3464
  },
1330
3465
  {
1331
3466
  matches: (path) => baseName(path) === WORKSPACE_FILE,
1332
- migrate: (text, _path, context) => migrateWorkspaceCatalogs(text, context.version)
3467
+ migrate: yamlConfig(({ version, catalogs }) => [
3468
+ (text, read) => migrateWorkspaceCatalogs(text, read, { version, catalogs }, PNPM_WORKSPACE_FILE)
3469
+ ])
3470
+ },
3471
+ {
3472
+ matches: (path) => baseName(path) === YARNRC_FILE_NAME,
3473
+ migrate: yamlConfig(({ version, catalogs }) => [
3474
+ (text, read) => migrateWorkspaceCatalogs(text, read, { version, catalogs }, YARNRC_FILE)
3475
+ ])
1333
3476
  },
1334
3477
  {
1335
3478
  matches: (path) => SOURCE_EXTENSIONS.has(extensionOf(path)),
1336
- migrate: (text, path, context) => chain(
1337
- migrateModuleSpecifiers(text, context.scanner.scan(text, path)),
1338
- (renamed) => context.rewriter.rewrite(renamed, path)
3479
+ migrate: migrateSource
3480
+ },
3481
+ {
3482
+ matches: (path) => baseName(path) === NX_PROJECT_FILE,
3483
+ migrate: jsonConfig(() => [jsonCommandsStep(NX_COMMAND_PATHS)])
3484
+ },
3485
+ {
3486
+ matches: (path) => DENO_CONFIG_FILES.has(baseName(path)),
3487
+ migrate: jsonConfig(({ version }) => [
3488
+ (text, read) => migrateImportMap(text, read, version),
3489
+ jsonCommandsStep(DENO_TASK_PATHS)
3490
+ ])
3491
+ },
3492
+ {
3493
+ matches: (path) => baseName(path) === IMPORT_MAP_FILE,
3494
+ migrate: jsonConfig(({ version }) => [(text, read) => migrateImportMap(text, read, version)])
3495
+ },
3496
+ {
3497
+ matches: isDependabotConfig,
3498
+ migrate: whenNamed(
3499
+ yamlConfig((_context, path) => [
3500
+ stringsStep((text, strings) => migrateDependabotConfig(text, path, strings))
3501
+ ])
3502
+ )
3503
+ },
3504
+ {
3505
+ matches: (path) => YAML_EXTENSIONS.has(extensionOf(path)),
3506
+ migrate: whenNamed(
3507
+ yamlConfig((_context, path) => [
3508
+ stringsStep((text, strings) => migrateYamlCommands(text, path, strings))
3509
+ ])
1339
3510
  )
1340
3511
  },
1341
3512
  {
1342
- matches: (path) => SCRIPT_EXTENSIONS.has(extensionOf(path)) || SCRIPT_NAMES.has(baseName(path)),
1343
- migrate: (text, path) => migrateCommandLines(text, path)
3513
+ matches: (path) => DOCKERFILE_NAMES.has(baseName(path)),
3514
+ migrate: whenNamed((text, path) => migrateDockerfile(text, path))
3515
+ },
3516
+ {
3517
+ matches: (path) => MAKEFILE_NAMES.has(baseName(path)) || extensionOf(path) === MAKEFILE_EXTENSION,
3518
+ migrate: whenNamed((text, path) => migrateMakefile(text, path))
3519
+ },
3520
+ {
3521
+ matches: (path) => SHELL_EXTENSIONS.has(extensionOf(path)) || SHELL_FILE_NAMES.has(baseName(path)) || isHuskyHook(path),
3522
+ migrate: whenNamed((text, path) => migrateCommandLines(text, path))
1344
3523
  }
1345
3524
  ];
1346
3525
  function fileMigrationFor(path) {
@@ -1350,118 +3529,327 @@ function fileMigrationFor(path) {
1350
3529
  return FILE_KINDS.find((kind) => kind.matches(path))?.migrate;
1351
3530
  }
1352
3531
 
1353
- async function migrateFile(path, dependencies, input) {
3532
+ const readJson = (text, readers) => readers.json.read(text);
3533
+ const readYaml = (text, readers) => readers.yaml.read(text);
3534
+ function planSourceOf(path) {
3535
+ const name = baseName(path);
3536
+ if (name === MANIFEST_FILE) {
3537
+ return { role: "manifest", read: readJson };
3538
+ }
3539
+ if (DENO_CONFIG_FILES.has(name) || name === IMPORT_MAP_FILE) {
3540
+ return { role: "import-map", read: readJson };
3541
+ }
3542
+ if (YAML_MANIFEST_FILES.has(name)) {
3543
+ return { role: "manifest", read: readYaml };
3544
+ }
3545
+ if (name === JSON5_MANIFEST_FILE) {
3546
+ return { role: "manifest", read: void 0 };
3547
+ }
3548
+ const isCatalogFile = name === WORKSPACE_FILE || name === YARNRC_FILE_NAME;
3549
+ return isCatalogFile ? { role: "catalogs", read: readYaml } : void 0;
3550
+ }
3551
+
3552
+ async function planFile(path, dependencies, target) {
1354
3553
  const migrate = fileMigrationFor(path);
1355
3554
  if (migrate === void 0) {
1356
- return void 0;
3555
+ return { kind: "not-migrated" };
1357
3556
  }
1358
- const text = await dependencies.files.read(path);
1359
- const { rewriter, scanner } = dependencies;
1360
- const context = { version: input.version, rewriter, scanner };
1361
- const migration = migrate(text, path, context);
1362
- const isChanged = migration.text !== text;
1363
- if (isChanged && input.isWrite) {
1364
- await dependencies.files.write(path, migration.text);
3557
+ const read = await dependencies.files.read(path);
3558
+ if (read.kind === "skipped") {
3559
+ return read;
1365
3560
  }
3561
+ const { rewriter, scanner, readers } = dependencies;
3562
+ const context = { ...target, rewriter, scanner, readers };
3563
+ const migration = migrate(read.text, path, context);
3564
+ const isChanged = migration.text !== read.text;
1366
3565
  const { changes, manualSteps } = migration;
1367
3566
  if (isChanged || manualSteps.length > 0) {
1368
3567
  dependencies.logger.info("file_migrated", { path, isChanged, changes, manualSteps });
1369
3568
  }
1370
- return { path, isChanged, changes, manualSteps };
3569
+ const report = { path, isChanged, changes, manualSteps };
3570
+ return { kind: "planned", file: { report, text: isChanged ? migration.text : void 0 } };
1371
3571
  }
1372
- async function migrateProject(dependencies, input) {
1373
- const { files, skipped } = await dependencies.files.listFiles();
1374
- const paths = [...files].sort();
1375
- dependencies.logger.info("migration_started", { fileCount: paths.length, ...input });
1376
- const reports = [];
3572
+ async function readForPlan(path, source, dependencies) {
3573
+ const read = await dependencies.files.read(path);
3574
+ if (read.kind !== "read") {
3575
+ return { namesOriginal: false, roots: void 0 };
3576
+ }
3577
+ const namesOriginal = namesOriginalPackage(read.text);
3578
+ if (!namesOriginal && !namesUninstalledPackage(read.text)) {
3579
+ return { namesOriginal, roots: [] };
3580
+ }
3581
+ const parsed = source.read?.(read.text, dependencies.readers);
3582
+ const roots = parsed?.kind === "read" ? parsed.documents.filter(isMap) : void 0;
3583
+ return { namesOriginal, roots };
3584
+ }
3585
+ const DECLARED_NAMES = { manifest: declaredNames, "import-map": importMapNames, catalogs: void 0 };
3586
+ async function planProjectTarget(paths, dependencies, version) {
3587
+ const documents = { manifest: [], catalogs: [] };
3588
+ const manifests = [];
1377
3589
  for (const path of paths) {
1378
- const report = await migrateFile(path, dependencies, input);
1379
- if (report !== void 0 && (report.isChanged || report.manualSteps.length > 0)) {
1380
- reports.push(report);
3590
+ const source = planSourceOf(path);
3591
+ if (source === void 0) {
3592
+ continue;
3593
+ }
3594
+ const { namesOriginal, roots } = await readForPlan(path, source, dependencies);
3595
+ if (namesOriginal && source.role !== "import-map") {
3596
+ documents[source.role].push(...roots ?? []);
3597
+ }
3598
+ const names = DECLARED_NAMES[source.role];
3599
+ if (names !== void 0) {
3600
+ manifests.push({ path, names: roots && new Set(roots.flatMap(names)) });
1381
3601
  }
1382
3602
  }
1383
- dependencies.logger.info("migration_finished", {
1384
- changedFiles: reports.length,
1385
- skippedFiles: skipped.length
3603
+ const catalogs = planCatalogs({
3604
+ manifests: documents.manifest,
3605
+ catalogFiles: documents.catalogs
1386
3606
  });
1387
- return { files: reports, skipped };
3607
+ return { version, catalogs, manifests };
1388
3608
  }
1389
-
1390
- function assertNever(value) {
1391
- throw new Error(`Unexpected variant: ${JSON.stringify(value)}`);
3609
+ async function planProject(paths, dependencies, version) {
3610
+ const files = [];
3611
+ const skipped = [];
3612
+ const target = await planProjectTarget(paths, dependencies, version);
3613
+ for (const path of paths) {
3614
+ const plan = await planFile(path, dependencies, target);
3615
+ if (plan.kind === "skipped") {
3616
+ skipped.push({ path, reason: plan.reason });
3617
+ dependencies.logger.warn("file_skipped", { path, reason: plan.reason });
3618
+ } else if (plan.kind === "planned") {
3619
+ const { report } = plan.file;
3620
+ const isReported = report.isChanged || report.manualSteps.length > 0;
3621
+ files.push(...isReported ? [plan.file] : []);
3622
+ }
3623
+ }
3624
+ return { files, skipped };
3625
+ }
3626
+ async function writeFiles(planned, dependencies) {
3627
+ const changed = planned.flatMap(
3628
+ ({ report, text }) => text === void 0 ? [] : [{ path: report.path, text }]
3629
+ );
3630
+ const written = [];
3631
+ for (const [index, { path, text }] of changed.entries()) {
3632
+ const result = await dependencies.files.write(path, text);
3633
+ if (result.kind === "failed") {
3634
+ dependencies.logger.warn("file_write_failed", { path, reason: result.reason, written });
3635
+ const notWritten = changed.slice(index + 1).map((file) => file.path);
3636
+ return { kind: "failed", written, failed: { path, reason: result.reason }, notWritten };
3637
+ }
3638
+ written.push(path);
3639
+ }
3640
+ return { kind: "written", paths: written };
3641
+ }
3642
+ async function migrateProject(dependencies, input) {
3643
+ const listing = await dependencies.files.listFiles();
3644
+ const paths = [...listing.files].sort();
3645
+ dependencies.logger.info("migration_started", { fileCount: paths.length, ...input });
3646
+ const plan = await planProject(paths, dependencies, input.version);
3647
+ const skipped = [...listing.skipped, ...plan.skipped];
3648
+ const write = input.isWrite ? await writeFiles(plan.files, dependencies) : { kind: "dry-run" };
3649
+ dependencies.logger.info("migration_finished", {
3650
+ changedFiles: plan.files.length,
3651
+ skippedFiles: skipped.length,
3652
+ write: write.kind
3653
+ });
3654
+ return { files: plan.files.map((file) => file.report), skipped, write };
1392
3655
  }
1393
3656
 
1394
3657
  const IR_CALLBACKS_GUIDE = "https://github.com/openapi-typesafe/openapi-typesafe/blob/main/docs/node.md#ir-callbacks";
1395
3658
  const OPENAPI_FETCH_ALIAS_GUIDE = "https://github.com/openapi-typesafe/openapi-typesafe/blob/main/docs/fetch/index.md#libraries-built-on-openapi-fetch";
1396
- function changeMessage(change) {
1397
- switch (change.kind) {
1398
- case "dependency-renamed":
1399
- return `${change.field}: ${change.from} \u2192 ${change.to}@${change.spec}`;
1400
- case "catalog-entry-renamed":
1401
- return `catalog: ${change.from} \u2192 ${change.to}`;
1402
- case "command-renamed":
1403
- return `${change.location}: openapi-typescript \u2192 openapi-typesafe`;
1404
- case "flag-renamed":
1405
- return `${change.location}: ${change.from} \u2192 ${change.to}`;
1406
- case "specifier-renamed":
1407
- return `line ${change.line}: "${change.from}" \u2192 "${change.to}"`;
1408
- case "transform-rewritten":
1409
- return `line ${change.line}: transform returns type text; irCallbacks: true added`;
1410
- default:
1411
- return assertNever(change);
1412
- }
1413
- }
1414
3659
  const KEEP_REASONS = {
1415
3660
  "other-callbacks": "postTransform or transformProperty are set too, and they need TypeScript nodes",
1416
3661
  "not-a-function": "it is not an inline function or a function of this file",
1417
3662
  "shared-function": "the function it names is used elsewhere too",
3663
+ "shared-options": "its options object is used elsewhere too",
1418
3664
  "unknown-return": "a return value is not a plain ts.factory type"
1419
3665
  };
1420
3666
  const PEER_NOTE = "users of this package must switch too (a breaking change for them)";
1421
3667
  const PINNED_NOTE = "change it to openapi-typesafe@<version>";
3668
+ const OLD_FILE_NOTE = "is a file of openapi-typescript, which is no longer installed: run openapi-typesafe instead";
3669
+ const FLAGS_IN_VARIABLE_NOTE = "holds old flags of the generator and may serve other commands, so it was kept: rename";
3670
+ const NOT_ONLY_GENERATOR_NOTE = "runs openapi-typescript in some of its definitions in this file and another program in others, so the flags after it were kept; where it runs the generator, rename";
3671
+ const COMMAND_STRING_NOTE = "a string runs openapi-typescript, so it was kept; change it to";
3672
+ const OPTIONS_NOTE = `the options of openapiTS() are not an object written in this file: check them for transform, postTransform and transformProperty. See ${IR_CALLBACKS_GUIDE}`;
3673
+ const MANIFEST_NOT_READ_NOTE = "is a JSON5 manifest, which the tool does not read: rename the packages and the generator command by hand, or write it as package.json and run the tool again";
1422
3674
  const DEEP_IMPORT_NOTE = "imports a file inside the package; check that it exists";
3675
+ const DYNAMIC_NOTE = "builds its module name at run time from an old package name, so it was kept; rename it to";
1423
3676
  const STRING_NOTE = "is a string, not a module specifier, so it was kept";
1424
3677
  const STRING_FIX_NOTE = "if it names the package, change it to";
1425
3678
  const KEPT_NOTE = `it works, with a deprecation warning. See ${IR_CALLBACKS_GUIDE}`;
1426
3679
  const LEGACY_CALLBACK_NOTE = `receives TypeScript nodes (deprecated). See ${IR_CALLBACKS_GUIDE}`;
3680
+ const HELPER_TYPES_ENTRY = "openapi-typesafe-fetch/helpers";
3681
+ function packageNotDeclaredMessage(step) {
3682
+ const { line, name, installers } = step;
3683
+ return `line ${line}: no package.json or Deno import map at or above this file lists ${name}; ${installers.join(" and ")} installed it, the openapi-typesafe packages do not: add it to the dependencies, or import the types from ${HELPER_TYPES_ENTRY}`;
3684
+ }
1427
3685
  function oldPeerMessage(step) {
1428
3686
  const { dependent, peer, range } = step;
1429
3687
  const successor = PACKAGE_RENAMES.get(peer) ?? peer;
1430
3688
  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
3689
  }
3690
+ function pinnedPackageMessage(step) {
3691
+ const successor = PACKAGE_RENAMES.get(step.name) ?? step.name;
3692
+ return `${step.location} names a pinned ${step.name}@<version>: change it to ${successor}@<version>`;
3693
+ }
3694
+ const flagRenames = (flags) => flags.map((flag) => `${flag} to ${FLAG_RENAMES.get(flag) ?? flag}`).join(", ");
3695
+ function flagsInVariableMessage(step) {
3696
+ return `${step.location}: ${step.variable} ${FLAGS_IN_VARIABLE_NOTE} ${flagRenames(step.flags)}`;
3697
+ }
3698
+ function notOnlyGeneratorMessage(step) {
3699
+ return `${step.location}: ${step.variable} ${NOT_ONLY_GENERATOR_NOTE} ${flagRenames(step.flags)}`;
3700
+ }
3701
+ const COMMAND_STEP_KINDS = /* @__PURE__ */ new Set([
3702
+ "versioned-command",
3703
+ "flags-in-variable",
3704
+ "variable-not-only-generator",
3705
+ "pinned-package",
3706
+ "old-package-file",
3707
+ "command-string"
3708
+ ]);
3709
+ const isCommandStep = (step) => COMMAND_STEP_KINDS.has(step.kind);
3710
+ function commandStepMessage(step) {
3711
+ switch (step.kind) {
3712
+ case "versioned-command":
3713
+ return `${step.location} runs a pinned openapi-typescript@<version>: ${PINNED_NOTE}`;
3714
+ case "flags-in-variable":
3715
+ return flagsInVariableMessage(step);
3716
+ case "variable-not-only-generator":
3717
+ return notOnlyGeneratorMessage(step);
3718
+ case "pinned-package":
3719
+ return pinnedPackageMessage(step);
3720
+ case "old-package-file":
3721
+ return `${step.location}: ${step.path} ${OLD_FILE_NOTE}`;
3722
+ case "command-string":
3723
+ return `line ${step.line}: ${COMMAND_STRING_NOTE} "${step.to}"`;
3724
+ default:
3725
+ return assertNever(step);
3726
+ }
3727
+ }
3728
+ function uneditedStepMessage(step) {
3729
+ switch (step.kind) {
3730
+ case "config-unreadable":
3731
+ return step.position === void 0 ? "nests too deep to be read; migrate it by hand" : `cannot be parsed at line ${step.position.line}, column ${step.position.column}; migrate it by hand`;
3732
+ case "manifest-not-read":
3733
+ return MANIFEST_NOT_READ_NOTE;
3734
+ case "entry-not-edited":
3735
+ return `${step.field}: ${step.name} has a value the tool does not read; rename it by hand`;
3736
+ case "name-collision":
3737
+ return `${step.field} has both ${step.from} and ${step.to}, so ${step.from} was kept: remove one of them`;
3738
+ case "catalog-reference-kept":
3739
+ return `${step.field}: ${step.name} was kept, because catalog "${step.catalog}" keeps ${step.name}: rename the entry and the dependency together`;
3740
+ case "catalog-entry-in-use":
3741
+ return `${step.catalog}: ${step.name} was kept, because a dependency or an override still names it: rename the entry and what names it together`;
3742
+ default:
3743
+ return assertNever(step);
3744
+ }
3745
+ }
3746
+ function sourceStepMessage(step) {
3747
+ switch (step.kind) {
3748
+ case "options-not-inspected":
3749
+ return `line ${step.line}: ${OPTIONS_NOTE}`;
3750
+ case "deep-import":
3751
+ return `line ${step.line}: "${step.specifier}" ${DEEP_IMPORT_NOTE}`;
3752
+ case "dynamic-specifier":
3753
+ return `line ${step.line}: \`${step.value}\` ${DYNAMIC_NOTE} ${step.to}`;
3754
+ case "string-names-package":
3755
+ return `line ${step.line}: "${step.value}" ${STRING_NOTE}; ${STRING_FIX_NOTE} "${step.to}"`;
3756
+ case "transform-not-rewritten":
3757
+ return `line ${step.line}: transform kept (${KEEP_REASONS[step.reason]}); ${KEPT_NOTE}`;
3758
+ case "legacy-callback":
3759
+ return `line ${step.line}: ${step.callback} ${LEGACY_CALLBACK_NOTE}`;
3760
+ case "package-not-declared":
3761
+ return packageNotDeclaredMessage(step);
3762
+ default:
3763
+ return assertNever(step);
3764
+ }
3765
+ }
1432
3766
  function manualStepMessage(step) {
3767
+ if (isCommandStep(step)) {
3768
+ return commandStepMessage(step);
3769
+ }
1433
3770
  switch (step.kind) {
1434
- case "manifest-unreadable":
1435
- return `package.json is not valid JSON (${step.reason}); migrate it by hand`;
3771
+ case "config-unreadable":
3772
+ case "manifest-not-read":
3773
+ case "entry-not-edited":
3774
+ case "name-collision":
3775
+ case "catalog-reference-kept":
3776
+ case "catalog-entry-in-use":
3777
+ return uneditedStepMessage(step);
1436
3778
  case "peer-dependency-renamed":
1437
3779
  return `${step.name} is a peer dependency: ${PEER_NOTE}`;
1438
3780
  case "spec-kept":
1439
3781
  return `${step.name} kept the spec "${step.spec}": point it at an openapi-typesafe version`;
1440
3782
  case "override-names-package":
1441
3783
  return `"${step.field}" names ${step.name}: update or remove that override`;
1442
- case "versioned-command":
1443
- return `${step.location} runs a pinned openapi-typescript@<version>: ${PINNED_NOTE}`;
3784
+ case "options-not-inspected":
1444
3785
  case "deep-import":
1445
- return `line ${step.line}: "${step.specifier}" ${DEEP_IMPORT_NOTE}`;
3786
+ case "dynamic-specifier":
1446
3787
  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);
1450
3788
  case "transform-not-rewritten":
1451
- return `line ${step.line}: transform kept (${KEEP_REASONS[step.reason]}); ${KEPT_NOTE}`;
1452
3789
  case "legacy-callback":
1453
- return `line ${step.line}: ${step.callback} ${LEGACY_CALLBACK_NOTE}`;
3790
+ case "package-not-declared":
3791
+ return sourceStepMessage(step);
3792
+ case "old-peer-needed":
3793
+ return oldPeerMessage(step);
1454
3794
  default:
1455
3795
  return assertNever(step);
1456
3796
  }
1457
3797
  }
3798
+
3799
+ function changeMessage(change) {
3800
+ switch (change.kind) {
3801
+ case "dependency-renamed":
3802
+ return `${change.field}: ${change.from} \u2192 ${change.to}@${change.spec}`;
3803
+ case "catalog-entry-renamed":
3804
+ return `${change.catalog}: ${change.from} \u2192 ${change.to}@${change.spec}`;
3805
+ case "import-renamed":
3806
+ return `${change.field}: "${change.from}" \u2192 "${change.to}" (${change.target})`;
3807
+ case "alias-renamed":
3808
+ return `${change.field}: ${change.name} \u2192 ${change.spec}`;
3809
+ case "command-renamed":
3810
+ return `${change.location}: openapi-typescript \u2192 openapi-typesafe`;
3811
+ case "flag-renamed":
3812
+ case "package-name-renamed":
3813
+ return `${change.location}: ${change.from} \u2192 ${change.to}`;
3814
+ case "specifier-renamed":
3815
+ return `line ${change.line}: "${change.from}" \u2192 "${change.to}"`;
3816
+ case "transform-rewritten":
3817
+ return `line ${change.line}: transform returns type text; irCallbacks: true added`;
3818
+ case "declaration-removed":
3819
+ return `line ${change.line}: ${change.name} removed; only the old transform returns used it`;
3820
+ default:
3821
+ return assertNever(change);
3822
+ }
3823
+ }
1458
3824
  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];
3825
+ function skippedMessage(reason) {
3826
+ switch (reason.kind) {
3827
+ case "outside-root":
3828
+ return "it leads outside the project folder (a link, or a path in the git index)";
3829
+ case "not-utf-8":
3830
+ return "it is not UTF-8 text, so it was not changed; check it by hand";
3831
+ case "io-error":
3832
+ return `it could not be read (${reason.code})`;
3833
+ default:
3834
+ return assertNever(reason);
3835
+ }
3836
+ }
3837
+ function writeProblemMessage(reason) {
3838
+ switch (reason.kind) {
3839
+ case "outside-root":
3840
+ return "it leads outside the project folder";
3841
+ case "not-utf-8":
3842
+ return "its text is not UTF-8";
3843
+ case "io-error":
3844
+ return reason.code;
3845
+ default:
3846
+ return assertNever(reason);
3847
+ }
1464
3848
  }
3849
+ const writeFailedMessage = (path, reason) => `Writing stopped: ${path} could not be written (${writeProblemMessage(reason)}).`;
3850
+ const WRITTEN_HEADING = "Written before it:";
3851
+ const NOT_WRITTEN_HEADING = "Not written (still as they were):";
3852
+ const NONE_WRITTEN = "Nothing was written before it.";
1465
3853
  const NEXT_STEPS = [
1466
3854
  "Install: run your package manager's install, so the lockfile has the new packages.",
1467
3855
  "Regenerate your types and commit them once: the file header names openapi-typesafe now, so `--check` fails until you do.",
@@ -1495,10 +3883,29 @@ function skippedSection(skipped) {
1495
3883
  if (skipped.length === 0) {
1496
3884
  return [];
1497
3885
  }
1498
- const lines = skipped.map((file) => ` ${file.path}: ${skipReasonMessage(file.reason)}`);
3886
+ const lines = skipped.map((file) => ` ${file.path}: ${skippedMessage(file.reason)}`);
1499
3887
  return [SKIPPED_HEADING, ...lines, ""];
1500
3888
  }
1501
- function renderReport(migration, isWrite) {
3889
+ const listed = (paths) => paths.map((path) => ` ${path}`);
3890
+ function writeSection(write) {
3891
+ switch (write.kind) {
3892
+ case "dry-run":
3893
+ return [DRY_RUN_NOTE];
3894
+ case "written":
3895
+ return [WRITTEN_NOTE];
3896
+ case "failed":
3897
+ return [
3898
+ writeFailedMessage(write.failed.path, write.failed.reason),
3899
+ ...write.written.length === 0 ? [NONE_WRITTEN] : [WRITTEN_HEADING],
3900
+ ...listed(write.written),
3901
+ ...write.notWritten.length === 0 ? [] : [NOT_WRITTEN_HEADING],
3902
+ ...listed(write.notWritten)
3903
+ ];
3904
+ default:
3905
+ return assertNever(write);
3906
+ }
3907
+ }
3908
+ function renderReport(migration) {
1502
3909
  const reports = migration.files;
1503
3910
  const skipped = skippedSection(migration.skipped);
1504
3911
  if (reports.length === 0) {
@@ -1512,7 +3919,7 @@ function renderReport(migration, isWrite) {
1512
3919
  "Next steps:",
1513
3920
  ...NEXT_STEPS.map((step, index) => ` ${index + 1}. ${step}`),
1514
3921
  "",
1515
- isWrite ? WRITTEN_NOTE : DRY_RUN_NOTE
3922
+ ...writeSection(migration.write)
1516
3923
  ];
1517
3924
  return `${lines.join("\n")}
1518
3925
  `;
@@ -1556,7 +3963,8 @@ Options
1556
3963
 
1557
3964
  Exit codes
1558
3965
  0 Done
1559
- 1 The migration failed (the message and the log say why), or the log could not be created
3966
+ 1 The migration failed (the message and the log say why), a file could not be written
3967
+ (the report names what was written), or the log could not be created
1560
3968
  2 Unknown option or argument
1561
3969
  `;
1562
3970
  const usageError = (message) => `${message}
@@ -1564,6 +3972,12 @@ Run openapi-typesafe-migrate --help for the options.
1564
3972
  `;
1565
3973
  const logNote = (path) => `Log: ${path}
1566
3974
  `;
3975
+ const ROOT_PROBLEMS = {
3976
+ missing: "does not exist",
3977
+ "not-a-directory": "is not a folder; --root takes the project folder"
3978
+ };
3979
+ const rootError = (root, state) => `The project folder ${root} ${ROOT_PROBLEMS[state]}
3980
+ `;
1567
3981
  const logError = (path, message) => `Cannot create the log ${path ?? "in the temporary directory"}: ${message}
1568
3982
  `;
1569
3983
 
@@ -1588,6 +4002,20 @@ function openLog(request, dependencies) {
1588
4002
  return void 0;
1589
4003
  }
1590
4004
  }
4005
+ async function migrateRoot(request, root, logger, dependencies) {
4006
+ const rootState = await dependencies.inspectRoot(root);
4007
+ if (rootState !== "directory") {
4008
+ logger.warn("root_unusable", { root, rootState });
4009
+ dependencies.writeStderr(rootError(root, rootState));
4010
+ return EXIT_CODES.failure;
4011
+ }
4012
+ const files = dependencies.createFiles(root, logger);
4013
+ const { rewriter, scanner, readers } = dependencies;
4014
+ const input = { version: dependencies.version, isWrite: request.isWrite };
4015
+ const migration = await migrateProject({ files, rewriter, scanner, readers, logger }, input);
4016
+ dependencies.writeStdout(renderReport(migration));
4017
+ return migration.write.kind === "failed" ? EXIT_CODES.failure : EXIT_CODES.success;
4018
+ }
1591
4019
  async function runMigration(request, dependencies) {
1592
4020
  const log = openLog(request, dependencies);
1593
4021
  if (log === void 0) {
@@ -1596,12 +4024,7 @@ async function runMigration(request, dependencies) {
1596
4024
  const { logger, path } = log;
1597
4025
  const root = dependencies.resolvePath(dependencies.cwd, request.root ?? ".");
1598
4026
  try {
1599
- const files = dependencies.createFiles(root, logger);
1600
- const { rewriter, scanner } = dependencies;
1601
- const input = { version: dependencies.version, isWrite: request.isWrite };
1602
- const migration = await migrateProject({ files, rewriter, scanner, logger }, input);
1603
- dependencies.writeStdout(renderReport(migration, request.isWrite));
1604
- return EXIT_CODES.success;
4027
+ return await migrateRoot(request, root, logger, dependencies);
1605
4028
  } catch (error) {
1606
4029
  const message = errorMessage(error);
1607
4030
  logger.warn("migration_failed", { root, message });
@@ -1640,15 +4063,23 @@ function createLogger(path) {
1640
4063
  const logPath = isExclusive ? createTemporaryLogPath(tmpdir(), now()) : resolve(path);
1641
4064
  return { logger: createJsonLinesLogger({ path: logPath, now, isExclusive }), path: logPath };
1642
4065
  }
4066
+ const createGit = (root) => createGitFileList({
4067
+ environment: process.env,
4068
+ platform: process.platform,
4069
+ fileSystem: createNodeGitFileSystem(),
4070
+ projectRoot: root
4071
+ });
1643
4072
  const dependencies = {
1644
4073
  version: readVersion(),
1645
4074
  cwd: process.cwd(),
1646
4075
  writeStdout: (text) => process.stdout.write(text),
1647
4076
  writeStderr: (text) => process.stderr.write(text),
1648
- createFiles: (root, logger) => createNodeProjectFiles(root, { git: createGitFileList(), logger }),
4077
+ inspectRoot: inspectProjectRoot,
4078
+ createFiles: (root, logger) => createNodeProjectFiles(root, { git: createGit(root), logger }),
1649
4079
  createLogger,
1650
4080
  rewriter: createTypescriptCallbackRewriter(),
1651
4081
  scanner: createTypescriptStringScanner(),
4082
+ readers: { json: createJsonTextReader(), yaml: createYamlTextReader() },
1652
4083
  resolvePath: resolve
1653
4084
  };
1654
4085
  runCli(process.argv.slice(2), dependencies).then((exitCode) => {