@rebasepro/cli 0.23.0 → 0.24.1-canary.g19596ef
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/README.md +19 -3
- package/bin/rebase.js +4 -1
- package/dist/agent-setup-B31F00DC.js +324 -0
- package/dist/agent-setup-B31F00DC.js.map +1 -0
- package/dist/api-keys-Bl1I9q7M.js +525 -0
- package/dist/api-keys-Bl1I9q7M.js.map +1 -0
- package/dist/apps-CNgQJjg5.js +340 -0
- package/dist/apps-CNgQJjg5.js.map +1 -0
- package/dist/args-BnmsEXvu.js +109 -0
- package/dist/args-BnmsEXvu.js.map +1 -0
- package/dist/atlas-dev-database-D_LP_Woq.js +62 -0
- package/dist/atlas-dev-database-D_LP_Woq.js.map +1 -0
- package/dist/auth-aySHEF45.js +414 -0
- package/dist/auth-aySHEF45.js.map +1 -0
- package/dist/branch-pointer-DvSHBjjz.js +2 -0
- package/dist/build-DIHWIqO4.js +336 -0
- package/dist/build-DIHWIqO4.js.map +1 -0
- package/dist/bundle-CqURVN-K.js +1621 -0
- package/dist/bundle-CqURVN-K.js.map +1 -0
- package/dist/bundle.d.ts +12 -3
- package/dist/cli.d.ts +23 -2
- package/dist/cloud-DjByQlkW.js +9667 -0
- package/dist/cloud-DjByQlkW.js.map +1 -0
- package/dist/collection-drift-BKj_BoMq.js +106 -0
- package/dist/collection-drift-BKj_BoMq.js.map +1 -0
- package/dist/commands/__test_support__/parser-specs.d.ts +13 -0
- package/dist/commands/agent-setup.d.ts +116 -0
- package/dist/commands/api-keys.d.ts +86 -23
- package/dist/commands/apps.d.ts +17 -0
- package/dist/commands/auth.d.ts +7 -0
- package/dist/commands/build.d.ts +5 -1
- package/dist/commands/cloud/auth.d.ts +4 -3
- package/dist/commands/cloud/bundle-deploy.d.ts +45 -0
- package/dist/commands/cloud/context.d.ts +44 -5
- package/dist/commands/cloud/cron.d.ts +53 -0
- package/dist/commands/cloud/debug.d.ts +134 -0
- package/dist/commands/cloud/index.d.ts +89 -12
- package/dist/commands/cloud/token-capabilities.d.ts +76 -0
- package/dist/commands/cloud/tokens.d.ts +23 -0
- package/dist/commands/console.d.ts +12 -0
- package/dist/commands/db.d.ts +79 -25
- package/dist/commands/dev.d.ts +181 -13
- package/dist/commands/doctor.d.ts +5 -0
- package/dist/commands/generate_sdk.d.ts +29 -2
- package/dist/commands/init.d.ts +115 -14
- package/dist/commands/schema.d.ts +6 -0
- package/dist/commands/skills.d.ts +126 -7
- package/dist/commands/start.d.ts +44 -0
- package/dist/commands/storage.d.ts +46 -0
- package/dist/commands/upgrade.d.ts +1 -0
- package/dist/consent-ClRHmcxf.js +76 -0
- package/dist/consent-ClRHmcxf.js.map +1 -0
- package/dist/console-D6SJf8k_.js +102 -0
- package/dist/console-D6SJf8k_.js.map +1 -0
- package/dist/context-BqhRydBg.js +1234 -0
- package/dist/context-BqhRydBg.js.map +1 -0
- package/dist/{daemon-entry-LTFKtpHy.js → daemon-entry-Da-D3rmH.js} +3 -4
- package/dist/daemon-entry-Da-D3rmH.js.map +1 -0
- package/dist/daemon-pzsQwDHa.js +2 -0
- package/dist/db-B7RuNuaQ.js +1312 -0
- package/dist/db-B7RuNuaQ.js.map +1 -0
- package/dist/derive-Dl0z2OWi.js +484 -0
- package/dist/derive-Dl0z2OWi.js.map +1 -0
- package/dist/dev-CYr_WXan.js +1599 -0
- package/dist/dev-CYr_WXan.js.map +1 -0
- package/dist/dev-db/atlas-dev-database.d.ts +32 -0
- package/dist/dev-db/daemon-entry.d.ts +13 -0
- package/dist/dev-db/daemon.d.ts +10 -0
- package/dist/dev-db/last-dev-database.d.ts +41 -0
- package/dist/dev-db/prepare.d.ts +29 -3
- package/dist/dev-db/pull.d.ts +90 -5
- package/dist/dev-db/resolve.d.ts +26 -2
- package/dist/dev-preflight-B-LYyBf1.js +2 -0
- package/dist/dev-server-DG8Tt-uf.js +96 -0
- package/dist/dev-server-DG8Tt-uf.js.map +1 -0
- package/dist/doctor-BP8cZ-7F.js +771 -0
- package/dist/doctor-BP8cZ-7F.js.map +1 -0
- package/dist/eject-DizdoecV.js +335 -0
- package/dist/eject-DizdoecV.js.map +1 -0
- package/dist/fold-static-BC9CYicw.js +201 -0
- package/dist/fold-static-BC9CYicw.js.map +1 -0
- package/dist/fold-static.d.ts +26 -3
- package/dist/function-portability-BIvlvR9q.js +265 -0
- package/dist/function-portability-BIvlvR9q.js.map +1 -0
- package/dist/generate_sdk-BPqdJc3Q.js +489 -0
- package/dist/generate_sdk-BPqdJc3Q.js.map +1 -0
- package/dist/index.es.js +199 -22095
- package/dist/index.es.js.map +1 -1
- package/dist/init-Utstvh2Q.js +1149 -0
- package/dist/init-Utstvh2Q.js.map +1 -0
- package/dist/last-dev-database-B58g89uA.js +2 -0
- package/dist/manifest-DRp0UVTj.js +880 -0
- package/dist/manifest-DRp0UVTj.js.map +1 -0
- package/dist/manifest.d.ts +56 -4
- package/dist/normalize-imports-X0Tgze9y.js +107 -0
- package/dist/normalize-imports-X0Tgze9y.js.map +1 -0
- package/dist/package-manager-DSEOwTqM.js +193 -0
- package/dist/package-manager-DSEOwTqM.js.map +1 -0
- package/dist/path-flags-BFgG9V6y.js +91 -0
- package/dist/path-flags-BFgG9V6y.js.map +1 -0
- package/dist/precompress.d.ts +17 -0
- package/dist/prepare-CDN6lSpz.js +1236 -0
- package/dist/prepare-CDN6lSpz.js.map +1 -0
- package/dist/{project-3WCbrrfW.js → project-Wp2zoyeN.js} +34 -184
- package/dist/project-Wp2zoyeN.js.map +1 -0
- package/dist/pull-CrGYMEsy.js +458 -0
- package/dist/pull-CrGYMEsy.js.map +1 -0
- package/dist/resolve-B2O-wKKk.js +2 -0
- package/dist/resources-FD7AfZx8.js +183 -0
- package/dist/resources-FD7AfZx8.js.map +1 -0
- package/dist/schema-DMdr431S.js +125 -0
- package/dist/schema-DMdr431S.js.map +1 -0
- package/dist/skills-DTFibT1m.js +642 -0
- package/dist/skills-DTFibT1m.js.map +1 -0
- package/dist/start-DEa-hOBh.js +267 -0
- package/dist/start-DEa-hOBh.js.map +1 -0
- package/dist/{state-C59Elrnt.js → state-Do7ZJYkF.js} +49 -4
- package/dist/state-Do7ZJYkF.js.map +1 -0
- package/dist/status-DxnJTekv.js +352 -0
- package/dist/status-DxnJTekv.js.map +1 -0
- package/dist/storage-DyVq-f_F.js +303 -0
- package/dist/storage-DyVq-f_F.js.map +1 -0
- package/dist/telemetry/identity.d.ts +10 -1
- package/dist/telemetry-C8Uq8phb.js +129 -0
- package/dist/telemetry-C8Uq8phb.js.map +1 -0
- package/dist/telemetry-EMFRVRQi.js +351 -0
- package/dist/telemetry-EMFRVRQi.js.map +1 -0
- package/dist/unknown-command-DNwDcX3w.js +98 -0
- package/dist/unknown-command-DNwDcX3w.js.map +1 -0
- package/dist/upgrade-CtZtRkTE.js +1332 -0
- package/dist/upgrade-CtZtRkTE.js.map +1 -0
- package/dist/upgrade.d.ts +38 -5
- package/dist/utils/dev-preflight.d.ts +47 -5
- package/dist/utils/dev-server.d.ts +30 -0
- package/dist/utils/path-flags.d.ts +31 -0
- package/dist/utils/project.d.ts +16 -0
- package/dist/utils/temp-archive.d.ts +9 -0
- package/dist/version-Buyyo1ut.js +58 -0
- package/dist/version-Buyyo1ut.js.map +1 -0
- package/package.json +7 -7
- package/templates/eject/Dockerfile +4 -2
- package/templates/eject/backend/src/env.ts +1 -1
- package/templates/eject/backend/src/index.ts +42 -4
- package/templates/eject/docker-compose.custom.yml +8 -4
- package/templates/generated-schemas/blank.ts +40 -0
- package/templates/generated-schemas/blog.ts +127 -0
- package/templates/generated-schemas/ecommerce.ts +93 -0
- package/templates/overlays/baas/ai-instructions.md +1 -1
- package/templates/overlays/baas/config/package.json +1 -0
- package/templates/overlays/baas/config/storage.ts +5 -3
- package/templates/overlays/baas/package.json +1 -0
- package/templates/template/.cursorrules +1 -1
- package/templates/template/.env.example +20 -10
- package/templates/template/.github/copilot-instructions.md +1 -1
- package/templates/template/.mcp.json +2 -2
- package/templates/template/.windsurfrules +1 -1
- package/templates/template/AGENTS.md +1 -1
- package/templates/template/CLAUDE.md +1 -1
- package/templates/template/README.md +21 -15
- package/templates/template/ai-instructions.md +5 -4
- package/templates/template/backend/functions/hello.ts +8 -0
- package/templates/template/backend/package.json +1 -1
- package/templates/template/config/resources.ts +7 -6
- package/templates/template/config/storage.ts +5 -3
- package/templates/template/docker-compose.yml +15 -9
- package/templates/template/frontend/vite.config.ts +13 -3
- package/templates/template/gitignore +6 -1
- package/templates/template/package.json +5 -3
- package/templates/template/scripts/example.ts +6 -4
- package/templates/template/tsconfig.json +25 -0
- package/dist/branch-pointer-dBiClJ0p.js +0 -111
- package/dist/branch-pointer-dBiClJ0p.js.map +0 -1
- package/dist/constraints-DKGbNfUW.js +0 -48
- package/dist/constraints-DKGbNfUW.js.map +0 -1
- package/dist/daemon-DR9Tk0La.js +0 -355
- package/dist/daemon-DR9Tk0La.js.map +0 -1
- package/dist/daemon-entry-LTFKtpHy.js.map +0 -1
- package/dist/dev-preflight-CNLl4rdo.js +0 -367
- package/dist/dev-preflight-CNLl4rdo.js.map +0 -1
- package/dist/project-3WCbrrfW.js.map +0 -1
- package/dist/pull-Dywm5FWP.js +0 -232
- package/dist/pull-Dywm5FWP.js.map +0 -1
- package/dist/resolve-BAeRUBhg.js +0 -74
- package/dist/resolve-BAeRUBhg.js.map +0 -1
- package/dist/state-C59Elrnt.js.map +0 -1
- package/templates/template/backend/src/schema.generated.ts +0 -13
|
@@ -0,0 +1,1332 @@
|
|
|
1
|
+
import { n as getPMCommands } from "./package-manager-DSEOwTqM.js";
|
|
2
|
+
import { i as failAsJson, p as requireProjectRoot } from "./project-Wp2zoyeN.js";
|
|
3
|
+
import { n as parseCommandArgs, r as wantsHelp } from "./args-BnmsEXvu.js";
|
|
4
|
+
import { o as ManifestError, p as loadManifest } from "./manifest-DRp0UVTj.js";
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
import fs from "fs";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import { execFileSync } from "child_process";
|
|
9
|
+
import { execa } from "execa";
|
|
10
|
+
import { isDeepStrictEqual } from "util";
|
|
11
|
+
//#region src/upgrade.ts
|
|
12
|
+
/**
|
|
13
|
+
* Moving a project's `@rebasepro/*` pins to one release.
|
|
14
|
+
*
|
|
15
|
+
* Every `@rebasepro/*` package ships at one version, and a project declares them
|
|
16
|
+
* in several `package.json` files — the root, `backend/`, `frontend/`,
|
|
17
|
+
* `config/`, and whatever else the repository holds. Bumping them by hand means
|
|
18
|
+
* finding every one, keeping each range's `^` or `~`, and noticing the override
|
|
19
|
+
* that quietly wins over all of them. `rebase upgrade` is that job, done the same
|
|
20
|
+
* way every time; the control plane runs it too, when it rebuilds a managed
|
|
21
|
+
* project on a newer release.
|
|
22
|
+
*
|
|
23
|
+
* Everything here is file work — git is asked only which files are the
|
|
24
|
+
* project's — so it can be tested against a temporary directory: which files
|
|
25
|
+
* are read, which specs move, the rewrite itself, and what overrides say. The
|
|
26
|
+
* command in `commands/upgrade.ts` adds the registry lookup, the install and the
|
|
27
|
+
* printing.
|
|
28
|
+
*
|
|
29
|
+
* ## The rewrite is textual
|
|
30
|
+
*
|
|
31
|
+
* A `package.json` is authored. Parsing it and writing `JSON.stringify` back
|
|
32
|
+
* reformats the file, reorders nothing but reindents everything, and drops a
|
|
33
|
+
* trailing newline or a two-space indent somebody chose — a one-line version
|
|
34
|
+
* bump that arrives as a whole-file diff. So the file is scanned for the exact
|
|
35
|
+
* byte range of each value, only those bytes are replaced, and the result is
|
|
36
|
+
* parsed again and compared against what was intended. A file whose rewrite
|
|
37
|
+
* does not parse back to exactly the intended values is not written.
|
|
38
|
+
*/
|
|
39
|
+
/** The dependency blocks a pin is moved in. `peerDependencies` is not one. */
|
|
40
|
+
var PIN_FIELDS = [
|
|
41
|
+
"dependencies",
|
|
42
|
+
"devDependencies",
|
|
43
|
+
"optionalDependencies"
|
|
44
|
+
];
|
|
45
|
+
/** The package whose published versions stand for the release as a whole. */
|
|
46
|
+
var RELEASE_PACKAGE = "@rebasepro/cli";
|
|
47
|
+
/** `MAJOR.MINOR.PATCH`, with an optional prerelease and build, and nothing else. */
|
|
48
|
+
var SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
49
|
+
/** Whether `value` is one exact version — `0.21.0`, `0.21.1-canary.g8c5a265`. */
|
|
50
|
+
function isExactVersion(value) {
|
|
51
|
+
return SEMVER.test(value);
|
|
52
|
+
}
|
|
53
|
+
/** An error that carries the `--json` envelope's code and a remedy. */
|
|
54
|
+
var UpgradeError = class extends Error {
|
|
55
|
+
code;
|
|
56
|
+
hint;
|
|
57
|
+
constructor(message, code, hint) {
|
|
58
|
+
super(message);
|
|
59
|
+
this.code = code;
|
|
60
|
+
this.hint = hint;
|
|
61
|
+
this.name = "UpgradeError";
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* What a dependency spec is, as far as moving it goes.
|
|
66
|
+
*
|
|
67
|
+
* Only a plain version is moved, because only a plain version has an obvious
|
|
68
|
+
* answer: `^0.19.1` becomes `^0.21.0`. A `>=` floor, a `||` union or a partial
|
|
69
|
+
* `^0.19` could each be rewritten several ways, and a tag or a workspace link
|
|
70
|
+
* already means something other than "this release" — so each is reported with
|
|
71
|
+
* its reason and left as written.
|
|
72
|
+
*/
|
|
73
|
+
function classifySpec(spec) {
|
|
74
|
+
const movable = /^([\^~]?)(.+)$/.exec(spec);
|
|
75
|
+
if (movable && isExactVersion(movable[2])) return {
|
|
76
|
+
kind: "movable",
|
|
77
|
+
prefix: movable[1],
|
|
78
|
+
version: movable[2]
|
|
79
|
+
};
|
|
80
|
+
for (const protocol of [
|
|
81
|
+
"link:",
|
|
82
|
+
"file:",
|
|
83
|
+
"portal:"
|
|
84
|
+
]) if (spec.startsWith(protocol)) return {
|
|
85
|
+
kind: "local",
|
|
86
|
+
protocol
|
|
87
|
+
};
|
|
88
|
+
const catalog = /^catalog:(.*)$/.exec(spec.trim());
|
|
89
|
+
if (catalog) return {
|
|
90
|
+
kind: "catalog",
|
|
91
|
+
catalog: catalog[1].trim() || "default"
|
|
92
|
+
};
|
|
93
|
+
return {
|
|
94
|
+
kind: "other",
|
|
95
|
+
reason: otherReason(spec)
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/** Why a spec that does not move is left alone, as the report says it. */
|
|
99
|
+
function leftAloneReason(cls) {
|
|
100
|
+
if (cls.kind === "local") return `a local ${cls.protocol} path, which points at a checkout on this machine rather than a release`;
|
|
101
|
+
if (cls.kind === "catalog") return "a catalog: reference inside a catalog, which pnpm does not resolve";
|
|
102
|
+
return cls.reason;
|
|
103
|
+
}
|
|
104
|
+
function otherReason(spec) {
|
|
105
|
+
const trimmed = spec.trim();
|
|
106
|
+
if (trimmed.startsWith("workspace:")) return "a workspace: link, resolved inside this repository rather than from the registry";
|
|
107
|
+
if (trimmed.startsWith("npm:")) return "an npm: alias; move the aliased version by hand";
|
|
108
|
+
if (/^(git\+|git:|github:|gitlab:|bitbucket:|git@)/.test(trimmed) || /^https?:\/\//.test(trimmed)) return "a git or URL dependency, not a registry version";
|
|
109
|
+
if (trimmed === "" || trimmed === "*" || /^[xX]$/.test(trimmed)) return "a wildcard, which already takes whatever the registry has";
|
|
110
|
+
if (trimmed.includes("||")) return "a union of ranges, with no single version to move";
|
|
111
|
+
if (/^[<>=]/.test(trimmed) || /\s-\s/.test(trimmed)) return "a comparator range, with no single version to move";
|
|
112
|
+
if (/^[\^~]?v?\d/.test(trimmed)) return "not a plain MAJOR.MINOR.PATCH version";
|
|
113
|
+
return "a dist-tag, not a version";
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The `@rebasepro/*` package an override key targets, or null.
|
|
117
|
+
*
|
|
118
|
+
* pnpm and npm key an override by name, optionally narrowed: `@rebasepro/x@<1`,
|
|
119
|
+
* `parent>@rebasepro/x`; yarn's `resolutions` by a path, `**\/@rebasepro/x`. The
|
|
120
|
+
* target is the last segment either way.
|
|
121
|
+
*/
|
|
122
|
+
function overrideTarget(key) {
|
|
123
|
+
const match = /(?:^|[/>])(@rebasepro\/[^/@>\s]+)(?:@[^/>]*)?$/.exec(key);
|
|
124
|
+
return match ? match[1] : null;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Directories the walk never enters: installed packages, build output (`dist`,
|
|
128
|
+
* `dist-bundle`, `dist-bundle-admin`, …) and anything hidden (`.git`,
|
|
129
|
+
* `.rebase`, an editor's worktrees). None of them hold a manifest anybody
|
|
130
|
+
* authored for this project, and `dist-bundle/package.json` in particular is a
|
|
131
|
+
* generated copy that `rebase build` rewrites anyway. They are skipped whether
|
|
132
|
+
* or not git ignores them.
|
|
133
|
+
*/
|
|
134
|
+
function skipDirectory(name) {
|
|
135
|
+
return name === "node_modules" || name.startsWith("dist") || name.startsWith(".");
|
|
136
|
+
}
|
|
137
|
+
var runGit = (cwd, args) => execFileSync("git", [
|
|
138
|
+
"-C",
|
|
139
|
+
cwd,
|
|
140
|
+
...args
|
|
141
|
+
], {
|
|
142
|
+
encoding: "utf8",
|
|
143
|
+
stdio: [
|
|
144
|
+
"ignore",
|
|
145
|
+
"pipe",
|
|
146
|
+
"ignore"
|
|
147
|
+
],
|
|
148
|
+
maxBuffer: 256 * 1024 * 1024
|
|
149
|
+
});
|
|
150
|
+
/**
|
|
151
|
+
* The files git counts as part of the project — tracked, or untracked and not
|
|
152
|
+
* ignored — relative to `projectRoot`, POSIX. Null when git has no say: no git,
|
|
153
|
+
* no repository, or a repository that lists nothing under the project root.
|
|
154
|
+
*
|
|
155
|
+
* Listing nothing means the repository ignores the project as a whole: a copy
|
|
156
|
+
* of it under a gitignored scratch directory, say. Running the upgrade in that
|
|
157
|
+
* copy is asking for the copy to move, and the repository's rules would leave
|
|
158
|
+
* nothing in it to move. Asked with `git ls-files` rather than
|
|
159
|
+
* `git check-ignore`, which answers differently across git versions for a
|
|
160
|
+
* directory and for a negated pattern.
|
|
161
|
+
*/
|
|
162
|
+
function gitProjectFiles(projectRoot, git) {
|
|
163
|
+
let listed;
|
|
164
|
+
try {
|
|
165
|
+
listed = git(projectRoot, [
|
|
166
|
+
"ls-files",
|
|
167
|
+
"-z",
|
|
168
|
+
"--cached",
|
|
169
|
+
"--others",
|
|
170
|
+
"--exclude-standard"
|
|
171
|
+
]);
|
|
172
|
+
} catch {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
const files = new Set(listed.split("\0").filter(Boolean));
|
|
176
|
+
return files.size > 0 ? files : null;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Every `package.json` and `pnpm-workspace.yaml` under the project root that
|
|
180
|
+
* belongs to the project.
|
|
181
|
+
*
|
|
182
|
+
* In a git repository that means what git lists: a gitignored build context,
|
|
183
|
+
* scratch copy or vendored package is not the project's, and rewriting it
|
|
184
|
+
* would change files nobody committed. The walk enters only directories that
|
|
185
|
+
* hold a listed file, so an ignored tree is not read at all. Outside a
|
|
186
|
+
* repository every directory is walked. {@link skipDirectory} applies either
|
|
187
|
+
* way.
|
|
188
|
+
*/
|
|
189
|
+
function discoverProjectFiles(projectRoot, git = runGit) {
|
|
190
|
+
const found = {
|
|
191
|
+
packageJsons: [],
|
|
192
|
+
workspaceYamls: []
|
|
193
|
+
};
|
|
194
|
+
const listed = gitProjectFiles(projectRoot, git);
|
|
195
|
+
/** Every directory a listed file sits in, at any depth. */
|
|
196
|
+
const listedDirectories = /* @__PURE__ */ new Set();
|
|
197
|
+
for (const file of listed ?? []) for (let dir = path.posix.dirname(file); dir !== "." && !listedDirectories.has(dir); dir = path.posix.dirname(dir)) listedDirectories.add(dir);
|
|
198
|
+
const walk = (dir, relativeDir) => {
|
|
199
|
+
let entries;
|
|
200
|
+
try {
|
|
201
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
202
|
+
} catch {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
206
|
+
for (const entry of entries) {
|
|
207
|
+
const full = path.join(dir, entry.name);
|
|
208
|
+
const relative = relativeDir ? `${relativeDir}/${entry.name}` : entry.name;
|
|
209
|
+
if (entry.isDirectory()) {
|
|
210
|
+
if (!skipDirectory(entry.name) && (!listed || listedDirectories.has(relative))) walk(full, relative);
|
|
211
|
+
} else if (entry.isFile() && (!listed || listed.has(relative))) {
|
|
212
|
+
if (entry.name === "package.json") found.packageJsons.push(full);
|
|
213
|
+
else if (entry.name === "pnpm-workspace.yaml") found.workspaceYamls.push(full);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
walk(projectRoot, "");
|
|
218
|
+
return found;
|
|
219
|
+
}
|
|
220
|
+
function catalogKey(catalog, pkg) {
|
|
221
|
+
return `${catalog}\u0000${pkg}`;
|
|
222
|
+
}
|
|
223
|
+
/** Project-relative, POSIX. What every file path in the output looks like. */
|
|
224
|
+
function relativeTo(projectRoot, absolute) {
|
|
225
|
+
return path.relative(projectRoot, absolute).split(path.sep).join("/");
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Work out every change the upgrade makes, without writing anything.
|
|
229
|
+
*
|
|
230
|
+
* `--dry-run` prints this; a real run writes `writes` and nothing else.
|
|
231
|
+
*/
|
|
232
|
+
function planUpgrade(projectRoot, target, options = {}) {
|
|
233
|
+
if (!isExactVersion(target)) throw new UpgradeError(`"${target}" is not an exact version.`, "target_invalid");
|
|
234
|
+
const plan = {
|
|
235
|
+
target,
|
|
236
|
+
changed: [],
|
|
237
|
+
skipped: [],
|
|
238
|
+
overrides: [],
|
|
239
|
+
unreadable: [],
|
|
240
|
+
writes: [],
|
|
241
|
+
unchanged: 0
|
|
242
|
+
};
|
|
243
|
+
const ledger = {
|
|
244
|
+
refs: [],
|
|
245
|
+
defined: /* @__PURE__ */ new Set()
|
|
246
|
+
};
|
|
247
|
+
const files = discoverProjectFiles(projectRoot);
|
|
248
|
+
for (const absolute of files.packageJsons) {
|
|
249
|
+
const file = relativeTo(projectRoot, absolute);
|
|
250
|
+
const original = fs.readFileSync(absolute, "utf8");
|
|
251
|
+
const content = planPackageJson(file, original, target, options, plan, ledger);
|
|
252
|
+
if (content !== null && content !== original) plan.writes.push({
|
|
253
|
+
file,
|
|
254
|
+
absolute,
|
|
255
|
+
content
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
for (const absolute of files.workspaceYamls) {
|
|
259
|
+
const file = relativeTo(projectRoot, absolute);
|
|
260
|
+
const original = fs.readFileSync(absolute, "utf8");
|
|
261
|
+
const content = planWorkspaceYaml(file, original, target, options, plan, ledger);
|
|
262
|
+
if (content !== original) plan.writes.push({
|
|
263
|
+
file,
|
|
264
|
+
absolute,
|
|
265
|
+
content
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
for (const ref of ledger.refs) {
|
|
269
|
+
if (ledger.defined.has(catalogKey(ref.catalog, ref.pkg))) continue;
|
|
270
|
+
plan.skipped.push({
|
|
271
|
+
file: ref.file,
|
|
272
|
+
name: ref.name,
|
|
273
|
+
field: ref.field,
|
|
274
|
+
spec: ref.spec,
|
|
275
|
+
reason: `a pnpm catalog: reference, and no catalog under this project defines it — move the version where the "${ref.catalog}" catalog is declared`
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
return plan;
|
|
279
|
+
}
|
|
280
|
+
/** Write what the plan changes. Nothing else on disk is touched. */
|
|
281
|
+
function applyUpgradePlan(plan) {
|
|
282
|
+
for (const write of plan.writes) fs.writeFileSync(write.absolute, write.content, "utf8");
|
|
283
|
+
}
|
|
284
|
+
/** Override blocks in a `package.json`, as paths from the root object. */
|
|
285
|
+
var JSON_OVERRIDE_BLOCKS = [
|
|
286
|
+
["pnpm", "overrides"],
|
|
287
|
+
["overrides"],
|
|
288
|
+
["resolutions"]
|
|
289
|
+
];
|
|
290
|
+
function planPackageJson(file, original, target, options, plan, ledger) {
|
|
291
|
+
const bom = original.startsWith("") ? "" : "";
|
|
292
|
+
const text = original.slice(bom.length);
|
|
293
|
+
let parsed;
|
|
294
|
+
let tree;
|
|
295
|
+
try {
|
|
296
|
+
parsed = JSON.parse(text);
|
|
297
|
+
tree = scanJson(text);
|
|
298
|
+
} catch (err) {
|
|
299
|
+
plan.unreadable.push({
|
|
300
|
+
file,
|
|
301
|
+
reason: `not valid JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
302
|
+
});
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
if (!isRecord(parsed) || tree.kind !== "object") {
|
|
306
|
+
plan.unreadable.push({
|
|
307
|
+
file,
|
|
308
|
+
reason: "not a JSON object"
|
|
309
|
+
});
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
const expected = structuredClone(parsed);
|
|
313
|
+
const edits = [];
|
|
314
|
+
/** Override members to remove, as [block path, key]. */
|
|
315
|
+
const removals = [];
|
|
316
|
+
for (const field of PIN_FIELDS) {
|
|
317
|
+
const block = memberValue(tree, [field]);
|
|
318
|
+
if (block?.kind !== "object") continue;
|
|
319
|
+
for (const member of block.members) {
|
|
320
|
+
if (!member.key.startsWith("@rebasepro/")) continue;
|
|
321
|
+
if (member.value.kind !== "string") {
|
|
322
|
+
plan.skipped.push({
|
|
323
|
+
file,
|
|
324
|
+
name: member.key,
|
|
325
|
+
field,
|
|
326
|
+
spec: text.slice(member.value.start, member.value.end),
|
|
327
|
+
reason: "not a string"
|
|
328
|
+
});
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
const spec = member.value.value;
|
|
332
|
+
const cls = classifySpec(spec);
|
|
333
|
+
if (cls.kind === "movable") {
|
|
334
|
+
const to = `${cls.prefix}${target}`;
|
|
335
|
+
if (to === spec) {
|
|
336
|
+
plan.unchanged++;
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
edits.push({
|
|
340
|
+
start: member.value.start,
|
|
341
|
+
end: member.value.end,
|
|
342
|
+
replacement: JSON.stringify(to)
|
|
343
|
+
});
|
|
344
|
+
setPath(expected, [field, member.key], to);
|
|
345
|
+
plan.changed.push({
|
|
346
|
+
file,
|
|
347
|
+
name: member.key,
|
|
348
|
+
field,
|
|
349
|
+
from: spec,
|
|
350
|
+
to
|
|
351
|
+
});
|
|
352
|
+
} else if (cls.kind === "catalog") ledger.refs.push({
|
|
353
|
+
file,
|
|
354
|
+
name: member.key,
|
|
355
|
+
field,
|
|
356
|
+
spec,
|
|
357
|
+
catalog: cls.catalog,
|
|
358
|
+
pkg: member.key
|
|
359
|
+
});
|
|
360
|
+
else plan.skipped.push({
|
|
361
|
+
file,
|
|
362
|
+
name: member.key,
|
|
363
|
+
field,
|
|
364
|
+
spec,
|
|
365
|
+
reason: leftAloneReason(cls)
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
for (const blockPath of JSON_OVERRIDE_BLOCKS) {
|
|
370
|
+
const block = memberValue(tree, blockPath);
|
|
371
|
+
if (block?.kind !== "object") continue;
|
|
372
|
+
const field = blockPath.join(".");
|
|
373
|
+
for (const member of block.members) {
|
|
374
|
+
if (!overrideTarget(member.key)) continue;
|
|
375
|
+
if (member.value.kind !== "string") {
|
|
376
|
+
plan.skipped.push({
|
|
377
|
+
file,
|
|
378
|
+
name: member.key,
|
|
379
|
+
field,
|
|
380
|
+
spec: text.slice(member.value.start, member.value.end),
|
|
381
|
+
reason: "a nested override, which has no single version to move; edit it by hand"
|
|
382
|
+
});
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
const spec = member.value.value;
|
|
386
|
+
const cls = classifySpec(spec);
|
|
387
|
+
const pkg = overrideTarget(member.key);
|
|
388
|
+
if (cls.kind === "movable") {
|
|
389
|
+
const to = `${cls.prefix}${target}`;
|
|
390
|
+
if (to === spec) {
|
|
391
|
+
plan.unchanged++;
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
edits.push({
|
|
395
|
+
start: member.value.start,
|
|
396
|
+
end: member.value.end,
|
|
397
|
+
replacement: JSON.stringify(to)
|
|
398
|
+
});
|
|
399
|
+
setPath(expected, [...blockPath, member.key], to);
|
|
400
|
+
plan.overrides.push({
|
|
401
|
+
file,
|
|
402
|
+
name: member.key,
|
|
403
|
+
spec,
|
|
404
|
+
action: "bumped",
|
|
405
|
+
to
|
|
406
|
+
});
|
|
407
|
+
} else if (cls.kind === "catalog" && pkg) ledger.refs.push({
|
|
408
|
+
file,
|
|
409
|
+
name: member.key,
|
|
410
|
+
field,
|
|
411
|
+
spec,
|
|
412
|
+
catalog: cls.catalog,
|
|
413
|
+
pkg
|
|
414
|
+
});
|
|
415
|
+
else if (cls.kind === "local") if (options.dropLocalOverrides) {
|
|
416
|
+
removals.push([blockPath, member.key]);
|
|
417
|
+
deletePath(expected, [...blockPath, member.key]);
|
|
418
|
+
plan.overrides.push({
|
|
419
|
+
file,
|
|
420
|
+
name: member.key,
|
|
421
|
+
spec,
|
|
422
|
+
action: "removed-local"
|
|
423
|
+
});
|
|
424
|
+
} else plan.overrides.push({
|
|
425
|
+
file,
|
|
426
|
+
name: member.key,
|
|
427
|
+
spec,
|
|
428
|
+
action: "kept-local"
|
|
429
|
+
});
|
|
430
|
+
else plan.skipped.push({
|
|
431
|
+
file,
|
|
432
|
+
name: member.key,
|
|
433
|
+
field,
|
|
434
|
+
spec,
|
|
435
|
+
reason: leftAloneReason(cls)
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (edits.length === 0 && removals.length === 0) return original;
|
|
440
|
+
let next = applyEdits(text, edits);
|
|
441
|
+
for (const [blockPath, key] of removals) next = removeJsonMember(next, blockPath, key);
|
|
442
|
+
for (const blockPath of JSON_OVERRIDE_BLOCKS) {
|
|
443
|
+
if (!removals.some(([removedFrom]) => removedFrom === blockPath)) continue;
|
|
444
|
+
for (let depth = blockPath.length; depth > 0; depth--) {
|
|
445
|
+
const containerPath = blockPath.slice(0, depth);
|
|
446
|
+
const node = memberValue(scanJson(next), containerPath);
|
|
447
|
+
if (node?.kind !== "object" || node.members.length > 0) break;
|
|
448
|
+
next = removeJsonMember(next, containerPath.slice(0, -1), containerPath[containerPath.length - 1]);
|
|
449
|
+
deletePath(expected, containerPath);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
let reparsed;
|
|
453
|
+
try {
|
|
454
|
+
reparsed = JSON.parse(next);
|
|
455
|
+
} catch {
|
|
456
|
+
reparsed = void 0;
|
|
457
|
+
}
|
|
458
|
+
if (!isDeepStrictEqual(reparsed, expected)) throw new UpgradeError(`Could not rewrite ${file} without disturbing it; nothing was written.`, "rewrite_failed", "Move its @rebasepro versions by hand, then run `rebase upgrade` again.");
|
|
459
|
+
return bom + next;
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* The structure of a JSON document, with where each value starts and ends.
|
|
463
|
+
*
|
|
464
|
+
* `JSON.parse` answers what a file says; this answers where it says it, which is
|
|
465
|
+
* what a rewrite that leaves every other byte alone needs. Throws on anything
|
|
466
|
+
* that is not JSON — callers have already parsed the same text, so that never
|
|
467
|
+
* happens in practice.
|
|
468
|
+
*/
|
|
469
|
+
function scanJson(text) {
|
|
470
|
+
let i = 0;
|
|
471
|
+
const WHITESPACE = " \n\r";
|
|
472
|
+
const skipWhitespace = () => {
|
|
473
|
+
while (i < text.length && WHITESPACE.includes(text[i])) i++;
|
|
474
|
+
};
|
|
475
|
+
const unexpected = () => {
|
|
476
|
+
throw new Error(`unexpected ${i < text.length ? `"${text[i]}"` : "end of input"} at offset ${i}`);
|
|
477
|
+
};
|
|
478
|
+
const readString = () => {
|
|
479
|
+
const start = i;
|
|
480
|
+
if (text[i] !== "\"") unexpected();
|
|
481
|
+
i++;
|
|
482
|
+
while (i < text.length && text[i] !== "\"") i += text[i] === "\\" ? 2 : 1;
|
|
483
|
+
if (i >= text.length) unexpected();
|
|
484
|
+
i++;
|
|
485
|
+
return {
|
|
486
|
+
kind: "string",
|
|
487
|
+
start,
|
|
488
|
+
end: i,
|
|
489
|
+
value: JSON.parse(text.slice(start, i))
|
|
490
|
+
};
|
|
491
|
+
};
|
|
492
|
+
const readValue = () => {
|
|
493
|
+
skipWhitespace();
|
|
494
|
+
const start = i;
|
|
495
|
+
if (text[i] === "{") {
|
|
496
|
+
i++;
|
|
497
|
+
const members = [];
|
|
498
|
+
skipWhitespace();
|
|
499
|
+
if (text[i] === "}") {
|
|
500
|
+
i++;
|
|
501
|
+
return {
|
|
502
|
+
kind: "object",
|
|
503
|
+
start,
|
|
504
|
+
end: i,
|
|
505
|
+
members
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
for (;;) {
|
|
509
|
+
skipWhitespace();
|
|
510
|
+
const key = readString();
|
|
511
|
+
skipWhitespace();
|
|
512
|
+
if (text[i] !== ":") unexpected();
|
|
513
|
+
i++;
|
|
514
|
+
members.push({
|
|
515
|
+
key: key.value,
|
|
516
|
+
keyStart: key.start,
|
|
517
|
+
value: readValue()
|
|
518
|
+
});
|
|
519
|
+
skipWhitespace();
|
|
520
|
+
if (text[i] === ",") {
|
|
521
|
+
i++;
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
if (text[i] === "}") {
|
|
525
|
+
i++;
|
|
526
|
+
return {
|
|
527
|
+
kind: "object",
|
|
528
|
+
start,
|
|
529
|
+
end: i,
|
|
530
|
+
members
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
unexpected();
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
if (text[i] === "[") {
|
|
537
|
+
i++;
|
|
538
|
+
skipWhitespace();
|
|
539
|
+
if (text[i] === "]") {
|
|
540
|
+
i++;
|
|
541
|
+
return {
|
|
542
|
+
kind: "array",
|
|
543
|
+
start,
|
|
544
|
+
end: i
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
for (;;) {
|
|
548
|
+
readValue();
|
|
549
|
+
skipWhitespace();
|
|
550
|
+
if (text[i] === ",") {
|
|
551
|
+
i++;
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
if (text[i] === "]") {
|
|
555
|
+
i++;
|
|
556
|
+
return {
|
|
557
|
+
kind: "array",
|
|
558
|
+
start,
|
|
559
|
+
end: i
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
unexpected();
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
if (text[i] === "\"") return readString();
|
|
566
|
+
while (i < text.length && !",:{}[]\"".includes(text[i]) && !WHITESPACE.includes(text[i])) i++;
|
|
567
|
+
if (i === start) unexpected();
|
|
568
|
+
return {
|
|
569
|
+
kind: "scalar",
|
|
570
|
+
start,
|
|
571
|
+
end: i
|
|
572
|
+
};
|
|
573
|
+
};
|
|
574
|
+
const root = readValue();
|
|
575
|
+
skipWhitespace();
|
|
576
|
+
if (i !== text.length) unexpected();
|
|
577
|
+
return root;
|
|
578
|
+
}
|
|
579
|
+
/** The value at a path of object keys, or undefined. The last duplicate wins, as in `JSON.parse`. */
|
|
580
|
+
function memberValue(root, keys) {
|
|
581
|
+
let node = root;
|
|
582
|
+
for (const key of keys) {
|
|
583
|
+
if (node?.kind !== "object") return void 0;
|
|
584
|
+
const members = node.members.filter((m) => m.key === key);
|
|
585
|
+
node = members[members.length - 1]?.value;
|
|
586
|
+
}
|
|
587
|
+
return node;
|
|
588
|
+
}
|
|
589
|
+
/** Apply non-overlapping edits, last first so the earlier offsets stay valid. */
|
|
590
|
+
function applyEdits(text, edits) {
|
|
591
|
+
let out = text;
|
|
592
|
+
for (const edit of [...edits].sort((a, b) => b.start - a.start)) out = out.slice(0, edit.start) + edit.replacement + out.slice(edit.end);
|
|
593
|
+
return out;
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Remove one member from the object at `objectPath`, taking its comma with it.
|
|
597
|
+
*
|
|
598
|
+
* Rescans rather than trusting offsets from before an earlier edit. A middle or
|
|
599
|
+
* first member takes everything up to the next key, so the next member inherits
|
|
600
|
+
* its indentation; the last takes the comma and whitespace after the previous
|
|
601
|
+
* value, so the closing brace keeps its line; an only member leaves `{}`.
|
|
602
|
+
*/
|
|
603
|
+
function removeJsonMember(text, objectPath, key) {
|
|
604
|
+
const container = memberValue(scanJson(text), objectPath);
|
|
605
|
+
if (container?.kind !== "object") return text;
|
|
606
|
+
const index = container.members.findIndex((m) => m.key === key);
|
|
607
|
+
if (index === -1) return text;
|
|
608
|
+
const members = container.members;
|
|
609
|
+
const member = members[index];
|
|
610
|
+
if (members.length === 1) return text.slice(0, container.start + 1) + text.slice(container.end - 1);
|
|
611
|
+
if (index < members.length - 1) return text.slice(0, member.keyStart) + text.slice(members[index + 1].keyStart);
|
|
612
|
+
return text.slice(0, members[index - 1].value.end) + text.slice(member.value.end);
|
|
613
|
+
}
|
|
614
|
+
function isRecord(value) {
|
|
615
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
616
|
+
}
|
|
617
|
+
function setPath(root, keys, value) {
|
|
618
|
+
let node = root;
|
|
619
|
+
for (const key of keys.slice(0, -1)) {
|
|
620
|
+
const next = node[key];
|
|
621
|
+
if (!isRecord(next)) return;
|
|
622
|
+
node = next;
|
|
623
|
+
}
|
|
624
|
+
node[keys[keys.length - 1]] = value;
|
|
625
|
+
}
|
|
626
|
+
function deletePath(root, keys) {
|
|
627
|
+
let node = root;
|
|
628
|
+
for (const key of keys.slice(0, -1)) {
|
|
629
|
+
const next = node[key];
|
|
630
|
+
if (!isRecord(next)) return;
|
|
631
|
+
node = next;
|
|
632
|
+
}
|
|
633
|
+
delete node[keys[keys.length - 1]];
|
|
634
|
+
}
|
|
635
|
+
/** Lines, each without its `\r`, and whether the file used them. */
|
|
636
|
+
function splitLines(text) {
|
|
637
|
+
return text.split("\n").map((line) => line.endsWith("\r") ? line.slice(0, -1) : line);
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* A YAML scalar at the start of `rest`: its text, and where that text sits.
|
|
641
|
+
*
|
|
642
|
+
* Double- and single-quoted and plain scalars, which is every shape an override
|
|
643
|
+
* value is written in. A double-quoted one with an escape in it is refused
|
|
644
|
+
* rather than decoded: no version or path needs one, and getting the decoding
|
|
645
|
+
* wrong would rewrite a value this does not understand.
|
|
646
|
+
*/
|
|
647
|
+
function readYamlScalar(rest) {
|
|
648
|
+
if (rest.startsWith("\"")) {
|
|
649
|
+
const close = rest.indexOf("\"", 1);
|
|
650
|
+
if (close === -1 || rest.slice(1, close).includes("\\")) return null;
|
|
651
|
+
if (!/^\s*(#.*)?$/.test(rest.slice(close + 1))) return null;
|
|
652
|
+
return {
|
|
653
|
+
value: rest.slice(1, close),
|
|
654
|
+
start: 1,
|
|
655
|
+
end: close
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
if (rest.startsWith("'")) {
|
|
659
|
+
const close = rest.indexOf("'", 1);
|
|
660
|
+
if (close === -1 || rest[close + 1] === "'") return null;
|
|
661
|
+
if (!/^\s*(#.*)?$/.test(rest.slice(close + 1))) return null;
|
|
662
|
+
return {
|
|
663
|
+
value: rest.slice(1, close),
|
|
664
|
+
start: 1,
|
|
665
|
+
end: close
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
const comment = rest.search(/\s#/);
|
|
669
|
+
const plain = (comment === -1 ? rest : rest.slice(0, comment)).trimEnd();
|
|
670
|
+
if (plain === "" || /^[[{&*!|>%@`]/.test(plain)) return null;
|
|
671
|
+
return {
|
|
672
|
+
value: plain,
|
|
673
|
+
start: 0,
|
|
674
|
+
end: plain.length
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* A top-level block mapping of a `pnpm-workspace.yaml` — `overrides:`,
|
|
679
|
+
* `catalog:` — read line by line.
|
|
680
|
+
*
|
|
681
|
+
* Deliberately narrow. The file is YAML, and the CLI carries no YAML parser; the
|
|
682
|
+
* shape pnpm documents and every project writes is a block mapping of one
|
|
683
|
+
* scalar per line, which a line reader handles exactly. Anything else in the
|
|
684
|
+
* block is counted rather than guessed at, and an inline `overrides: { … }` is
|
|
685
|
+
* reported as not handled.
|
|
686
|
+
*/
|
|
687
|
+
function readYamlBlock(lines, key) {
|
|
688
|
+
const keyLine = lines.findIndex((line) => new RegExp(`^${key}:\\s*(#.*)?$`).test(line));
|
|
689
|
+
if (keyLine === -1) return null;
|
|
690
|
+
const block = {
|
|
691
|
+
keyLine,
|
|
692
|
+
lines: [],
|
|
693
|
+
entries: [],
|
|
694
|
+
otherContent: 0
|
|
695
|
+
};
|
|
696
|
+
for (let index = keyLine + 1; index < lines.length; index++) {
|
|
697
|
+
const line = lines[index];
|
|
698
|
+
if (line.trim() !== "" && !/^\s/.test(line) && !line.startsWith("#")) break;
|
|
699
|
+
block.lines.push(index);
|
|
700
|
+
if (line.trim() === "" || /^\s*#/.test(line)) continue;
|
|
701
|
+
const entry = readYamlEntry(line, index);
|
|
702
|
+
if (entry) block.entries.push(entry);
|
|
703
|
+
else block.otherContent++;
|
|
704
|
+
}
|
|
705
|
+
return block;
|
|
706
|
+
}
|
|
707
|
+
/** One `key: scalar` line of a block mapping, or null when it is anything else. */
|
|
708
|
+
function readYamlEntry(line, index) {
|
|
709
|
+
const entry = /^(\s+)("[^"]*"|'[^']*'|[^\s"'#][^:#]*?)\s*:(\s+|$)/.exec(line);
|
|
710
|
+
const valueOffset = entry ? entry[0].length : -1;
|
|
711
|
+
const scalar = entry ? readYamlScalar(line.slice(valueOffset)) : null;
|
|
712
|
+
if (!entry || !scalar) return null;
|
|
713
|
+
const rawKey = entry[2];
|
|
714
|
+
return {
|
|
715
|
+
line: index,
|
|
716
|
+
key: /^["']/.test(rawKey) ? rawKey.slice(1, -1) : rawKey,
|
|
717
|
+
value: scalar.value,
|
|
718
|
+
valueStart: valueOffset + scalar.start,
|
|
719
|
+
valueEnd: valueOffset + scalar.end
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Every catalog a `pnpm-workspace.yaml` declares: the top-level `catalog:`
|
|
724
|
+
* block, and each named block under `catalogs:`.
|
|
725
|
+
*
|
|
726
|
+
* `catalogs:` is one level deeper — a header line per catalog, its entries
|
|
727
|
+
* indented under it — and is read with the same one-scalar-per-line rule.
|
|
728
|
+
*/
|
|
729
|
+
function readYamlCatalogs(lines) {
|
|
730
|
+
const catalogs = [];
|
|
731
|
+
const single = readYamlBlock(lines, "catalog");
|
|
732
|
+
if (single) catalogs.push({
|
|
733
|
+
name: "default",
|
|
734
|
+
field: "catalog",
|
|
735
|
+
entries: single.entries
|
|
736
|
+
});
|
|
737
|
+
const keyLine = lines.findIndex((line) => /^catalogs:\s*(#.*)?$/.test(line));
|
|
738
|
+
if (keyLine === -1) return catalogs;
|
|
739
|
+
let current = null;
|
|
740
|
+
let headerIndent = -1;
|
|
741
|
+
for (let index = keyLine + 1; index < lines.length; index++) {
|
|
742
|
+
const line = lines[index];
|
|
743
|
+
if (line.trim() !== "" && !/^\s/.test(line) && !line.startsWith("#")) break;
|
|
744
|
+
if (line.trim() === "" || /^\s*#/.test(line)) continue;
|
|
745
|
+
const indent = line.length - line.trimStart().length;
|
|
746
|
+
const header = /^\s+("[^"]*"|'[^']*'|[^\s"'#][^:#]*?)\s*:\s*(#.*)?$/.exec(line);
|
|
747
|
+
if (header && (headerIndent === -1 || indent === headerIndent)) {
|
|
748
|
+
headerIndent = indent;
|
|
749
|
+
const name = /^["']/.test(header[1]) ? header[1].slice(1, -1) : header[1];
|
|
750
|
+
current = {
|
|
751
|
+
name,
|
|
752
|
+
field: `catalogs.${name}`,
|
|
753
|
+
entries: []
|
|
754
|
+
};
|
|
755
|
+
catalogs.push(current);
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
758
|
+
if (!current || indent <= headerIndent) continue;
|
|
759
|
+
const entry = readYamlEntry(line, index);
|
|
760
|
+
if (entry) current.entries.push(entry);
|
|
761
|
+
}
|
|
762
|
+
return catalogs;
|
|
763
|
+
}
|
|
764
|
+
/** A `pnpm-workspace.yaml`, with its overrides and then its catalogs moved. */
|
|
765
|
+
function planWorkspaceYaml(file, original, target, options, plan, ledger) {
|
|
766
|
+
return planYamlCatalogs(file, planYamlOverrides(file, original, target, options, plan, ledger), target, plan, ledger);
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Move the framework entries of every catalog. Entries are rewritten in place
|
|
770
|
+
* — a catalog loses no line — and the result is read back and held to what was
|
|
771
|
+
* meant, like every other rewrite here.
|
|
772
|
+
*/
|
|
773
|
+
function planYamlCatalogs(file, original, target, plan, ledger) {
|
|
774
|
+
const lines = splitLines(original);
|
|
775
|
+
const catalogs = readYamlCatalogs(lines);
|
|
776
|
+
for (const key of ["catalog", "catalogs"]) {
|
|
777
|
+
const inline = lines.find((line) => new RegExp(`^${key}:\\s*[^\\s#]`).test(line));
|
|
778
|
+
if (inline && inline.includes("@rebasepro/")) plan.skipped.push({
|
|
779
|
+
file,
|
|
780
|
+
name: key,
|
|
781
|
+
field: key,
|
|
782
|
+
spec: inline.slice(key.length + 1).trim(),
|
|
783
|
+
reason: `an inline ${key} map, which this does not rewrite; write it as a block, or edit it by hand`
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
if (catalogs.length === 0) return original;
|
|
787
|
+
const next = [...lines];
|
|
788
|
+
const expected = [];
|
|
789
|
+
for (const catalog of catalogs) for (const entry of catalog.entries) {
|
|
790
|
+
const meant = [
|
|
791
|
+
catalog.field,
|
|
792
|
+
entry.key,
|
|
793
|
+
entry.value
|
|
794
|
+
];
|
|
795
|
+
expected.push(meant);
|
|
796
|
+
if (!entry.key.startsWith("@rebasepro/")) continue;
|
|
797
|
+
ledger.defined.add(catalogKey(catalog.name, entry.key));
|
|
798
|
+
const cls = classifySpec(entry.value);
|
|
799
|
+
if (cls.kind !== "movable") {
|
|
800
|
+
plan.skipped.push({
|
|
801
|
+
file,
|
|
802
|
+
name: entry.key,
|
|
803
|
+
field: catalog.field,
|
|
804
|
+
spec: entry.value,
|
|
805
|
+
reason: leftAloneReason(cls)
|
|
806
|
+
});
|
|
807
|
+
continue;
|
|
808
|
+
}
|
|
809
|
+
const to = `${cls.prefix}${target}`;
|
|
810
|
+
if (to === entry.value) {
|
|
811
|
+
plan.unchanged++;
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
814
|
+
meant[2] = to;
|
|
815
|
+
const line = next[entry.line];
|
|
816
|
+
next[entry.line] = line.slice(0, entry.valueStart) + to + line.slice(entry.valueEnd);
|
|
817
|
+
plan.changed.push({
|
|
818
|
+
file,
|
|
819
|
+
name: entry.key,
|
|
820
|
+
field: catalog.field,
|
|
821
|
+
from: entry.value,
|
|
822
|
+
to
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
const content = next.join(original.includes("\r\n") ? "\r\n" : "\n");
|
|
826
|
+
if (!isDeepStrictEqual(readYamlCatalogs(splitLines(content)).flatMap((catalog) => catalog.entries.map((e) => [
|
|
827
|
+
catalog.field,
|
|
828
|
+
e.key,
|
|
829
|
+
e.value
|
|
830
|
+
])), expected)) throw new UpgradeError(`Could not rewrite ${file} without disturbing it; nothing was written.`, "rewrite_failed", "Move its @rebasepro catalog entries by hand, then run `rebase upgrade` again.");
|
|
831
|
+
return content;
|
|
832
|
+
}
|
|
833
|
+
function planYamlOverrides(file, original, target, options, plan, ledger) {
|
|
834
|
+
const lines = splitLines(original);
|
|
835
|
+
const crlf = original.includes("\r\n");
|
|
836
|
+
const block = readYamlBlock(lines, "overrides");
|
|
837
|
+
if (!block) {
|
|
838
|
+
const inline = lines.find((line) => /^overrides:\s*[^\s#]/.test(line));
|
|
839
|
+
if (inline && inline.includes("@rebasepro/")) plan.skipped.push({
|
|
840
|
+
file,
|
|
841
|
+
name: "overrides",
|
|
842
|
+
field: "overrides",
|
|
843
|
+
spec: inline.slice(10).trim(),
|
|
844
|
+
reason: "an inline overrides map, which this does not rewrite; write it as a block, or edit it by hand"
|
|
845
|
+
});
|
|
846
|
+
return original;
|
|
847
|
+
}
|
|
848
|
+
const next = [...lines];
|
|
849
|
+
const removed = /* @__PURE__ */ new Set();
|
|
850
|
+
const expected = /* @__PURE__ */ new Map();
|
|
851
|
+
for (const entry of block.entries) {
|
|
852
|
+
if (!overrideTarget(entry.key)) {
|
|
853
|
+
expected.set(entry.key, entry.value);
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
856
|
+
const cls = classifySpec(entry.value);
|
|
857
|
+
const pkg = overrideTarget(entry.key);
|
|
858
|
+
if (cls.kind === "movable") {
|
|
859
|
+
const to = `${cls.prefix}${target}`;
|
|
860
|
+
expected.set(entry.key, to);
|
|
861
|
+
if (to === entry.value) {
|
|
862
|
+
plan.unchanged++;
|
|
863
|
+
continue;
|
|
864
|
+
}
|
|
865
|
+
const line = lines[entry.line];
|
|
866
|
+
next[entry.line] = line.slice(0, entry.valueStart) + to + line.slice(entry.valueEnd);
|
|
867
|
+
plan.overrides.push({
|
|
868
|
+
file,
|
|
869
|
+
name: entry.key,
|
|
870
|
+
spec: entry.value,
|
|
871
|
+
action: "bumped",
|
|
872
|
+
to
|
|
873
|
+
});
|
|
874
|
+
} else if (cls.kind === "catalog" && pkg) {
|
|
875
|
+
expected.set(entry.key, entry.value);
|
|
876
|
+
ledger.refs.push({
|
|
877
|
+
file,
|
|
878
|
+
name: entry.key,
|
|
879
|
+
field: "overrides",
|
|
880
|
+
spec: entry.value,
|
|
881
|
+
catalog: cls.catalog,
|
|
882
|
+
pkg
|
|
883
|
+
});
|
|
884
|
+
} else if (cls.kind === "local") if (options.dropLocalOverrides) {
|
|
885
|
+
removed.add(entry.line);
|
|
886
|
+
plan.overrides.push({
|
|
887
|
+
file,
|
|
888
|
+
name: entry.key,
|
|
889
|
+
spec: entry.value,
|
|
890
|
+
action: "removed-local"
|
|
891
|
+
});
|
|
892
|
+
} else {
|
|
893
|
+
expected.set(entry.key, entry.value);
|
|
894
|
+
plan.overrides.push({
|
|
895
|
+
file,
|
|
896
|
+
name: entry.key,
|
|
897
|
+
spec: entry.value,
|
|
898
|
+
action: "kept-local"
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
else {
|
|
902
|
+
expected.set(entry.key, entry.value);
|
|
903
|
+
plan.skipped.push({
|
|
904
|
+
file,
|
|
905
|
+
name: entry.key,
|
|
906
|
+
field: "overrides",
|
|
907
|
+
spec: entry.value,
|
|
908
|
+
reason: leftAloneReason(cls)
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
const survivors = block.entries.filter((entry) => !removed.has(entry.line)).length + block.otherContent;
|
|
913
|
+
if (removed.size > 0 && survivors === 0) removed.add(block.keyLine);
|
|
914
|
+
const content = next.filter((_, index) => !removed.has(index)).join(crlf ? "\r\n" : "\n");
|
|
915
|
+
const after = readYamlBlock(splitLines(content), "overrides");
|
|
916
|
+
if (!isDeepStrictEqual(after ? after.entries.map((e) => [e.key, e.value]) : [], [...expected.entries()])) throw new UpgradeError(`Could not rewrite ${file} without disturbing it; nothing was written.`, "rewrite_failed", "Move its @rebasepro overrides by hand, then run `rebase upgrade` again.");
|
|
917
|
+
return content;
|
|
918
|
+
}
|
|
919
|
+
/** Lockfiles, in the order they are looked for in each directory. */
|
|
920
|
+
var LOCKFILES = [
|
|
921
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
922
|
+
["package-lock.json", "npm"],
|
|
923
|
+
["yarn.lock", "yarn"],
|
|
924
|
+
["bun.lockb", "bun"],
|
|
925
|
+
["bun.lock", "bun"]
|
|
926
|
+
];
|
|
927
|
+
/**
|
|
928
|
+
* The package manager this project installs with, from what is on disk.
|
|
929
|
+
*
|
|
930
|
+
* A lockfile is the project's own statement, so it wins. The search starts at
|
|
931
|
+
* the project root and walks up, because a project inside a workspace has its
|
|
932
|
+
* lockfile at the workspace root; it stops at the repository's root, so a stray
|
|
933
|
+
* lockfile in a home directory is never read as this project's choice. With no
|
|
934
|
+
* lockfile anywhere, a `pnpm-workspace.yaml` still says pnpm; otherwise npm,
|
|
935
|
+
* the one every Node install has.
|
|
936
|
+
*/
|
|
937
|
+
function detectInstaller(projectRoot) {
|
|
938
|
+
let dir = path.resolve(projectRoot);
|
|
939
|
+
let workspaceYaml = false;
|
|
940
|
+
for (;;) {
|
|
941
|
+
for (const [lockfile, installer] of LOCKFILES) if (fs.existsSync(path.join(dir, lockfile))) return installer;
|
|
942
|
+
if (fs.existsSync(path.join(dir, "pnpm-workspace.yaml"))) workspaceYaml = true;
|
|
943
|
+
const parent = path.dirname(dir);
|
|
944
|
+
if (parent === dir || fs.existsSync(path.join(dir, ".git"))) break;
|
|
945
|
+
dir = parent;
|
|
946
|
+
}
|
|
947
|
+
return workspaceYaml ? "pnpm" : "npm";
|
|
948
|
+
}
|
|
949
|
+
/**
|
|
950
|
+
* The install command for each package manager — pnpm's and npm's from the
|
|
951
|
+
* helpers every other command uses, yarn's and bun's spelled the same way.
|
|
952
|
+
*/
|
|
953
|
+
function installCommand(installer) {
|
|
954
|
+
const [bin, ...args] = installer === "pnpm" || installer === "npm" ? getPMCommands(installer).install : [installer, "install"];
|
|
955
|
+
return [bin, args];
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* The exact version `--to` names.
|
|
959
|
+
*
|
|
960
|
+
* An exact version is used as written, with no network call: the control plane
|
|
961
|
+
* passes one when it rebuilds a project, and that rebuild must not depend on a
|
|
962
|
+
* registry lookup succeeding for anything but the install itself. Anything
|
|
963
|
+
* else is a dist-tag, asked of the registry through `npmView` — which runs npm
|
|
964
|
+
* in the project, so its `.npmrc` and the user's registry config apply.
|
|
965
|
+
*/
|
|
966
|
+
async function resolveTarget(requested, npmView) {
|
|
967
|
+
const trimmed = requested.trim();
|
|
968
|
+
const bare = trimmed.replace(/^v(?=\d)/, "");
|
|
969
|
+
if (isExactVersion(bare)) return bare;
|
|
970
|
+
if (!/^[A-Za-z][\w.-]*$/.test(trimmed)) throw new UpgradeError(`"${requested}" is neither an exact version nor a dist-tag.`, "target_invalid", "Pass an exact version such as 0.21.0, or a dist-tag such as latest or canary.");
|
|
971
|
+
let answer;
|
|
972
|
+
try {
|
|
973
|
+
answer = (await npmView(`${RELEASE_PACKAGE}@${trimmed}`)).trim();
|
|
974
|
+
} catch (err) {
|
|
975
|
+
const detail = err instanceof Error ? firstLine(err.message) : String(err);
|
|
976
|
+
throw new UpgradeError(`Could not resolve "${trimmed}" on the npm registry${detail ? `: ${detail}` : "."}`, "target_unresolved", "Check the tag and your registry access, or pass an exact version with --to.");
|
|
977
|
+
}
|
|
978
|
+
if (!isExactVersion(answer)) throw new UpgradeError(`The registry has no single version for ${RELEASE_PACKAGE}@${trimmed}${answer ? ` (it answered "${firstLine(answer)}")` : ""}.`, "target_unresolved", "Check the tag, or pass an exact version with --to.");
|
|
979
|
+
return answer;
|
|
980
|
+
}
|
|
981
|
+
function firstLine(text) {
|
|
982
|
+
return text.split("\n").map((line) => line.trim()).filter(Boolean)[0] ?? "";
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
985
|
+
* Whether moving from `from` to `to` goes backwards — the note the summary
|
|
986
|
+
* prints, and what `rebase upgrade` refuses without `--allow-downgrade`.
|
|
987
|
+
*
|
|
988
|
+
* Prerelease identifiers compare as SemVer orders them: a release outranks its
|
|
989
|
+
* own prereleases, numbers compare as numbers, and the channel (`alpha` <
|
|
990
|
+
* `beta` < `rc`) compares as text. One case SemVer orders and this does not:
|
|
991
|
+
* two builds on one channel told apart by a non-numeric identifier. A canary
|
|
992
|
+
* is `0.24.1-canary.g<git hash>`, and hashes sort in no order at all — so
|
|
993
|
+
* `canary.gcb918fc` → `canary.g8c5a265` is neither forwards nor backwards as
|
|
994
|
+
* far as the version can say, and the `canary` tag, which points at the newest
|
|
995
|
+
* build, is the only thing that knows.
|
|
996
|
+
*/
|
|
997
|
+
function isDowngrade(from, to) {
|
|
998
|
+
const a = SEMVER.exec(from.replace(/^[\^~]/, ""));
|
|
999
|
+
const b = SEMVER.exec(to.replace(/^[\^~]/, ""));
|
|
1000
|
+
if (!a || !b) return false;
|
|
1001
|
+
for (let i = 1; i <= 3; i++) {
|
|
1002
|
+
const diff = Number(a[i]) - Number(b[i]);
|
|
1003
|
+
if (diff !== 0) return diff > 0;
|
|
1004
|
+
}
|
|
1005
|
+
const preA = a[4];
|
|
1006
|
+
const preB = b[4];
|
|
1007
|
+
if (preA === preB) return false;
|
|
1008
|
+
if (preA === void 0) return true;
|
|
1009
|
+
if (preB === void 0) return false;
|
|
1010
|
+
const partsA = preA.split(".");
|
|
1011
|
+
const partsB = preB.split(".");
|
|
1012
|
+
for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
|
|
1013
|
+
const x = partsA[i];
|
|
1014
|
+
const y = partsB[i];
|
|
1015
|
+
if (x === void 0) return false;
|
|
1016
|
+
if (y === void 0) return true;
|
|
1017
|
+
if (x === y) continue;
|
|
1018
|
+
const nx = /^\d+$/.test(x) ? Number(x) : NaN;
|
|
1019
|
+
const ny = /^\d+$/.test(y) ? Number(y) : NaN;
|
|
1020
|
+
if (!Number.isNaN(nx) && !Number.isNaN(ny)) return nx > ny;
|
|
1021
|
+
if (!Number.isNaN(nx)) return false;
|
|
1022
|
+
if (!Number.isNaN(ny)) return true;
|
|
1023
|
+
if (i > 0) return false;
|
|
1024
|
+
return x > y;
|
|
1025
|
+
}
|
|
1026
|
+
return false;
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* The prerelease channel a project's pins are on — `canary` for
|
|
1030
|
+
* `0.24.1-canary.gcb918fc` — or null when none is on a prerelease, or they are
|
|
1031
|
+
* on more than one channel.
|
|
1032
|
+
*
|
|
1033
|
+
* The channel is the prerelease's first identifier, which is also the dist-tag
|
|
1034
|
+
* it is published under. `rebase upgrade` with no `--to` stays on it rather than
|
|
1035
|
+
* defaulting to `latest`, which for a canary project is usually older than the
|
|
1036
|
+
* build it already has.
|
|
1037
|
+
*/
|
|
1038
|
+
function prereleaseChannel(versions) {
|
|
1039
|
+
const channels = /* @__PURE__ */ new Set();
|
|
1040
|
+
for (const version of versions) {
|
|
1041
|
+
const channel = (SEMVER.exec(version.replace(/^[\^~]/, ""))?.[4])?.split(".")[0];
|
|
1042
|
+
if (channel && /^[A-Za-z][0-9A-Za-z-]*$/.test(channel)) channels.add(channel);
|
|
1043
|
+
}
|
|
1044
|
+
return channels.size === 1 ? [...channels][0] : null;
|
|
1045
|
+
}
|
|
1046
|
+
/** The pins a plan moves to an older release. */
|
|
1047
|
+
function downgradedPins(plan) {
|
|
1048
|
+
return plan.changed.filter((pin) => isDowngrade(pin.from, pin.to));
|
|
1049
|
+
}
|
|
1050
|
+
//#endregion
|
|
1051
|
+
//#region src/commands/upgrade.ts
|
|
1052
|
+
/**
|
|
1053
|
+
* CLI command: rebase upgrade [--to <version|tag>]
|
|
1054
|
+
*
|
|
1055
|
+
* Moves every `@rebasepro/*` package the project pins to one release, then
|
|
1056
|
+
* installs. The file work — which specs move, the rewrite, what overrides say —
|
|
1057
|
+
* is `../upgrade.ts`; this adds the registry lookup for a tag, the install, and
|
|
1058
|
+
* the two ways of saying what happened.
|
|
1059
|
+
*/
|
|
1060
|
+
/** Every flag `rebase upgrade` accepts. Its help page and the docs verifier read this. */
|
|
1061
|
+
var UPGRADE_FLAGS = {
|
|
1062
|
+
"--to": String,
|
|
1063
|
+
"--allow-downgrade": Boolean,
|
|
1064
|
+
"--drop-local-overrides": Boolean,
|
|
1065
|
+
"--no-install": Boolean,
|
|
1066
|
+
"--dry-run": Boolean,
|
|
1067
|
+
"--json": Boolean
|
|
1068
|
+
};
|
|
1069
|
+
function printHelp() {
|
|
1070
|
+
console.log(`
|
|
1071
|
+
${chalk.bold("rebase upgrade")} — move every @rebasepro package this project pins to one release
|
|
1072
|
+
|
|
1073
|
+
${chalk.bold("Usage")}
|
|
1074
|
+
rebase upgrade [--to <version|tag>] [options]
|
|
1075
|
+
|
|
1076
|
+
${chalk.bold("Options")}
|
|
1077
|
+
--to <version|tag> The release: an exact version (0.21.0) or a dist-tag
|
|
1078
|
+
(latest, canary). Default: latest — or, for a project
|
|
1079
|
+
on a prerelease, its channel (canary), unless latest
|
|
1080
|
+
is newer
|
|
1081
|
+
--allow-downgrade Let a dist-tag move pins to an older release. Without
|
|
1082
|
+
it that is refused; an exact --to version is taken
|
|
1083
|
+
as asked
|
|
1084
|
+
--drop-local-overrides Remove link: and file: overrides of @rebasepro packages,
|
|
1085
|
+
which win over every pin
|
|
1086
|
+
--no-install Rewrite the package.json files, but do not install
|
|
1087
|
+
--dry-run Print what would change, and write nothing
|
|
1088
|
+
--json One JSON document on stdout
|
|
1089
|
+
-h, --help Show this help
|
|
1090
|
+
|
|
1091
|
+
${chalk.bold("What it changes")}
|
|
1092
|
+
Every @rebasepro/* entry in dependencies, devDependencies and optionalDependencies,
|
|
1093
|
+
in every package.json under the project that git does not ignore (node_modules,
|
|
1094
|
+
dist* and hidden directories excepted, in a repository or not). ^ and ~ are kept.
|
|
1095
|
+
peerDependencies are left alone, and so are workspace:, link:, file:, git and tag
|
|
1096
|
+
specs, which are listed with the reason.
|
|
1097
|
+
Overrides in pnpm-workspace.yaml and package.json are bumped the same way, and so
|
|
1098
|
+
are the entries of pnpm-workspace.yaml's catalog: and catalogs: blocks, which is
|
|
1099
|
+
where a catalog: pin's version lives.
|
|
1100
|
+
|
|
1101
|
+
${chalk.bold("Examples")}
|
|
1102
|
+
rebase upgrade Move to the newest release on this project's channel
|
|
1103
|
+
rebase upgrade --to latest --allow-downgrade Move a canary project back to stable
|
|
1104
|
+
rebase upgrade --to canary --dry-run See what the canary would change
|
|
1105
|
+
rebase upgrade --to 0.21.0 --no-install --json
|
|
1106
|
+
`.trim());
|
|
1107
|
+
}
|
|
1108
|
+
var defaultIo = {
|
|
1109
|
+
npmView: async (spec, cwd) => {
|
|
1110
|
+
try {
|
|
1111
|
+
return (await execa("npm", [
|
|
1112
|
+
"view",
|
|
1113
|
+
spec,
|
|
1114
|
+
"version"
|
|
1115
|
+
], { cwd })).stdout;
|
|
1116
|
+
} catch (err) {
|
|
1117
|
+
throw new Error(npmFailure(err));
|
|
1118
|
+
}
|
|
1119
|
+
},
|
|
1120
|
+
install: async ([bin, args], cwd, quietStdout) => {
|
|
1121
|
+
await execa(bin, args, {
|
|
1122
|
+
cwd,
|
|
1123
|
+
stdio: quietStdout ? [
|
|
1124
|
+
"ignore",
|
|
1125
|
+
2,
|
|
1126
|
+
"inherit"
|
|
1127
|
+
] : "inherit"
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
};
|
|
1131
|
+
/**
|
|
1132
|
+
* The one line of a failed `npm view` worth printing.
|
|
1133
|
+
*
|
|
1134
|
+
* execa's own message is the command line that failed, which says nothing the
|
|
1135
|
+
* reader does not know. npm's stderr says why — `404 No match found for version
|
|
1136
|
+
* nosuchtag` — under an `npm error code E404` line that says less.
|
|
1137
|
+
*/
|
|
1138
|
+
function npmFailure(err) {
|
|
1139
|
+
return (typeof err === "object" && err !== null && "stderr" in err && typeof err.stderr === "string" ? err.stderr : "").split("\n").map((line) => line.replace(/^npm (?:error|ERR!)\s*/, "").trim()).find((line) => line !== "" && !/^code\s/.test(line) && !/^\d{3}$/.test(line)) ?? (err instanceof Error ? err.message : String(err));
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* The one release every moved pin came from, or `null` when they disagreed.
|
|
1143
|
+
* A range's `^`/`~` is not part of it: `upgrade --to` keeps each pin's operator.
|
|
1144
|
+
*/
|
|
1145
|
+
function previousRelease(plan) {
|
|
1146
|
+
const versions = new Set(plan.changed.map((pin) => pin.from.replace(/^[\^~]/, "")));
|
|
1147
|
+
if (versions.size !== 1) return null;
|
|
1148
|
+
const [version] = versions;
|
|
1149
|
+
return /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version) ? version : null;
|
|
1150
|
+
}
|
|
1151
|
+
async function upgradeCommand(rawArgs, io = defaultIo) {
|
|
1152
|
+
if (wantsHelp(rawArgs)) {
|
|
1153
|
+
printHelp();
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
const { flags } = parseCommandArgs({
|
|
1157
|
+
spec: UPGRADE_FLAGS,
|
|
1158
|
+
rawArgs,
|
|
1159
|
+
commandWords: 1,
|
|
1160
|
+
command: "upgrade",
|
|
1161
|
+
maxPositionals: 0
|
|
1162
|
+
});
|
|
1163
|
+
const json = flags["--json"] === true;
|
|
1164
|
+
const dryRun = flags["--dry-run"] === true;
|
|
1165
|
+
const projectRoot = requireProjectRoot();
|
|
1166
|
+
/** Every refusal, in whichever of the two languages was asked for. */
|
|
1167
|
+
const refuse = (message, code, hint, issues) => {
|
|
1168
|
+
if (json) failAsJson(message, code, hint, issues);
|
|
1169
|
+
console.error(chalk.red(`✗ ${message}`));
|
|
1170
|
+
for (const issue of issues ?? []) console.error(` ${chalk.gray(issue.path ?? "")} ${issue.message}`);
|
|
1171
|
+
if (hint) console.error(chalk.gray(` ${hint}`));
|
|
1172
|
+
process.exit(1);
|
|
1173
|
+
};
|
|
1174
|
+
try {
|
|
1175
|
+
loadManifest(projectRoot);
|
|
1176
|
+
} catch (err) {
|
|
1177
|
+
if (err instanceof ManifestError) refuse(err.message, "manifest_invalid", "Fix rebase.json, then run this again.", err.issues.map((issue) => ({
|
|
1178
|
+
path: issue.path,
|
|
1179
|
+
message: issue.message
|
|
1180
|
+
})));
|
|
1181
|
+
throw err;
|
|
1182
|
+
}
|
|
1183
|
+
const requested = flags["--to"];
|
|
1184
|
+
let plan;
|
|
1185
|
+
let channel = null;
|
|
1186
|
+
try {
|
|
1187
|
+
const npmView = (spec) => io.npmView(spec, projectRoot);
|
|
1188
|
+
const planFor = (target) => planUpgrade(projectRoot, target, { dropLocalOverrides: flags["--drop-local-overrides"] === true });
|
|
1189
|
+
plan = planFor(await resolveTarget(requested ?? "latest", npmView));
|
|
1190
|
+
channel = prereleaseChannel(plan.changed.map((pin) => pin.from));
|
|
1191
|
+
if (requested === void 0 && channel) {
|
|
1192
|
+
const onChannel = await resolveTarget(channel, npmView).catch(() => null);
|
|
1193
|
+
if (onChannel && isDowngrade(onChannel, plan.target)) {
|
|
1194
|
+
if (!json) {
|
|
1195
|
+
console.log(chalk.gray(` This project is on the ${channel} channel, so this stays on it: ${channel} is ${onChannel}.`));
|
|
1196
|
+
console.log(chalk.gray(` \`rebase upgrade --to latest\` is the stable release, ${plan.target}.`));
|
|
1197
|
+
}
|
|
1198
|
+
plan = planFor(onChannel);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
} catch (err) {
|
|
1202
|
+
if (err instanceof UpgradeError) refuse(err.message, err.code, err.hint);
|
|
1203
|
+
throw err;
|
|
1204
|
+
}
|
|
1205
|
+
const backwards = downgradedPins(plan);
|
|
1206
|
+
const exactRequest = requested !== void 0 && isExactVersion(requested.trim().replace(/^v(?=\d)/, ""));
|
|
1207
|
+
if (backwards.length > 0 && !exactRequest && flags["--allow-downgrade"] !== true && !dryRun) {
|
|
1208
|
+
const tag = requested?.trim() ?? "latest";
|
|
1209
|
+
const current = [...new Set(backwards.map((pin) => pin.from.replace(/^[\^~]/, "")))].join(", ");
|
|
1210
|
+
refuse(`\`${tag}\` is ${plan.target}, older than this project's ${current}: upgrading to it would move ${backwards.length} pin${backwards.length === 1 ? "" : "s"} backwards. Nothing was changed.`, "downgrade_refused", channel ? `Stay on the ${channel} channel with \`rebase upgrade --to ${channel}\`, or pass --allow-downgrade to move to ${plan.target}.` : `Pass --allow-downgrade to move to ${plan.target} anyway, or --to <version> for an exact release.`);
|
|
1211
|
+
}
|
|
1212
|
+
if (!dryRun) applyUpgradePlan(plan);
|
|
1213
|
+
const wrote = !dryRun && plan.writes.length > 0;
|
|
1214
|
+
const install = wrote && flags["--no-install"] !== true;
|
|
1215
|
+
const installer = detectInstaller(projectRoot);
|
|
1216
|
+
const [bin, args] = installCommand(installer);
|
|
1217
|
+
const installLine = [bin, ...args].join(" ");
|
|
1218
|
+
if (!json) {
|
|
1219
|
+
printSummary(plan, dryRun);
|
|
1220
|
+
if (backwards.length > 0) {
|
|
1221
|
+
const pins = `${backwards.length} pin${backwards.length === 1 ? "" : "s"}`;
|
|
1222
|
+
console.log(chalk.yellow(exactRequest || flags["--allow-downgrade"] === true ? ` ⚠ ${plan.target} is an older release than ${pins} here ${dryRun ? "are" : "were"} on: this is a downgrade.` : ` ⚠ ${plan.target} is an older release than ${pins} here are on. A real run refuses it without --allow-downgrade.`));
|
|
1223
|
+
console.log("");
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
let installed = false;
|
|
1227
|
+
if (install) {
|
|
1228
|
+
if (!json) console.log(chalk.gray(` Installing with ${installer}...\n`));
|
|
1229
|
+
try {
|
|
1230
|
+
await io.install([bin, args], projectRoot, json);
|
|
1231
|
+
installed = true;
|
|
1232
|
+
} catch {
|
|
1233
|
+
const previous = previousRelease(plan);
|
|
1234
|
+
refuse(`\`${installLine}\` failed.`, "install_failed", `The package.json edits were kept. Fix what the install reported and run \`${installLine}\` again, ` + (previous ? `or put every pin back with \`rebase upgrade --to ${previous}\`.` : "or revert the edits with git."));
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
const result = {
|
|
1238
|
+
target: plan.target,
|
|
1239
|
+
changed: plan.changed,
|
|
1240
|
+
skipped: plan.skipped,
|
|
1241
|
+
overrides: plan.overrides,
|
|
1242
|
+
unreadable: plan.unreadable,
|
|
1243
|
+
installed,
|
|
1244
|
+
dryRun
|
|
1245
|
+
};
|
|
1246
|
+
if (json) {
|
|
1247
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
printOutcome(plan, {
|
|
1251
|
+
dryRun,
|
|
1252
|
+
wrote,
|
|
1253
|
+
installed,
|
|
1254
|
+
installLine
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1257
|
+
/** Changes grouped by file, then what was left alone and why, then overrides. */
|
|
1258
|
+
function printSummary(plan, dryRun) {
|
|
1259
|
+
console.log("");
|
|
1260
|
+
console.log(`${chalk.bold("Rebase")} — ${dryRun ? "what upgrading" : "upgrading"} @rebasepro packages to ${chalk.cyan(plan.target)}${dryRun ? " would change" : ""}`);
|
|
1261
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
1262
|
+
for (const pin of plan.changed) byFile.set(pin.file, [...byFile.get(pin.file) ?? [], pin]);
|
|
1263
|
+
const nameWidth = Math.max(0, ...plan.changed.map((pin) => pin.name.length));
|
|
1264
|
+
for (const [file, pins] of byFile) {
|
|
1265
|
+
console.log("");
|
|
1266
|
+
console.log(` ${chalk.bold(file)}`);
|
|
1267
|
+
for (const pin of pins) {
|
|
1268
|
+
const field = pin.field === "dependencies" ? "" : chalk.gray(` ${pin.field}`);
|
|
1269
|
+
const older = isDowngrade(pin.from, pin.to) ? chalk.yellow(" (older)") : "";
|
|
1270
|
+
console.log(` ${pin.name.padEnd(nameWidth)} ${chalk.gray(pin.from)} → ${chalk.green(pin.to)}${field}${older}`);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
if (plan.skipped.length > 0) {
|
|
1274
|
+
console.log("");
|
|
1275
|
+
console.log(` ${chalk.bold("Left alone")}`);
|
|
1276
|
+
for (const skip of plan.skipped) {
|
|
1277
|
+
console.log(` ${skip.name} ${chalk.gray(skip.spec)} ${chalk.gray(`(${skip.file}, ${skip.field})`)}`);
|
|
1278
|
+
console.log(chalk.gray(` ${skip.reason}`));
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
if (plan.overrides.length > 0) {
|
|
1282
|
+
console.log("");
|
|
1283
|
+
console.log(` ${chalk.bold("Overrides")}`);
|
|
1284
|
+
for (const override of plan.overrides) {
|
|
1285
|
+
const where = chalk.gray(`(${override.file})`);
|
|
1286
|
+
if (override.action === "bumped") console.log(` ${override.name} ${chalk.gray(override.spec)} → ${chalk.green(override.to ?? "")} ${where}`);
|
|
1287
|
+
else if (override.action === "removed-local") console.log(` ${override.name} ${chalk.gray(override.spec)} ${where} ${chalk.green(dryRun ? "would be removed" : "removed")}`);
|
|
1288
|
+
else {
|
|
1289
|
+
console.log(` ${chalk.yellow("⚠")} ${override.name} → ${override.spec} ${where}`);
|
|
1290
|
+
console.log(chalk.yellow(" local override — wins over every pin, the upgrade will not reach these packages"));
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
if (plan.overrides.some((o) => o.action === "kept-local")) console.log(chalk.gray(" Run again with --drop-local-overrides to remove them."));
|
|
1294
|
+
}
|
|
1295
|
+
if (plan.unreadable.length > 0) {
|
|
1296
|
+
console.log("");
|
|
1297
|
+
console.log(` ${chalk.bold("Not read")}`);
|
|
1298
|
+
for (const entry of plan.unreadable) console.log(` ${entry.file} ${chalk.gray(entry.reason)}`);
|
|
1299
|
+
}
|
|
1300
|
+
console.log("");
|
|
1301
|
+
}
|
|
1302
|
+
function printOutcome(plan, outcome) {
|
|
1303
|
+
const moved = plan.changed.length + plan.overrides.filter((o) => o.action !== "kept-local").length;
|
|
1304
|
+
const leftAlone = plan.skipped.length + plan.overrides.filter((o) => o.action === "kept-local").length;
|
|
1305
|
+
const entries = (n) => `${n} entr${n === 1 ? "y" : "ies"}`;
|
|
1306
|
+
if (moved === 0 && leftAlone > 0) {
|
|
1307
|
+
if (plan.unchanged > 0) console.log(chalk.green(`✓ Every pin this can move is already on ${plan.target}; ${entries(leftAlone)} ${leftAlone === 1 ? "was" : "were"} left alone, above.`));
|
|
1308
|
+
else if (outcome.dryRun) console.log(chalk.yellow(` Nothing to move to ${plan.target}: ${entries(leftAlone)} ${leftAlone === 1 ? "is" : "are"} left alone, above.`));
|
|
1309
|
+
else console.log(chalk.yellow(`⚠ Nothing moved to ${plan.target}: ${entries(leftAlone)} ${leftAlone === 1 ? "was" : "were"} left alone, above.`));
|
|
1310
|
+
console.log("");
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
if (outcome.dryRun) {
|
|
1314
|
+
console.log(chalk.gray(moved === 0 ? ` Nothing to change: this project is already on ${plan.target}.` : " Dry run — nothing was written. Run it without --dry-run to apply."));
|
|
1315
|
+
console.log("");
|
|
1316
|
+
return;
|
|
1317
|
+
}
|
|
1318
|
+
if (!outcome.wrote) {
|
|
1319
|
+
console.log(chalk.green(`✓ This project is already on ${plan.target}.`));
|
|
1320
|
+
console.log("");
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
const files = plan.writes.length;
|
|
1324
|
+
console.log(chalk.green(`✓ Moved ${moved} entr${moved === 1 ? "y" : "ies"} in ${files} file${files === 1 ? "" : "s"} to ${plan.target}${outcome.installed ? " and installed" : ""}.`));
|
|
1325
|
+
if (!outcome.installed) console.log(chalk.gray(` Install with \`${outcome.installLine}\`, then run \`rebase build\`, then \`rebase cloud deploy\`.`));
|
|
1326
|
+
else console.log(chalk.gray(" Run `rebase build`, then `rebase cloud deploy`."));
|
|
1327
|
+
console.log("");
|
|
1328
|
+
}
|
|
1329
|
+
//#endregion
|
|
1330
|
+
export { upgradeCommand };
|
|
1331
|
+
|
|
1332
|
+
//# sourceMappingURL=upgrade-CtZtRkTE.js.map
|