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