@rebasepro/cli 0.21.1-canary.g8c5a265 → 0.21.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/cloud/bundle-deploy.d.ts +9 -0
- package/dist/commands/cloud/deploy.d.ts +33 -0
- package/dist/commands/cloud/rebuild-source.d.ts +110 -0
- package/dist/commands/cloud/settings.d.ts +6 -1
- package/dist/commands/upgrade.d.ts +38 -0
- package/dist/index.es.js +1552 -48
- package/dist/index.es.js.map +1 -1
- package/dist/upgrade.d.ts +145 -0
- package/package.json +7 -7
package/dist/index.es.js
CHANGED
|
@@ -13,7 +13,7 @@ import path from "path";
|
|
|
13
13
|
import * as fs$1 from "fs";
|
|
14
14
|
import fs from "fs";
|
|
15
15
|
import net from "net";
|
|
16
|
-
import { promisify } from "util";
|
|
16
|
+
import { isDeepStrictEqual, promisify } from "util";
|
|
17
17
|
import { execa, execaCommandSync } from "execa";
|
|
18
18
|
import { cp } from "fs/promises";
|
|
19
19
|
import { fileURLToPath, pathToFileURL } from "url";
|
|
@@ -2362,11 +2362,11 @@ async function probeRelease(version) {
|
|
|
2362
2362
|
};
|
|
2363
2363
|
return {
|
|
2364
2364
|
kind: "unreachable",
|
|
2365
|
-
reason: firstLine(text) || "the registry could not be reached"
|
|
2365
|
+
reason: firstLine$1(text) || "the registry could not be reached"
|
|
2366
2366
|
};
|
|
2367
2367
|
}
|
|
2368
2368
|
}
|
|
2369
|
-
function firstLine(text) {
|
|
2369
|
+
function firstLine$1(text) {
|
|
2370
2370
|
return text.split("\n").map((l) => l.trim()).filter(Boolean)[0] ?? "";
|
|
2371
2371
|
}
|
|
2372
2372
|
async function replacePlaceholders(options) {
|
|
@@ -4545,7 +4545,7 @@ function checkCmsPath(value, appPath, fieldPath, issues) {
|
|
|
4545
4545
|
}
|
|
4546
4546
|
return cms;
|
|
4547
4547
|
}
|
|
4548
|
-
function isRecord(value) {
|
|
4548
|
+
function isRecord$1(value) {
|
|
4549
4549
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4550
4550
|
}
|
|
4551
4551
|
/**
|
|
@@ -4682,7 +4682,7 @@ function isNearMiss(a, b) {
|
|
|
4682
4682
|
}
|
|
4683
4683
|
function validateApp(name, raw, issues) {
|
|
4684
4684
|
const base = `apps.${name}`;
|
|
4685
|
-
if (!isRecord(raw)) {
|
|
4685
|
+
if (!isRecord$1(raw)) {
|
|
4686
4686
|
issues.push({
|
|
4687
4687
|
path: base,
|
|
4688
4688
|
message: "must be an object"
|
|
@@ -4782,7 +4782,7 @@ function validateApp(name, raw, issues) {
|
|
|
4782
4782
|
*/
|
|
4783
4783
|
function validateManifest(raw) {
|
|
4784
4784
|
const issues = [];
|
|
4785
|
-
if (!isRecord(raw)) return { issues: [{
|
|
4785
|
+
if (!isRecord$1(raw)) return { issues: [{
|
|
4786
4786
|
path: "",
|
|
4787
4787
|
message: `${MANIFEST_FILENAME} must contain a JSON object`
|
|
4788
4788
|
}] };
|
|
@@ -4793,7 +4793,7 @@ function validateManifest(raw) {
|
|
|
4793
4793
|
message
|
|
4794
4794
|
});
|
|
4795
4795
|
}
|
|
4796
|
-
if (!isRecord(raw.apps)) {
|
|
4796
|
+
if (!isRecord$1(raw.apps)) {
|
|
4797
4797
|
issues.push({
|
|
4798
4798
|
path: "apps",
|
|
4799
4799
|
message: "is required and must be an object"
|
|
@@ -4981,7 +4981,7 @@ var MODELLED_MANIFEST_KEYS = [
|
|
|
4981
4981
|
function readManifestObject(filePath) {
|
|
4982
4982
|
try {
|
|
4983
4983
|
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
4984
|
-
return isRecord(parsed) ? parsed : {};
|
|
4984
|
+
return isRecord$1(parsed) ? parsed : {};
|
|
4985
4985
|
} catch {
|
|
4986
4986
|
return {};
|
|
4987
4987
|
}
|
|
@@ -8824,7 +8824,7 @@ function projectResourceGraph(graph) {
|
|
|
8824
8824
|
* entrypoint, falls back to the previous behaviour: run every workspace's own
|
|
8825
8825
|
* `build` script. Nothing that built before stops building.
|
|
8826
8826
|
*/
|
|
8827
|
-
function printHelp$
|
|
8827
|
+
function printHelp$6() {
|
|
8828
8828
|
console.log(`
|
|
8829
8829
|
${chalk.bold("rebase build")} — build the apps declared in rebase.json
|
|
8830
8830
|
|
|
@@ -8886,7 +8886,7 @@ function dockerBuildHint(projectRoot, name, app) {
|
|
|
8886
8886
|
}
|
|
8887
8887
|
async function buildCommand(rawArgs = []) {
|
|
8888
8888
|
if (wantsHelp(rawArgs)) {
|
|
8889
|
-
printHelp$
|
|
8889
|
+
printHelp$6();
|
|
8890
8890
|
return;
|
|
8891
8891
|
}
|
|
8892
8892
|
const { flags: args, positionals: requested } = parseCommandArgs({
|
|
@@ -9012,10 +9012,12 @@ async function buildCommand(rawArgs = []) {
|
|
|
9012
9012
|
console.log(chalk.yellow(` ⚠ framework dependencies older than this CLI (${cliVersion$1()}):`));
|
|
9013
9013
|
for (const dep of drift.behind) console.log(chalk.dim(` ${dep.name}@${dep.range} (${dep.file})`));
|
|
9014
9014
|
console.log(chalk.dim(" The image supplies the server, but your bundle supplies the database"));
|
|
9015
|
-
console.log(chalk.dim(" driver — a newer runtime does not update it.
|
|
9015
|
+
console.log(chalk.dim(" driver — a newer runtime does not update it."));
|
|
9016
|
+
console.log(chalk.dim(` Run \`${upgradeToThisCli()}\`, then rebuild.`));
|
|
9016
9017
|
} else if (drift.disagreeing.length > 0) {
|
|
9017
9018
|
console.log(chalk.yellow(` ⚠ mixed @rebasepro versions declared: ${drift.disagreeing.join(", ")}`));
|
|
9018
|
-
console.log(chalk.dim(" These are published together and expect to run together
|
|
9019
|
+
console.log(chalk.dim(" These are published together and expect to run together."));
|
|
9020
|
+
console.log(chalk.dim(` Run \`${upgradeToThisCli()}\` to pin them alike, then rebuild.`));
|
|
9019
9021
|
}
|
|
9020
9022
|
if (!args["--no-static"]) {
|
|
9021
9023
|
const folded = await foldFrontendIntoBundle({
|
|
@@ -9091,6 +9093,17 @@ async function buildAssetApp(projectRoot, name, app, runtimeRange, outOverride)
|
|
|
9091
9093
|
console.log(chalk.green(` ✓ static bundle → ${rel}/`) + chalk.dim(` (${result.fileCount} file(s) → served at ${basePath})`));
|
|
9092
9094
|
return result.outDir;
|
|
9093
9095
|
}
|
|
9096
|
+
/**
|
|
9097
|
+
* The `rebase upgrade` line the drift warnings print.
|
|
9098
|
+
*
|
|
9099
|
+
* Aimed at this CLI's own version, because that is what the warning measured
|
|
9100
|
+
* against: "older than this CLI" is answered by moving to exactly this CLI, not
|
|
9101
|
+
* to whatever `latest` happens to be — which, on a canary, is older still.
|
|
9102
|
+
*/
|
|
9103
|
+
function upgradeToThisCli() {
|
|
9104
|
+
const version = cliVersion$1();
|
|
9105
|
+
return version === "unknown" ? "rebase upgrade" : `rebase upgrade --to ${version}`;
|
|
9106
|
+
}
|
|
9094
9107
|
/** The pre-manifest behaviour: build every workspace package. */
|
|
9095
9108
|
async function runWorkspaceBuilds(projectRoot) {
|
|
9096
9109
|
const pm = detectPackageManager(projectRoot);
|
|
@@ -9240,7 +9253,7 @@ function projectNameOf(projectRoot) {
|
|
|
9240
9253
|
} catch {}
|
|
9241
9254
|
return path.basename(projectRoot);
|
|
9242
9255
|
}
|
|
9243
|
-
function printHelp$
|
|
9256
|
+
function printHelp$5() {
|
|
9244
9257
|
console.log(`
|
|
9245
9258
|
${chalk.bold("rebase eject")} — take ownership of the server process
|
|
9246
9259
|
|
|
@@ -9261,7 +9274,7 @@ ${chalk.bold("Options")}
|
|
|
9261
9274
|
}
|
|
9262
9275
|
async function ejectCommand(rawArgs = []) {
|
|
9263
9276
|
if (wantsHelp(rawArgs)) {
|
|
9264
|
-
printHelp$
|
|
9277
|
+
printHelp$5();
|
|
9265
9278
|
return;
|
|
9266
9279
|
}
|
|
9267
9280
|
const { flags: args, positionals } = parseCommandArgs({
|
|
@@ -9443,7 +9456,7 @@ function restoreBackendScripts(projectRoot) {
|
|
|
9443
9456
|
* `rebase.json`) this falls back to the backend workspace's own `start` script,
|
|
9444
9457
|
* which is what such a project has always used.
|
|
9445
9458
|
*/
|
|
9446
|
-
function printHelp$
|
|
9459
|
+
function printHelp$4() {
|
|
9447
9460
|
console.log(`
|
|
9448
9461
|
${chalk.bold("rebase start")} — run a built bundle
|
|
9449
9462
|
|
|
@@ -9460,7 +9473,7 @@ Build first with ${chalk.cyan("rebase build")}.
|
|
|
9460
9473
|
}
|
|
9461
9474
|
async function startCommand(rawArgs = []) {
|
|
9462
9475
|
if (wantsHelp(rawArgs)) {
|
|
9463
|
-
printHelp$
|
|
9476
|
+
printHelp$4();
|
|
9464
9477
|
return;
|
|
9465
9478
|
}
|
|
9466
9479
|
const { flags: args } = parseCommandArgs({
|
|
@@ -10056,7 +10069,7 @@ function checkNodeVersion(running, required) {
|
|
|
10056
10069
|
}];
|
|
10057
10070
|
}
|
|
10058
10071
|
/** The lockfiles a project may have, and the manager each one belongs to. */
|
|
10059
|
-
var LOCKFILES = {
|
|
10072
|
+
var LOCKFILES$1 = {
|
|
10060
10073
|
"pnpm-lock.yaml": "pnpm",
|
|
10061
10074
|
"package-lock.json": "npm",
|
|
10062
10075
|
"yarn.lock": "yarn",
|
|
@@ -10073,18 +10086,18 @@ var LOCKFILES = {
|
|
|
10073
10086
|
* at it.
|
|
10074
10087
|
*/
|
|
10075
10088
|
function checkPackageManager(lockfilesPresent, declared) {
|
|
10076
|
-
const known = lockfilesPresent.filter((file) => file in LOCKFILES);
|
|
10089
|
+
const known = lockfilesPresent.filter((file) => file in LOCKFILES$1);
|
|
10077
10090
|
if (known.length < 2) {
|
|
10078
10091
|
const declaredName = declared?.split("@")[0];
|
|
10079
|
-
if (known.length === 1 && declaredName && LOCKFILES[known[0]] !== declaredName) return [{
|
|
10092
|
+
if (known.length === 1 && declaredName && LOCKFILES$1[known[0]] !== declaredName) return [{
|
|
10080
10093
|
check: "package-manager",
|
|
10081
10094
|
severity: "warning",
|
|
10082
|
-
message: `This project declares packageManager "${declared}" and has a ${LOCKFILES[known[0]]} lockfile (${known[0]}).`,
|
|
10095
|
+
message: `This project declares packageManager "${declared}" and has a ${LOCKFILES$1[known[0]]} lockfile (${known[0]}).`,
|
|
10083
10096
|
fix: `Delete ${known[0]} and node_modules, then install with ${declaredName}.`
|
|
10084
10097
|
}];
|
|
10085
10098
|
return [];
|
|
10086
10099
|
}
|
|
10087
|
-
const managers = known.map((file) => LOCKFILES[file]);
|
|
10100
|
+
const managers = known.map((file) => LOCKFILES$1[file]);
|
|
10088
10101
|
return [{
|
|
10089
10102
|
check: "package-manager",
|
|
10090
10103
|
severity: "error",
|
|
@@ -10192,7 +10205,7 @@ function checkVersionSkew(declared) {
|
|
|
10192
10205
|
check: "versions",
|
|
10193
10206
|
severity: "error",
|
|
10194
10207
|
message: `${name} is pinned to different versions in this project: ${described}.`,
|
|
10195
|
-
fix: "
|
|
10208
|
+
fix: "Run `rebase upgrade` to pin every @rebasepro package to one release and reinstall. Two copies of a Rebase package break `instanceof` between them, which fails as a type guard rejecting its own type."
|
|
10196
10209
|
});
|
|
10197
10210
|
}
|
|
10198
10211
|
return findings;
|
|
@@ -10318,7 +10331,7 @@ function collectEnvironmentFindings(projectRoot, envFile) {
|
|
|
10318
10331
|
const findings = [];
|
|
10319
10332
|
findings.push(...checkNodeVersion(process.versions.node, readCliEngines()));
|
|
10320
10333
|
const rootPackage = readJson(path.join(projectRoot, "package.json"));
|
|
10321
|
-
findings.push(...checkPackageManager(Object.keys(LOCKFILES).filter((file) => fs.existsSync(path.join(projectRoot, file))), typeof rootPackage?.packageManager === "string" ? rootPackage.packageManager : void 0));
|
|
10334
|
+
findings.push(...checkPackageManager(Object.keys(LOCKFILES$1).filter((file) => fs.existsSync(path.join(projectRoot, file))), typeof rootPackage?.packageManager === "string" ? rootPackage.packageManager : void 0));
|
|
10322
10335
|
findings.push(...checkDuplicateSlugs(readDeclaredSlugs(projectRoot)));
|
|
10323
10336
|
if (envFile && fs.existsSync(envFile)) try {
|
|
10324
10337
|
findings.push(...checkEnvSanity(parseEnvFile(fs.readFileSync(envFile, "utf-8"))));
|
|
@@ -10385,7 +10398,7 @@ function readAtlasBinaryState(projectRoot) {
|
|
|
10385
10398
|
}
|
|
10386
10399
|
/** The reader's own package manager, from the lockfile beside their project. */
|
|
10387
10400
|
function readPackageManagerName(projectRoot) {
|
|
10388
|
-
for (const [file, manager] of Object.entries(LOCKFILES)) if (fs.existsSync(path.join(projectRoot, file))) return manager;
|
|
10401
|
+
for (const [file, manager] of Object.entries(LOCKFILES$1)) if (fs.existsSync(path.join(projectRoot, file))) return manager;
|
|
10389
10402
|
return "pnpm";
|
|
10390
10403
|
}
|
|
10391
10404
|
/** The `engines.node` range of the CLI actually running. */
|
|
@@ -11942,7 +11955,7 @@ var TELEMETRY_SUBCOMMANDS = [
|
|
|
11942
11955
|
];
|
|
11943
11956
|
async function telemetryCommand(rawArgs) {
|
|
11944
11957
|
if (wantsHelp(rawArgs)) {
|
|
11945
|
-
printHelp$
|
|
11958
|
+
printHelp$3();
|
|
11946
11959
|
return;
|
|
11947
11960
|
}
|
|
11948
11961
|
const { positionals } = parseCommandArgs({
|
|
@@ -12020,7 +12033,7 @@ function printPayload() {
|
|
|
12020
12033
|
console.log(chalk.gray(" hostname, your project name or anything you have typed."));
|
|
12021
12034
|
console.log("");
|
|
12022
12035
|
}
|
|
12023
|
-
function printHelp$
|
|
12036
|
+
function printHelp$3() {
|
|
12024
12037
|
console.log(`
|
|
12025
12038
|
${chalk.bold("rebase telemetry")} — anonymous usage sharing (asked once per project)
|
|
12026
12039
|
|
|
@@ -15120,7 +15133,13 @@ function bundleDeployBody(input) {
|
|
|
15120
15133
|
...input.commit ? {
|
|
15121
15134
|
gitCommitHash: input.commit.hash,
|
|
15122
15135
|
gitCommitMessage: input.commit.message
|
|
15123
|
-
} : {}
|
|
15136
|
+
} : {},
|
|
15137
|
+
...input.rebuildSource ? { rebuildSource: {
|
|
15138
|
+
sourceId: input.rebuildSource.sourceId,
|
|
15139
|
+
projectPath: input.rebuildSource.projectPath,
|
|
15140
|
+
buildEnv: input.rebuildSource.buildEnv
|
|
15141
|
+
} } : {},
|
|
15142
|
+
...input.allowFrameworkDowngrade ? { allowFrameworkDowngrade: true } : {}
|
|
15124
15143
|
};
|
|
15125
15144
|
}
|
|
15126
15145
|
/**
|
|
@@ -15161,10 +15180,393 @@ async function uploadBundle(url, token, projectId, tarPath) {
|
|
|
15161
15180
|
return data.bundleId;
|
|
15162
15181
|
}
|
|
15163
15182
|
//#endregion
|
|
15183
|
+
//#region src/commands/cloud/rebuild-source.ts
|
|
15184
|
+
/**
|
|
15185
|
+
* The project's source, uploaded beside its bundle so the platform can rebuild it.
|
|
15186
|
+
*
|
|
15187
|
+
* A managed deploy ships a built bundle, and a bundle is fixed to the framework
|
|
15188
|
+
* release it was built on. When the platform rolls a newer release across the
|
|
15189
|
+
* fleet it has to rebuild each project from source — and the bundle path never
|
|
15190
|
+
* uploaded any: the control plane held a tarball of compiled output and
|
|
15191
|
+
* nothing it could compile again. So every bundle deploy now also packs the
|
|
15192
|
+
* source the bundle was built from, uploads it, and names it in the trigger.
|
|
15193
|
+
*
|
|
15194
|
+
* Three rules shape what goes in:
|
|
15195
|
+
*
|
|
15196
|
+
* - **Git decides, when there is a repository.** `git ls-files --cached
|
|
15197
|
+
* --others --exclude-standard` at the repository's top level is exactly what
|
|
15198
|
+
* the developer considers source: `.gitignore` honoured the way git honours
|
|
15199
|
+
* it, and a sibling package the project links (`link:../../packages/editor`)
|
|
15200
|
+
* included, because it is outside the project directory but inside the
|
|
15201
|
+
* repository.
|
|
15202
|
+
* - **Secrets never leave the machine, whatever git says.** A committed `.env`
|
|
15203
|
+
* is still a `.env`. {@link neverUploaded} is applied after git, not instead
|
|
15204
|
+
* of it.
|
|
15205
|
+
* - **It never costs the deploy.** The bundle is what runs; the source only
|
|
15206
|
+
* lets a later platform upgrade rebuild it. Too large, unpackable, refused by
|
|
15207
|
+
* the control plane — each is a warning, and the deploy goes on without it.
|
|
15208
|
+
*/
|
|
15209
|
+
/**
|
|
15210
|
+
* The control plane's cap on an uploaded source archive.
|
|
15211
|
+
*
|
|
15212
|
+
* Keep in sync with its build-context cap (deploy/upload `MAX_BYTES` and the
|
|
15213
|
+
* backend's `maxBodySize`). Checked before uploading, so an oversized archive is
|
|
15214
|
+
* a message in milliseconds rather than a bare 413 after the upload.
|
|
15215
|
+
*/
|
|
15216
|
+
var MAX_SOURCE_UPLOAD_BYTES = 100 * 1024 * 1024;
|
|
15217
|
+
/** Env files that are templates by convention, and safe to ship. */
|
|
15218
|
+
var ENV_TEMPLATES = /* @__PURE__ */ new Set([
|
|
15219
|
+
".env.example",
|
|
15220
|
+
".env.sample",
|
|
15221
|
+
".env.template"
|
|
15222
|
+
]);
|
|
15223
|
+
/**
|
|
15224
|
+
* Whether a file must never be uploaded, whatever the ignore rules said.
|
|
15225
|
+
*
|
|
15226
|
+
* `relativePath` is POSIX. Installed packages, git's own directory and built
|
|
15227
|
+
* bundles are never source. Every `.env` and `.env.*` is excluded except the
|
|
15228
|
+
* three template names, and so is direnv's `.envrc`, which is the same thing
|
|
15229
|
+
* under another name.
|
|
15230
|
+
*/
|
|
15231
|
+
function neverUploaded(relativePath) {
|
|
15232
|
+
const segments = relativePath.split("/");
|
|
15233
|
+
if (segments.some((segment) => segment === "node_modules" || segment === ".git" || segment.startsWith("dist-bundle"))) return true;
|
|
15234
|
+
const base = segments[segments.length - 1];
|
|
15235
|
+
if (base === ".envrc") return true;
|
|
15236
|
+
if ((base === ".env" || base.startsWith(".env.")) && !ENV_TEMPLATES.has(base)) return true;
|
|
15237
|
+
return false;
|
|
15238
|
+
}
|
|
15239
|
+
/** Directories the walk outside a repository never enters. */
|
|
15240
|
+
var WALK_SKIP = /* @__PURE__ */ new Set([
|
|
15241
|
+
"node_modules",
|
|
15242
|
+
".git",
|
|
15243
|
+
".rebase",
|
|
15244
|
+
".turbo",
|
|
15245
|
+
".next",
|
|
15246
|
+
"coverage"
|
|
15247
|
+
]);
|
|
15248
|
+
var runGit = (cwd, args) => execFileSync("git", [
|
|
15249
|
+
"-C",
|
|
15250
|
+
cwd,
|
|
15251
|
+
...args
|
|
15252
|
+
], {
|
|
15253
|
+
encoding: "utf8",
|
|
15254
|
+
stdio: [
|
|
15255
|
+
"ignore",
|
|
15256
|
+
"pipe",
|
|
15257
|
+
"ignore"
|
|
15258
|
+
],
|
|
15259
|
+
maxBuffer: 256 * 1024 * 1024
|
|
15260
|
+
});
|
|
15261
|
+
function toPosix(relative) {
|
|
15262
|
+
return relative.split(path.sep).join("/");
|
|
15263
|
+
}
|
|
15264
|
+
/** A regular file on disk: not missing, not a directory or submodule, not a symlink. */
|
|
15265
|
+
function isRegularFile(absolute) {
|
|
15266
|
+
try {
|
|
15267
|
+
return fs.lstatSync(absolute).isFile();
|
|
15268
|
+
} catch {
|
|
15269
|
+
return false;
|
|
15270
|
+
}
|
|
15271
|
+
}
|
|
15272
|
+
/**
|
|
15273
|
+
* The files of the repository `dir` belongs to — tracked and unignored, the way
|
|
15274
|
+
* git sees them — or, outside any repository, `dir` walked with fixed excludes.
|
|
15275
|
+
*/
|
|
15276
|
+
function listUnit(dir, git) {
|
|
15277
|
+
try {
|
|
15278
|
+
const toplevel = git(dir, ["rev-parse", "--show-toplevel"]).trim();
|
|
15279
|
+
if (toplevel) {
|
|
15280
|
+
const root = fs.realpathSync(toplevel);
|
|
15281
|
+
const listed = git(root, [
|
|
15282
|
+
"ls-files",
|
|
15283
|
+
"-z",
|
|
15284
|
+
"--cached",
|
|
15285
|
+
"--others",
|
|
15286
|
+
"--exclude-standard"
|
|
15287
|
+
]);
|
|
15288
|
+
return {
|
|
15289
|
+
root,
|
|
15290
|
+
files: [...new Set(listed.split("\0").filter(Boolean))].filter((relative) => !neverUploaded(relative) && isRegularFile(path.join(root, relative))),
|
|
15291
|
+
fromGit: true
|
|
15292
|
+
};
|
|
15293
|
+
}
|
|
15294
|
+
} catch {}
|
|
15295
|
+
const files = [];
|
|
15296
|
+
const walk = (current, prefix) => {
|
|
15297
|
+
let entries;
|
|
15298
|
+
try {
|
|
15299
|
+
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
15300
|
+
} catch {
|
|
15301
|
+
return;
|
|
15302
|
+
}
|
|
15303
|
+
for (const entry of entries) {
|
|
15304
|
+
const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
15305
|
+
if (entry.isDirectory()) {
|
|
15306
|
+
if (WALK_SKIP.has(entry.name) || entry.name.startsWith("dist")) continue;
|
|
15307
|
+
walk(path.join(current, entry.name), relative);
|
|
15308
|
+
} else if (entry.isFile() && !neverUploaded(relative)) files.push(relative);
|
|
15309
|
+
}
|
|
15310
|
+
};
|
|
15311
|
+
walk(dir, "");
|
|
15312
|
+
return {
|
|
15313
|
+
root: dir,
|
|
15314
|
+
files,
|
|
15315
|
+
fromGit: false
|
|
15316
|
+
};
|
|
15317
|
+
}
|
|
15318
|
+
/** Whether `candidate` is `dir` or inside it. */
|
|
15319
|
+
function isWithin(dir, candidate) {
|
|
15320
|
+
const relative = path.relative(dir, candidate);
|
|
15321
|
+
return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
15322
|
+
}
|
|
15323
|
+
/** The deepest directory containing every path given. */
|
|
15324
|
+
function commonAncestor(paths) {
|
|
15325
|
+
let ancestor = paths[0];
|
|
15326
|
+
for (const p of paths.slice(1)) while (!isWithin(ancestor, p)) ancestor = path.dirname(ancestor);
|
|
15327
|
+
return ancestor;
|
|
15328
|
+
}
|
|
15329
|
+
var DEPENDENCY_FIELDS = [
|
|
15330
|
+
"dependencies",
|
|
15331
|
+
"devDependencies",
|
|
15332
|
+
"optionalDependencies"
|
|
15333
|
+
];
|
|
15334
|
+
/**
|
|
15335
|
+
* Directories outside every unit that a unit's `package.json` files link to
|
|
15336
|
+
* with `link:` or `file:`.
|
|
15337
|
+
*
|
|
15338
|
+
* `@rebasepro/*` links are not followed. They point at a local framework
|
|
15339
|
+
* checkout, which is not the project's source and would drag a whole monorepo
|
|
15340
|
+
* into the archive; a rebuild moves those packages to a published release
|
|
15341
|
+
* anyway.
|
|
15342
|
+
*/
|
|
15343
|
+
function externalLinkTargets(units) {
|
|
15344
|
+
const targets = /* @__PURE__ */ new Set();
|
|
15345
|
+
for (const unit of units) for (const relative of unit.files) {
|
|
15346
|
+
if (path.posix.basename(relative) !== "package.json") continue;
|
|
15347
|
+
let manifest;
|
|
15348
|
+
try {
|
|
15349
|
+
manifest = JSON.parse(fs.readFileSync(path.join(unit.root, relative), "utf8"));
|
|
15350
|
+
} catch {
|
|
15351
|
+
continue;
|
|
15352
|
+
}
|
|
15353
|
+
if (typeof manifest !== "object" || manifest === null) continue;
|
|
15354
|
+
for (const field of DEPENDENCY_FIELDS) {
|
|
15355
|
+
const deps = manifest[field];
|
|
15356
|
+
if (typeof deps !== "object" || deps === null) continue;
|
|
15357
|
+
for (const [name, spec] of Object.entries(deps)) {
|
|
15358
|
+
if (name.startsWith("@rebasepro/") || typeof spec !== "string") continue;
|
|
15359
|
+
const match = /^(?:link|file):(.+)$/.exec(spec);
|
|
15360
|
+
if (!match) continue;
|
|
15361
|
+
const target = path.resolve(unit.root, path.posix.dirname(relative), match[1]);
|
|
15362
|
+
let real;
|
|
15363
|
+
try {
|
|
15364
|
+
real = fs.realpathSync(target);
|
|
15365
|
+
if (!fs.statSync(real).isDirectory()) continue;
|
|
15366
|
+
} catch {
|
|
15367
|
+
continue;
|
|
15368
|
+
}
|
|
15369
|
+
if (!units.some((u) => isWithin(u.root, real))) targets.add(real);
|
|
15370
|
+
}
|
|
15371
|
+
}
|
|
15372
|
+
}
|
|
15373
|
+
return [...targets];
|
|
15374
|
+
}
|
|
15375
|
+
/**
|
|
15376
|
+
* The files a source upload carries, and where they are rooted.
|
|
15377
|
+
*
|
|
15378
|
+
* The project's repository first — or the project walked, outside git. Then
|
|
15379
|
+
* every repository a local `link:`/`file:` dependency reaches outside it, until
|
|
15380
|
+
* nothing new is reached, rooted together at their deepest common directory.
|
|
15381
|
+
* That is dadaki's shape: its Rebase project is a repository of its own, nested
|
|
15382
|
+
* inside the editor's repository (which ignores it), and its frontend links
|
|
15383
|
+
* `../../packages/editor` from there. A listing of the project's repository
|
|
15384
|
+
* alone rebuilt into "Rollup failed to resolve import @dadaki/editor".
|
|
15385
|
+
*
|
|
15386
|
+
* Paths are resolved through `realpath` first: git reports its top level with
|
|
15387
|
+
* symlinks resolved (`/private/var/…` for a macOS temp directory), and a
|
|
15388
|
+
* project path computed against the unresolved one would climb out of the
|
|
15389
|
+
* archive with `../`.
|
|
15390
|
+
*/
|
|
15391
|
+
function listSourceFiles(projectRoot, git = runGit) {
|
|
15392
|
+
const realRoot = fs.realpathSync(projectRoot);
|
|
15393
|
+
const units = [listUnit(realRoot, git)];
|
|
15394
|
+
for (let reached = externalLinkTargets(units); reached.length > 0; reached = externalLinkTargets(units)) for (const target of reached) {
|
|
15395
|
+
if (units.some((u) => isWithin(u.root, target))) continue;
|
|
15396
|
+
units.push(listUnit(target, git));
|
|
15397
|
+
}
|
|
15398
|
+
const root = commonAncestor(units.map((u) => u.root));
|
|
15399
|
+
const files = /* @__PURE__ */ new Set();
|
|
15400
|
+
for (const unit of units) {
|
|
15401
|
+
const prefix = toPosix(path.relative(root, unit.root));
|
|
15402
|
+
for (const file of unit.files) files.add(prefix ? `${prefix}/${file}` : file);
|
|
15403
|
+
}
|
|
15404
|
+
return {
|
|
15405
|
+
root,
|
|
15406
|
+
files: [...files].sort(),
|
|
15407
|
+
projectPath: toPosix(path.relative(root, realRoot)),
|
|
15408
|
+
fromGit: units.every((u) => u.fromGit)
|
|
15409
|
+
};
|
|
15410
|
+
}
|
|
15411
|
+
/**
|
|
15412
|
+
* Pack a listing into a gzipped tarball at `outPath`.
|
|
15413
|
+
*
|
|
15414
|
+
* The list goes to tar as a NUL-separated file (`--null -T`), so a path with a
|
|
15415
|
+
* space or a newline in it is one path. `--no-xattrs` and `COPYFILE_DISABLE`
|
|
15416
|
+
* keep macOS's extended attributes and AppleDouble `._*` sidecars out of the
|
|
15417
|
+
* archive, as `packBundle` does; GNU tar accepts both. Every path handed to tar
|
|
15418
|
+
* is absolute, because GNU tar resolves a `-T` file after it has applied `-C`.
|
|
15419
|
+
*/
|
|
15420
|
+
function packSource(listing, outPath) {
|
|
15421
|
+
const listPath = `${outPath}.files`;
|
|
15422
|
+
fs.writeFileSync(listPath, listing.files.map((file) => `${file}\0`).join(""));
|
|
15423
|
+
return new Promise((resolve, reject) => {
|
|
15424
|
+
const child = spawn("tar", [
|
|
15425
|
+
"-czf",
|
|
15426
|
+
path.resolve(outPath),
|
|
15427
|
+
"--no-xattrs",
|
|
15428
|
+
"--null",
|
|
15429
|
+
"-C",
|
|
15430
|
+
listing.root,
|
|
15431
|
+
"-T",
|
|
15432
|
+
path.resolve(listPath)
|
|
15433
|
+
], {
|
|
15434
|
+
stdio: [
|
|
15435
|
+
"ignore",
|
|
15436
|
+
"ignore",
|
|
15437
|
+
"pipe"
|
|
15438
|
+
],
|
|
15439
|
+
env: {
|
|
15440
|
+
...process.env,
|
|
15441
|
+
COPYFILE_DISABLE: "1"
|
|
15442
|
+
}
|
|
15443
|
+
});
|
|
15444
|
+
let stderr = "";
|
|
15445
|
+
child.stderr.on("data", (chunk) => stderr += String(chunk));
|
|
15446
|
+
child.on("error", reject);
|
|
15447
|
+
child.on("close", (code) => code === 0 ? resolve() : reject(new Error(stderr.trim() || `tar exited ${code}`)));
|
|
15448
|
+
}).finally(() => fs.rmSync(listPath, { force: true }));
|
|
15449
|
+
}
|
|
15450
|
+
/** Upload a source archive; returns the control plane's id for it. */
|
|
15451
|
+
async function uploadRebuildSource(url, token, projectId, tarPath) {
|
|
15452
|
+
const bytes = fs.readFileSync(tarPath);
|
|
15453
|
+
const res = await fetch(`${url}/api/functions/deploy/source/upload?projectId=${encodeURIComponent(projectId)}`, {
|
|
15454
|
+
method: "POST",
|
|
15455
|
+
headers: {
|
|
15456
|
+
Authorization: `Bearer ${token}`,
|
|
15457
|
+
"Content-Type": "application/gzip",
|
|
15458
|
+
"User-Agent": cliUserAgent()
|
|
15459
|
+
},
|
|
15460
|
+
body: bytes
|
|
15461
|
+
});
|
|
15462
|
+
if (!res.ok) {
|
|
15463
|
+
const body = await res.text().catch(() => "");
|
|
15464
|
+
throw new Error(`the upload was refused (${res.status}): ${body || res.statusText}`);
|
|
15465
|
+
}
|
|
15466
|
+
const data = await res.json();
|
|
15467
|
+
const sourceId = typeof data === "object" && data !== null && "sourceId" in data ? data.sourceId : void 0;
|
|
15468
|
+
if (typeof sourceId !== "string" || !/^[0-9a-f]{32}$/i.test(sourceId)) throw new Error("the control plane did not return a source id");
|
|
15469
|
+
return sourceId;
|
|
15470
|
+
}
|
|
15471
|
+
/** The env files Vite reads for a production build, lowest precedence first. */
|
|
15472
|
+
var PRODUCTION_ENV_FILES = [
|
|
15473
|
+
".env",
|
|
15474
|
+
".env.local",
|
|
15475
|
+
".env.production",
|
|
15476
|
+
".env.production.local"
|
|
15477
|
+
];
|
|
15478
|
+
/**
|
|
15479
|
+
* The `VITE_*` values a rebuild of this project's frontend needs.
|
|
15480
|
+
*
|
|
15481
|
+
* The `.env` files are never uploaded, so the values a static app is built with
|
|
15482
|
+
* have to travel separately — and only the `VITE_*` ones, which Vite inlines
|
|
15483
|
+
* into the client bundle and are public by construction. Nothing else is read.
|
|
15484
|
+
*
|
|
15485
|
+
* Read the way Vite reads them for `vite build`: `.env`, `.env.local`,
|
|
15486
|
+
* `.env.production`, `.env.production.local`, later winning — from the project
|
|
15487
|
+
* root, then from each static app's own `root`, which is where its Vite config
|
|
15488
|
+
* usually points. `process.env` wins over every file, as it does in Vite.
|
|
15489
|
+
*
|
|
15490
|
+
* `VITE_API_URL` is the one exception, taken from `process.env` only.
|
|
15491
|
+
* `staticBuildEnv` sets it to `process.env.VITE_API_URL ?? ""` for every
|
|
15492
|
+
* static build, so a value in a `.env` file never reaches a deployed bundle —
|
|
15493
|
+
* it is the `http://localhost:3001` of local development. Sending it would make
|
|
15494
|
+
* the rebuild bake in exactly the address the local build refuses to.
|
|
15495
|
+
*/
|
|
15496
|
+
function collectBuildEnv(projectRoot, appRoots, env = process.env) {
|
|
15497
|
+
const collected = {};
|
|
15498
|
+
const seen = /* @__PURE__ */ new Set();
|
|
15499
|
+
for (const dir of [projectRoot, ...appRoots.map((root) => path.resolve(projectRoot, root))]) {
|
|
15500
|
+
const resolved = path.resolve(dir);
|
|
15501
|
+
if (seen.has(resolved) || !fs.existsSync(resolved)) continue;
|
|
15502
|
+
seen.add(resolved);
|
|
15503
|
+
for (const name of PRODUCTION_ENV_FILES) {
|
|
15504
|
+
let parsed;
|
|
15505
|
+
try {
|
|
15506
|
+
parsed = dotenv.parse(fs.readFileSync(path.join(resolved, name), "utf8"));
|
|
15507
|
+
} catch {
|
|
15508
|
+
continue;
|
|
15509
|
+
}
|
|
15510
|
+
for (const [key, value] of Object.entries(parsed)) if (key.startsWith("VITE_")) collected[key] = value;
|
|
15511
|
+
}
|
|
15512
|
+
}
|
|
15513
|
+
delete collected.VITE_API_URL;
|
|
15514
|
+
for (const [key, value] of Object.entries(env)) if (key.startsWith("VITE_") && value !== void 0) collected[key] = value;
|
|
15515
|
+
return collected;
|
|
15516
|
+
}
|
|
15517
|
+
/** The `root` of every static app a manifest declares, in declaration order. */
|
|
15518
|
+
function staticAppRoots(manifest) {
|
|
15519
|
+
if (!manifest) return [];
|
|
15520
|
+
const roots = [];
|
|
15521
|
+
for (const app of Object.values(manifest.apps)) if (app.type === "static" && typeof app.root === "string" && app.root !== "") roots.push(app.root);
|
|
15522
|
+
return roots;
|
|
15523
|
+
}
|
|
15524
|
+
var DEFAULT_STEPS = {
|
|
15525
|
+
list: (projectRoot) => listSourceFiles(projectRoot),
|
|
15526
|
+
pack: packSource,
|
|
15527
|
+
upload: uploadRebuildSource
|
|
15528
|
+
};
|
|
15529
|
+
/**
|
|
15530
|
+
* List, pack and upload the project's source. Never throws.
|
|
15531
|
+
*
|
|
15532
|
+
* Returns what the trigger should carry, or null — after a warning saying why
|
|
15533
|
+
* and what it costs — when the source could not be sent. The caller deploys
|
|
15534
|
+
* either way.
|
|
15535
|
+
*/
|
|
15536
|
+
async function prepareRebuildSource(opts) {
|
|
15537
|
+
const steps = {
|
|
15538
|
+
...DEFAULT_STEPS,
|
|
15539
|
+
...opts.steps
|
|
15540
|
+
};
|
|
15541
|
+
const maxBytes = opts.maxBytes ?? 104857600;
|
|
15542
|
+
const tarPath = path.join(os.tmpdir(), `rebase-rebuild-src-${process.pid}-${Date.now()}.tar.gz`);
|
|
15543
|
+
const cost = "Platform upgrades will not rebuild this project until a later deploy uploads its source.";
|
|
15544
|
+
try {
|
|
15545
|
+
const listing = steps.list(opts.projectRoot);
|
|
15546
|
+
await steps.pack(listing, tarPath);
|
|
15547
|
+
const size = fs.statSync(tarPath).size;
|
|
15548
|
+
const mb = (bytes) => (bytes / 1024 / 1024).toFixed(1);
|
|
15549
|
+
if (size > maxBytes) {
|
|
15550
|
+
opts.warn(`The project source is ${mb(size)} MB compressed, over the ${Math.round(maxBytes / 1024 / 1024)} MB upload limit, so it was not uploaded. ${cost}`, listing.fromGit ? "Stop tracking large assets and build output in git, or pass --no-source to skip it on purpose." : "Initialise a git repository with a .gitignore to choose what is source, or pass --no-source.");
|
|
15551
|
+
return null;
|
|
15552
|
+
}
|
|
15553
|
+
opts.progress(` Uploading source (${listing.files.length} files, ${mb(size)} MB)...`);
|
|
15554
|
+
return {
|
|
15555
|
+
sourceId: await steps.upload(opts.url, opts.token, opts.projectId, tarPath),
|
|
15556
|
+
projectPath: listing.projectPath,
|
|
15557
|
+
buildEnv: collectBuildEnv(opts.projectRoot, staticAppRoots(opts.manifest))
|
|
15558
|
+
};
|
|
15559
|
+
} catch (err) {
|
|
15560
|
+
opts.warn(`The project source was not uploaded: ${err instanceof Error ? err.message : String(err)}. ${cost}`, "The deploy continues with the bundle alone.");
|
|
15561
|
+
return null;
|
|
15562
|
+
} finally {
|
|
15563
|
+
fs.rmSync(tarPath, { force: true });
|
|
15564
|
+
}
|
|
15565
|
+
}
|
|
15566
|
+
//#endregion
|
|
15164
15567
|
//#region src/commands/cloud/deploy.ts
|
|
15165
15568
|
var POLL_INTERVAL_MS = 1500;
|
|
15166
15569
|
var POLL_TIMEOUT_MS = 900 * 1e3;
|
|
15167
|
-
var MAX_SOURCE_UPLOAD_BYTES = 100 * 1024 * 1024;
|
|
15168
15570
|
function sleep(ms) {
|
|
15169
15571
|
return new Promise((r) => setTimeout(r, ms));
|
|
15170
15572
|
}
|
|
@@ -15256,7 +15658,7 @@ function progress(line) {
|
|
|
15256
15658
|
async function uploadSource(url, token, projectId, tarPath) {
|
|
15257
15659
|
const bytes = fs.readFileSync(tarPath);
|
|
15258
15660
|
const sizeMb = (bytes.length / 1024 / 1024).toFixed(1);
|
|
15259
|
-
if (bytes.length >
|
|
15661
|
+
if (bytes.length > 104857600) fail(`Source context is ${sizeMb} MB — the upload cap is ${Math.round(MAX_SOURCE_UPLOAD_BYTES / 1024 / 1024)} MB.`, "Trim the build context: exclude sourcemaps (*.map), build output and large assets via .rebaseignore or .gitignore.");
|
|
15260
15662
|
progress(chalk.gray(` Uploading source (${sizeMb} MB)...`));
|
|
15261
15663
|
const res = await fetch(`${url}/api/functions/deploy/upload?projectId=${encodeURIComponent(projectId)}`, {
|
|
15262
15664
|
method: "POST",
|
|
@@ -15303,6 +15705,7 @@ async function deployBundle(opts) {
|
|
|
15303
15705
|
bundleDir = staticDir;
|
|
15304
15706
|
await uploadAndTrigger({
|
|
15305
15707
|
...opts,
|
|
15708
|
+
projectRoot,
|
|
15306
15709
|
bundleDir,
|
|
15307
15710
|
appName: target.name
|
|
15308
15711
|
});
|
|
@@ -15338,6 +15741,7 @@ async function deployBundle(opts) {
|
|
|
15338
15741
|
}
|
|
15339
15742
|
await uploadAndTrigger({
|
|
15340
15743
|
...opts,
|
|
15744
|
+
projectRoot,
|
|
15341
15745
|
bundleDir
|
|
15342
15746
|
});
|
|
15343
15747
|
}
|
|
@@ -15352,7 +15756,7 @@ async function deployBundle(opts) {
|
|
|
15352
15756
|
* bundle with no site in it.
|
|
15353
15757
|
*/
|
|
15354
15758
|
async function uploadAndTrigger(opts) {
|
|
15355
|
-
const { client, url, projectId, projectRef, bundleDir } = opts;
|
|
15759
|
+
const { client, url, projectId, projectRef, projectRoot, bundleDir } = opts;
|
|
15356
15760
|
const manifest = readBundleManifest(bundleDir);
|
|
15357
15761
|
if (manifest.hooks?.native) {
|
|
15358
15762
|
const names = (manifest.hooks.nativeModules ?? []).map((m) => m.name).join(", ");
|
|
@@ -15377,10 +15781,22 @@ async function uploadAndTrigger(opts) {
|
|
|
15377
15781
|
console.log("");
|
|
15378
15782
|
console.log(` 🚀 Triggering managed deployment for ${chalk.bold(projectRef)} (schema ${manifest.schemaVersion})...`);
|
|
15379
15783
|
}
|
|
15380
|
-
let
|
|
15784
|
+
let projectManifest;
|
|
15381
15785
|
try {
|
|
15382
|
-
|
|
15786
|
+
projectManifest = loadManifest(projectRoot).manifest;
|
|
15383
15787
|
} catch {}
|
|
15788
|
+
const declaredApps = declaredAppsFrom(projectManifest);
|
|
15789
|
+
let rebuildSource = null;
|
|
15790
|
+
if (opts.uploadSource && manifest.kind !== "static" && await platformRebuildsOff(client, projectId)) progress(chalk.gray(" Platform rebuilds are off for this project, so its source is not uploaded (turn them on with `rebase cloud settings set --platform-rebuilds on`)."));
|
|
15791
|
+
else if (opts.uploadSource && manifest.kind !== "static") rebuildSource = await prepareRebuildSource({
|
|
15792
|
+
projectRoot,
|
|
15793
|
+
url,
|
|
15794
|
+
token,
|
|
15795
|
+
projectId,
|
|
15796
|
+
manifest: projectManifest,
|
|
15797
|
+
progress: (line) => progress(chalk.gray(line)),
|
|
15798
|
+
warn
|
|
15799
|
+
});
|
|
15384
15800
|
const body = bundleDeployBody({
|
|
15385
15801
|
projectId,
|
|
15386
15802
|
bundleId,
|
|
@@ -15388,7 +15804,9 @@ async function uploadAndTrigger(opts) {
|
|
|
15388
15804
|
app: opts.appName,
|
|
15389
15805
|
message: opts.message,
|
|
15390
15806
|
declaredApps,
|
|
15391
|
-
commit: bundleCommit(
|
|
15807
|
+
commit: bundleCommit(projectRoot),
|
|
15808
|
+
rebuildSource,
|
|
15809
|
+
allowFrameworkDowngrade: opts.allowDowngrade
|
|
15392
15810
|
});
|
|
15393
15811
|
let deploymentId;
|
|
15394
15812
|
let managed;
|
|
@@ -15398,6 +15816,8 @@ async function uploadAndTrigger(opts) {
|
|
|
15398
15816
|
deploymentId = String(res.deployment.id);
|
|
15399
15817
|
managed = res.managed === true;
|
|
15400
15818
|
} catch (e) {
|
|
15819
|
+
const refusal = intakeRefusal(e);
|
|
15820
|
+
if (refusal) fail(refusal.message, refusal.hint, refusal.code);
|
|
15401
15821
|
reportError(e, "Managed deploy failed to start");
|
|
15402
15822
|
}
|
|
15403
15823
|
if (!opts.follow) {
|
|
@@ -15408,6 +15828,7 @@ async function uploadAndTrigger(opts) {
|
|
|
15408
15828
|
success: true,
|
|
15409
15829
|
deploymentId,
|
|
15410
15830
|
managed,
|
|
15831
|
+
sourceUploaded: rebuildSource !== null,
|
|
15411
15832
|
following: false
|
|
15412
15833
|
});
|
|
15413
15834
|
return;
|
|
@@ -15427,6 +15848,7 @@ async function uploadAndTrigger(opts) {
|
|
|
15427
15848
|
success: true,
|
|
15428
15849
|
deploymentId,
|
|
15429
15850
|
managed,
|
|
15851
|
+
sourceUploaded: rebuildSource !== null,
|
|
15430
15852
|
following: true,
|
|
15431
15853
|
status
|
|
15432
15854
|
});
|
|
@@ -15696,6 +16118,8 @@ var DEPLOY_FLAGS = {
|
|
|
15696
16118
|
"--bundle-dir": String,
|
|
15697
16119
|
"--skip-type-check": Boolean,
|
|
15698
16120
|
"--eject": Boolean,
|
|
16121
|
+
"--no-source": Boolean,
|
|
16122
|
+
"--allow-downgrade": Boolean,
|
|
15699
16123
|
"-m": "--message"
|
|
15700
16124
|
};
|
|
15701
16125
|
/**
|
|
@@ -15760,6 +16184,7 @@ async function deployCommand(rawArgs, projectRef) {
|
|
|
15760
16184
|
const startedAt = Date.now();
|
|
15761
16185
|
const { flags: args, appName } = resolveDeployArgs(rawArgs);
|
|
15762
16186
|
if (args["--wait"] && args["--no-follow"]) fail("--wait and --no-follow ask for opposite things.", "`deploy` follows by default — pass neither, or `--no-follow` to return as soon as the build is triggered.", "usage");
|
|
16187
|
+
if (args["--source"] && args["--no-source"]) fail("--source and --no-source ask for opposite things.", "`--source <path>` builds a container image from a directory; `--no-source` skips uploading the source beside a managed bundle. Pass one.", "usage");
|
|
15763
16188
|
const { client, url } = await requireClient(rawArgs);
|
|
15764
16189
|
const projectId = await resolveProjectRef(projectRef, client);
|
|
15765
16190
|
await requirePaymentMethod(client, projectId);
|
|
@@ -15777,7 +16202,9 @@ async function deployCommand(rawArgs, projectRef) {
|
|
|
15777
16202
|
appName,
|
|
15778
16203
|
skipTypeCheck: args["--skip-type-check"] === true,
|
|
15779
16204
|
follow: args["--no-follow"] !== true,
|
|
15780
|
-
timeoutMs: resolveDeployTimeout(args["--timeout"])
|
|
16205
|
+
timeoutMs: resolveDeployTimeout(args["--timeout"]),
|
|
16206
|
+
uploadSource: args["--no-source"] !== true,
|
|
16207
|
+
allowDowngrade: args["--allow-downgrade"] === true
|
|
15781
16208
|
});
|
|
15782
16209
|
return;
|
|
15783
16210
|
}
|
|
@@ -15909,10 +16336,62 @@ function resolveTriggerFailure(e) {
|
|
|
15909
16336
|
fail(blocking?.id ? `Deployment ${blocking.id} is already in progress for this project${blocking.triggerSource && blocking.triggerSource !== "unknown" ? `, triggered from the ${blocking.triggerSource}` : ""}${blocking.createdAt ? ` at ${fmtDate(blocking.createdAt)}` : ""}.` : "A deployment is already in progress for this project.", blocking?.id ? `Follow it with \`rebase cloud logs -f\`, or stop it with \`rebase cloud cancel ${blocking.id}\`.` : "Follow it with `rebase cloud logs -f`.", "deploy_in_progress");
|
|
15910
16337
|
}
|
|
15911
16338
|
if (err?.status === 402) fail(err.message || "Payment required before deploying.", "Attach a card once with `rebase cloud billing setup`, then deploy again.", "payment_required");
|
|
15912
|
-
|
|
16339
|
+
const refusal = intakeRefusal(e);
|
|
16340
|
+
if (refusal) fail(refusal.message, refusal.hint, refusal.code);
|
|
15913
16341
|
reportError(e, "Failed to trigger deployment");
|
|
15914
16342
|
}
|
|
15915
16343
|
/**
|
|
16344
|
+
* Whether the project's owner turned platform rebuilds off.
|
|
16345
|
+
*
|
|
16346
|
+
* Only an explicit `false` counts: a project row that predates the switch, or
|
|
16347
|
+
* one that could not be read, is on — which is the default, and the control
|
|
16348
|
+
* plane refuses an upload it should not have anyway.
|
|
16349
|
+
*/
|
|
16350
|
+
async function platformRebuildsOff(client, projectId) {
|
|
16351
|
+
try {
|
|
16352
|
+
const row = await client.data.collection("projects").findById(projectId);
|
|
16353
|
+
return typeof row === "object" && row !== null && "platformRebuilds" in row && row.platformRebuilds === false;
|
|
16354
|
+
} catch {
|
|
16355
|
+
return false;
|
|
16356
|
+
}
|
|
16357
|
+
}
|
|
16358
|
+
/**
|
|
16359
|
+
* An intake refusal, which is a decision about what was deployed rather than a
|
|
16360
|
+
* transport error — or undefined for anything else.
|
|
16361
|
+
*
|
|
16362
|
+
* It carries a stable code in `details.intakeCode` and usually a remedy in
|
|
16363
|
+
* `details.hint`, and both used to be discarded: the deploy printed `Failed to
|
|
16364
|
+
* trigger deployment (400): …` and threw the hint away. The managed path, where
|
|
16365
|
+
* bundles are refused, never looked for either. Both paths answer through this
|
|
16366
|
+
* now, in the three-argument shape `fail` takes, so `--json` carries the code
|
|
16367
|
+
* and a person sees the fix.
|
|
16368
|
+
*
|
|
16369
|
+
* A downgrade gets one more line, because its two remedies are both in this
|
|
16370
|
+
* CLI: move the project forward, or say the step back is meant.
|
|
16371
|
+
*/
|
|
16372
|
+
function intakeRefusal(e) {
|
|
16373
|
+
if (typeof e !== "object" || e === null) return void 0;
|
|
16374
|
+
const status = "status" in e && typeof e.status === "number" ? e.status : void 0;
|
|
16375
|
+
const details = "details" in e && typeof e.details === "object" && e.details !== null ? e.details : void 0;
|
|
16376
|
+
const code = details && "intakeCode" in details && typeof details.intakeCode === "string" ? details.intakeCode : void 0;
|
|
16377
|
+
if (status === void 0 || status < 400 || status >= 500 || !code) return void 0;
|
|
16378
|
+
const message = "message" in e && typeof e.message === "string" && e.message !== "" ? e.message : "This bundle was refused.";
|
|
16379
|
+
const serverHint = details && "hint" in details && typeof details.hint === "string" && details.hint !== "" ? details.hint : void 0;
|
|
16380
|
+
if (code === "FRAMEWORK_DOWNGRADE") {
|
|
16381
|
+
const remedy = "Run `rebase upgrade` to move this project's @rebasepro packages forward and deploy again, or pass `--allow-downgrade` to deploy the older release on purpose.";
|
|
16382
|
+
return {
|
|
16383
|
+
message,
|
|
16384
|
+
hint: serverHint ? `${serverHint}\n ${remedy}` : remedy,
|
|
16385
|
+
code
|
|
16386
|
+
};
|
|
16387
|
+
}
|
|
16388
|
+
return {
|
|
16389
|
+
message,
|
|
16390
|
+
hint: serverHint ?? "Run `rebase cloud compute` to see what this project reserves.",
|
|
16391
|
+
code
|
|
16392
|
+
};
|
|
16393
|
+
}
|
|
16394
|
+
/**
|
|
15916
16395
|
* Poll a deployment record and print new log output as it arrives. Returns the
|
|
15917
16396
|
* terminal status; a non-success still exits non-zero, as it always has.
|
|
15918
16397
|
*
|
|
@@ -17033,7 +17512,7 @@ function printExtensionsHelp() {
|
|
|
17033
17512
|
* `rebase cloud settings` — a project's editable configuration.
|
|
17034
17513
|
*
|
|
17035
17514
|
* settings Show the current settings
|
|
17036
|
-
* settings set [flags] Update name / branch / repo / subdomain
|
|
17515
|
+
* settings set [flags] Update name / branch / repo / subdomain / platform rebuilds
|
|
17037
17516
|
*
|
|
17038
17517
|
* These are plain `projects` updates. A subdomain change is validated against
|
|
17039
17518
|
* `check-subdomain` up front so the CLI fails with the real reason rather than a
|
|
@@ -17080,7 +17559,8 @@ async function showSettings(rawArgs) {
|
|
|
17080
17559
|
["Branch", p.gitBranch],
|
|
17081
17560
|
["Custom domain", p.customDomain],
|
|
17082
17561
|
["Provider", p.provider],
|
|
17083
|
-
["Region", p.region]
|
|
17562
|
+
["Region", p.region],
|
|
17563
|
+
["Platform rebuilds", p.platformRebuilds === false ? "off" : "on"]
|
|
17084
17564
|
]);
|
|
17085
17565
|
console.log("");
|
|
17086
17566
|
}, {
|
|
@@ -17091,12 +17571,20 @@ async function showSettings(rawArgs) {
|
|
|
17091
17571
|
gitBranch: p.gitBranch ?? null,
|
|
17092
17572
|
customDomain: p.customDomain ?? null,
|
|
17093
17573
|
provider: p.provider ?? null,
|
|
17094
|
-
region: p.region ?? null
|
|
17574
|
+
region: p.region ?? null,
|
|
17575
|
+
platformRebuilds: p.platformRebuilds !== false
|
|
17095
17576
|
});
|
|
17096
17577
|
} catch (e) {
|
|
17097
17578
|
reportError(e, "Failed to load settings");
|
|
17098
17579
|
}
|
|
17099
17580
|
}
|
|
17581
|
+
/** `on`/`off` (or `true`/`false`) as a boolean, or null for anything else. */
|
|
17582
|
+
function parseOnOff(value) {
|
|
17583
|
+
const v = value.trim().toLowerCase();
|
|
17584
|
+
if (v === "on" || v === "true") return true;
|
|
17585
|
+
if (v === "off" || v === "false") return false;
|
|
17586
|
+
return null;
|
|
17587
|
+
}
|
|
17100
17588
|
/** Build the update patch from the flags actually supplied (pure/testable). */
|
|
17101
17589
|
function buildSettingsPatch(args) {
|
|
17102
17590
|
const patch = {};
|
|
@@ -17104,6 +17592,7 @@ function buildSettingsPatch(args) {
|
|
|
17104
17592
|
if (args.subdomain !== void 0) patch.subdomain = args.subdomain.toLowerCase();
|
|
17105
17593
|
if (args.repo !== void 0) patch.gitRepoUrl = args.repo;
|
|
17106
17594
|
if (args.branch !== void 0) patch.gitBranch = args.branch;
|
|
17595
|
+
if (args.platformRebuilds !== void 0) patch.platformRebuilds = args.platformRebuilds;
|
|
17107
17596
|
return patch;
|
|
17108
17597
|
}
|
|
17109
17598
|
/** What `rebase cloud settings set` parses. Its page is the group's own. */
|
|
@@ -17111,7 +17600,8 @@ var SET_SETTINGS_FLAGS = {
|
|
|
17111
17600
|
"--name": String,
|
|
17112
17601
|
"--subdomain": String,
|
|
17113
17602
|
"--repo": String,
|
|
17114
|
-
"--branch": String
|
|
17603
|
+
"--branch": String,
|
|
17604
|
+
"--platform-rebuilds": String
|
|
17115
17605
|
};
|
|
17116
17606
|
async function setSettings(rawArgs) {
|
|
17117
17607
|
const { flags: args } = parseCloudArgs({
|
|
@@ -17124,15 +17614,22 @@ async function setSettings(rawArgs) {
|
|
|
17124
17614
|
const { client } = await requireClient(rawArgs);
|
|
17125
17615
|
const projectId = await requireProject(rawArgs, client);
|
|
17126
17616
|
const projectRef = displayProjectRef(rawArgs);
|
|
17617
|
+
let platformRebuilds;
|
|
17618
|
+
if (args["--platform-rebuilds"] !== void 0) {
|
|
17619
|
+
const parsed = parseOnOff(args["--platform-rebuilds"]);
|
|
17620
|
+
if (parsed === null) fail(`--platform-rebuilds takes on or off (got "${args["--platform-rebuilds"]}").`, void 0, "usage");
|
|
17621
|
+
platformRebuilds = parsed;
|
|
17622
|
+
}
|
|
17127
17623
|
const patch = buildSettingsPatch({
|
|
17128
17624
|
name: args["--name"],
|
|
17129
17625
|
subdomain: args["--subdomain"],
|
|
17130
17626
|
repo: args["--repo"],
|
|
17131
|
-
branch: args["--branch"]
|
|
17627
|
+
branch: args["--branch"],
|
|
17628
|
+
platformRebuilds
|
|
17132
17629
|
});
|
|
17133
|
-
if (Object.keys(patch).length === 0) fail("Nothing to update.", "Pass --name, --subdomain, --repo, or --
|
|
17630
|
+
if (Object.keys(patch).length === 0) fail("Nothing to update.", "Pass --name, --subdomain, --repo, --branch, or --platform-rebuilds.", "usage");
|
|
17134
17631
|
try {
|
|
17135
|
-
if (patch.subdomain) {
|
|
17632
|
+
if (typeof patch.subdomain === "string" && patch.subdomain) {
|
|
17136
17633
|
const check = await client.functions.invoke("check-subdomain", { subdomain: patch.subdomain }).catch(() => void 0);
|
|
17137
17634
|
if (check && !check.available) fail(`Subdomain "${patch.subdomain}" is not available${check.reason ? ` (${check.reason})` : ""}.`, void 0, "subdomain_taken");
|
|
17138
17635
|
}
|
|
@@ -17152,7 +17649,7 @@ function printSettingsHelp() {
|
|
|
17152
17649
|
title: "Project configuration",
|
|
17153
17650
|
actions: [{
|
|
17154
17651
|
action: "show",
|
|
17155
|
-
description: "Name, subdomain, repository and
|
|
17652
|
+
description: "Name, subdomain, repository, branch and platform rebuilds as recorded"
|
|
17156
17653
|
}, {
|
|
17157
17654
|
action: "set",
|
|
17158
17655
|
description: "Change one or more of them",
|
|
@@ -17160,7 +17657,8 @@ function printSettingsHelp() {
|
|
|
17160
17657
|
["--name <name>", "Display name"],
|
|
17161
17658
|
["--subdomain <sub>", "The <slug>.rebase.website host"],
|
|
17162
17659
|
["--repo <git url>", "Repository to build from"],
|
|
17163
|
-
["--branch <branch>", "Branch to build"]
|
|
17660
|
+
["--branch <branch>", "Branch to build"],
|
|
17661
|
+
["--platform-rebuilds <on|off>", "Rebuild this app on new framework releases (default on)"]
|
|
17164
17662
|
]
|
|
17165
17663
|
}]
|
|
17166
17664
|
});
|
|
@@ -18577,6 +19075,8 @@ var ACTION_HELP = {
|
|
|
18577
19075
|
["--bundle-dir <path>", "Deploy a bundle that is already built"],
|
|
18578
19076
|
["--source <path>", "Upload this directory and build a container image from it"],
|
|
18579
19077
|
["--skip-type-check", "Compile without type checking, as `rebase build` does"],
|
|
19078
|
+
["--no-source", "Do not upload the project's source beside a backend bundle"],
|
|
19079
|
+
["--allow-downgrade", "Deploy a bundle built on an older framework release than the project runs"],
|
|
18580
19080
|
["--eject", "Leave the managed runtime on purpose (ejects to a container image)"]
|
|
18581
19081
|
],
|
|
18582
19082
|
examples: [
|
|
@@ -18584,7 +19084,12 @@ var ACTION_HELP = {
|
|
|
18584
19084
|
"rebase cloud deploy web --message \"add search\"",
|
|
18585
19085
|
"rebase cloud deploy --timeout 300 --json"
|
|
18586
19086
|
],
|
|
18587
|
-
notes: [
|
|
19087
|
+
notes: [
|
|
19088
|
+
"A project whose rebase.json declares runtime: managed deploys a bundle without --bundle.",
|
|
19089
|
+
"--source on a managed project is refused: it would swap the project onto a container image.",
|
|
19090
|
+
"A backend bundle deploy also uploads the project's source (what git tracks, never a .env), so a platform upgrade can rebuild it on a newer release. If it is over 100 MB or the upload fails, the deploy goes on without it and says so.",
|
|
19091
|
+
"A bundle older than the release the project runs is refused as FRAMEWORK_DOWNGRADE. Run `rebase upgrade` and deploy again, or pass --allow-downgrade."
|
|
19092
|
+
]
|
|
18588
19093
|
},
|
|
18589
19094
|
logs: {
|
|
18590
19095
|
command: "cloud logs",
|
|
@@ -19444,7 +19949,7 @@ function printCloudHelp() {
|
|
|
19444
19949
|
* a mobile app with no repo relationship at all — an ordinary thing rather than
|
|
19445
19950
|
* a special case.
|
|
19446
19951
|
*/
|
|
19447
|
-
function printHelp$
|
|
19952
|
+
function printHelp$2() {
|
|
19448
19953
|
console.log(`
|
|
19449
19954
|
${chalk.bold("rebase apps")} — the apps this repository contributes
|
|
19450
19955
|
|
|
@@ -19467,7 +19972,7 @@ var APPS_SUBCOMMANDS = [
|
|
|
19467
19972
|
];
|
|
19468
19973
|
async function appsCommand(subcommand, rawArgs = []) {
|
|
19469
19974
|
if (!subcommand || subcommand === "--help" || wantsHelp(rawArgs)) {
|
|
19470
|
-
printHelp$
|
|
19975
|
+
printHelp$2();
|
|
19471
19976
|
return;
|
|
19472
19977
|
}
|
|
19473
19978
|
const { flags: args, positionals } = parseCommandArgs({
|
|
@@ -19629,6 +20134,1000 @@ function loadManifestOrExit(projectRoot, asJson = false) {
|
|
|
19629
20134
|
}
|
|
19630
20135
|
}
|
|
19631
20136
|
//#endregion
|
|
20137
|
+
//#region src/upgrade.ts
|
|
20138
|
+
/**
|
|
20139
|
+
* Moving a project's `@rebasepro/*` pins to one release.
|
|
20140
|
+
*
|
|
20141
|
+
* Every `@rebasepro/*` package ships at one version, and a project declares them
|
|
20142
|
+
* in several `package.json` files — the root, `backend/`, `frontend/`,
|
|
20143
|
+
* `config/`, and whatever else the repository holds. Bumping them by hand means
|
|
20144
|
+
* finding every one, keeping each range's `^` or `~`, and noticing the override
|
|
20145
|
+
* that quietly wins over all of them. `rebase upgrade` is that job, done the same
|
|
20146
|
+
* way every time; the control plane runs it too, when it rebuilds a managed
|
|
20147
|
+
* project on a newer release.
|
|
20148
|
+
*
|
|
20149
|
+
* Everything here is pure file work, so it can be tested against a temporary
|
|
20150
|
+
* directory: which specs move, the rewrite itself, and what overrides say. The
|
|
20151
|
+
* command in `commands/upgrade.ts` adds the registry lookup, the install and the
|
|
20152
|
+
* printing.
|
|
20153
|
+
*
|
|
20154
|
+
* ## The rewrite is textual
|
|
20155
|
+
*
|
|
20156
|
+
* A `package.json` is authored. Parsing it and writing `JSON.stringify` back
|
|
20157
|
+
* reformats the file, reorders nothing but reindents everything, and drops a
|
|
20158
|
+
* trailing newline or a two-space indent somebody chose — a one-line version
|
|
20159
|
+
* bump that arrives as a whole-file diff. So the file is scanned for the exact
|
|
20160
|
+
* byte range of each value, only those bytes are replaced, and the result is
|
|
20161
|
+
* parsed again and compared against what was intended. A file whose rewrite
|
|
20162
|
+
* does not parse back to exactly the intended values is not written.
|
|
20163
|
+
*/
|
|
20164
|
+
/** The dependency blocks a pin is moved in. `peerDependencies` is not one. */
|
|
20165
|
+
var PIN_FIELDS = [
|
|
20166
|
+
"dependencies",
|
|
20167
|
+
"devDependencies",
|
|
20168
|
+
"optionalDependencies"
|
|
20169
|
+
];
|
|
20170
|
+
/** The package whose published versions stand for the release as a whole. */
|
|
20171
|
+
var RELEASE_PACKAGE = "@rebasepro/cli";
|
|
20172
|
+
/** `MAJOR.MINOR.PATCH`, with an optional prerelease and build, and nothing else. */
|
|
20173
|
+
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-]+)*)?$/;
|
|
20174
|
+
/** Whether `value` is one exact version — `0.21.0`, `0.21.1-canary.g8c5a265`. */
|
|
20175
|
+
function isExactVersion(value) {
|
|
20176
|
+
return SEMVER.test(value);
|
|
20177
|
+
}
|
|
20178
|
+
/** An error that carries the `--json` envelope's code and a remedy. */
|
|
20179
|
+
var UpgradeError = class extends Error {
|
|
20180
|
+
code;
|
|
20181
|
+
hint;
|
|
20182
|
+
constructor(message, code, hint) {
|
|
20183
|
+
super(message);
|
|
20184
|
+
this.code = code;
|
|
20185
|
+
this.hint = hint;
|
|
20186
|
+
this.name = "UpgradeError";
|
|
20187
|
+
}
|
|
20188
|
+
};
|
|
20189
|
+
/**
|
|
20190
|
+
* What a dependency spec is, as far as moving it goes.
|
|
20191
|
+
*
|
|
20192
|
+
* Only a plain version is moved, because only a plain version has an obvious
|
|
20193
|
+
* answer: `^0.19.1` becomes `^0.21.0`. A `>=` floor, a `||` union or a partial
|
|
20194
|
+
* `^0.19` could each be rewritten several ways, and a tag or a workspace link
|
|
20195
|
+
* already means something other than "this release" — so each is reported with
|
|
20196
|
+
* its reason and left as written.
|
|
20197
|
+
*/
|
|
20198
|
+
function classifySpec(spec) {
|
|
20199
|
+
const movable = /^([\^~]?)(.+)$/.exec(spec);
|
|
20200
|
+
if (movable && isExactVersion(movable[2])) return {
|
|
20201
|
+
kind: "movable",
|
|
20202
|
+
prefix: movable[1],
|
|
20203
|
+
version: movable[2]
|
|
20204
|
+
};
|
|
20205
|
+
for (const protocol of [
|
|
20206
|
+
"link:",
|
|
20207
|
+
"file:",
|
|
20208
|
+
"portal:"
|
|
20209
|
+
]) if (spec.startsWith(protocol)) return {
|
|
20210
|
+
kind: "local",
|
|
20211
|
+
protocol
|
|
20212
|
+
};
|
|
20213
|
+
return {
|
|
20214
|
+
kind: "other",
|
|
20215
|
+
reason: otherReason(spec)
|
|
20216
|
+
};
|
|
20217
|
+
}
|
|
20218
|
+
function otherReason(spec) {
|
|
20219
|
+
const trimmed = spec.trim();
|
|
20220
|
+
if (trimmed.startsWith("workspace:")) return "a workspace: link, resolved inside this repository rather than from the registry";
|
|
20221
|
+
if (trimmed.startsWith("npm:")) return "an npm: alias; move the aliased version by hand";
|
|
20222
|
+
if (/^(git\+|git:|github:|gitlab:|bitbucket:|git@)/.test(trimmed) || /^https?:\/\//.test(trimmed)) return "a git or URL dependency, not a registry version";
|
|
20223
|
+
if (trimmed === "" || trimmed === "*" || /^[xX]$/.test(trimmed)) return "a wildcard, which already takes whatever the registry has";
|
|
20224
|
+
if (trimmed.includes("||")) return "a union of ranges, with no single version to move";
|
|
20225
|
+
if (/^[<>=]/.test(trimmed) || /\s-\s/.test(trimmed)) return "a comparator range, with no single version to move";
|
|
20226
|
+
if (/^[\^~]?v?\d/.test(trimmed)) return "not a plain MAJOR.MINOR.PATCH version";
|
|
20227
|
+
return "a dist-tag, not a version";
|
|
20228
|
+
}
|
|
20229
|
+
/**
|
|
20230
|
+
* The `@rebasepro/*` package an override key targets, or null.
|
|
20231
|
+
*
|
|
20232
|
+
* pnpm and npm key an override by name, optionally narrowed: `@rebasepro/x@<1`,
|
|
20233
|
+
* `parent>@rebasepro/x`; yarn's `resolutions` by a path, `**\/@rebasepro/x`. The
|
|
20234
|
+
* target is the last segment either way.
|
|
20235
|
+
*/
|
|
20236
|
+
function overrideTarget(key) {
|
|
20237
|
+
const match = /(?:^|[/>])(@rebasepro\/[^/@>\s]+)(?:@[^/>]*)?$/.exec(key);
|
|
20238
|
+
return match ? match[1] : null;
|
|
20239
|
+
}
|
|
20240
|
+
/**
|
|
20241
|
+
* Directories the walk never enters: installed packages, build output (`dist`,
|
|
20242
|
+
* `dist-bundle`, `dist-bundle-admin`, …) and anything hidden (`.git`,
|
|
20243
|
+
* `.rebase`, an editor's worktrees). None of them hold a manifest anybody
|
|
20244
|
+
* authored for this project, and `dist-bundle/package.json` in particular is a
|
|
20245
|
+
* generated copy that `rebase build` rewrites anyway.
|
|
20246
|
+
*/
|
|
20247
|
+
function skipDirectory(name) {
|
|
20248
|
+
return name === "node_modules" || name.startsWith("dist") || name.startsWith(".");
|
|
20249
|
+
}
|
|
20250
|
+
/** Every `package.json` and `pnpm-workspace.yaml` under the project root. */
|
|
20251
|
+
function discoverProjectFiles(projectRoot) {
|
|
20252
|
+
const found = {
|
|
20253
|
+
packageJsons: [],
|
|
20254
|
+
workspaceYamls: []
|
|
20255
|
+
};
|
|
20256
|
+
const walk = (dir) => {
|
|
20257
|
+
let entries;
|
|
20258
|
+
try {
|
|
20259
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
20260
|
+
} catch {
|
|
20261
|
+
return;
|
|
20262
|
+
}
|
|
20263
|
+
entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
20264
|
+
for (const entry of entries) {
|
|
20265
|
+
const full = path.join(dir, entry.name);
|
|
20266
|
+
if (entry.isDirectory()) {
|
|
20267
|
+
if (!skipDirectory(entry.name)) walk(full);
|
|
20268
|
+
} else if (entry.isFile()) {
|
|
20269
|
+
if (entry.name === "package.json") found.packageJsons.push(full);
|
|
20270
|
+
else if (entry.name === "pnpm-workspace.yaml") found.workspaceYamls.push(full);
|
|
20271
|
+
}
|
|
20272
|
+
}
|
|
20273
|
+
};
|
|
20274
|
+
walk(projectRoot);
|
|
20275
|
+
return found;
|
|
20276
|
+
}
|
|
20277
|
+
/** Project-relative, POSIX. What every file path in the output looks like. */
|
|
20278
|
+
function relativeTo(projectRoot, absolute) {
|
|
20279
|
+
return path.relative(projectRoot, absolute).split(path.sep).join("/");
|
|
20280
|
+
}
|
|
20281
|
+
/**
|
|
20282
|
+
* Work out every change the upgrade makes, without writing anything.
|
|
20283
|
+
*
|
|
20284
|
+
* `--dry-run` prints this; a real run writes `writes` and nothing else.
|
|
20285
|
+
*/
|
|
20286
|
+
function planUpgrade(projectRoot, target, options = {}) {
|
|
20287
|
+
if (!isExactVersion(target)) throw new UpgradeError(`"${target}" is not an exact version.`, "target_invalid");
|
|
20288
|
+
const plan = {
|
|
20289
|
+
target,
|
|
20290
|
+
changed: [],
|
|
20291
|
+
skipped: [],
|
|
20292
|
+
overrides: [],
|
|
20293
|
+
unreadable: [],
|
|
20294
|
+
writes: []
|
|
20295
|
+
};
|
|
20296
|
+
const files = discoverProjectFiles(projectRoot);
|
|
20297
|
+
for (const absolute of files.packageJsons) {
|
|
20298
|
+
const file = relativeTo(projectRoot, absolute);
|
|
20299
|
+
const original = fs.readFileSync(absolute, "utf8");
|
|
20300
|
+
const content = planPackageJson(file, original, target, options, plan);
|
|
20301
|
+
if (content !== null && content !== original) plan.writes.push({
|
|
20302
|
+
file,
|
|
20303
|
+
absolute,
|
|
20304
|
+
content
|
|
20305
|
+
});
|
|
20306
|
+
}
|
|
20307
|
+
for (const absolute of files.workspaceYamls) {
|
|
20308
|
+
const file = relativeTo(projectRoot, absolute);
|
|
20309
|
+
const original = fs.readFileSync(absolute, "utf8");
|
|
20310
|
+
const content = planWorkspaceYaml(file, original, target, options, plan);
|
|
20311
|
+
if (content !== original) plan.writes.push({
|
|
20312
|
+
file,
|
|
20313
|
+
absolute,
|
|
20314
|
+
content
|
|
20315
|
+
});
|
|
20316
|
+
}
|
|
20317
|
+
return plan;
|
|
20318
|
+
}
|
|
20319
|
+
/** Write what the plan changes. Nothing else on disk is touched. */
|
|
20320
|
+
function applyUpgradePlan(plan) {
|
|
20321
|
+
for (const write of plan.writes) fs.writeFileSync(write.absolute, write.content, "utf8");
|
|
20322
|
+
}
|
|
20323
|
+
/** Override blocks in a `package.json`, as paths from the root object. */
|
|
20324
|
+
var JSON_OVERRIDE_BLOCKS = [
|
|
20325
|
+
["pnpm", "overrides"],
|
|
20326
|
+
["overrides"],
|
|
20327
|
+
["resolutions"]
|
|
20328
|
+
];
|
|
20329
|
+
function planPackageJson(file, original, target, options, plan) {
|
|
20330
|
+
const bom = original.startsWith("") ? "" : "";
|
|
20331
|
+
const text = original.slice(bom.length);
|
|
20332
|
+
let parsed;
|
|
20333
|
+
let tree;
|
|
20334
|
+
try {
|
|
20335
|
+
parsed = JSON.parse(text);
|
|
20336
|
+
tree = scanJson(text);
|
|
20337
|
+
} catch (err) {
|
|
20338
|
+
plan.unreadable.push({
|
|
20339
|
+
file,
|
|
20340
|
+
reason: `not valid JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
20341
|
+
});
|
|
20342
|
+
return null;
|
|
20343
|
+
}
|
|
20344
|
+
if (!isRecord(parsed) || tree.kind !== "object") {
|
|
20345
|
+
plan.unreadable.push({
|
|
20346
|
+
file,
|
|
20347
|
+
reason: "not a JSON object"
|
|
20348
|
+
});
|
|
20349
|
+
return null;
|
|
20350
|
+
}
|
|
20351
|
+
const expected = structuredClone(parsed);
|
|
20352
|
+
const edits = [];
|
|
20353
|
+
/** Override members to remove, as [block path, key]. */
|
|
20354
|
+
const removals = [];
|
|
20355
|
+
for (const field of PIN_FIELDS) {
|
|
20356
|
+
const block = memberValue(tree, [field]);
|
|
20357
|
+
if (block?.kind !== "object") continue;
|
|
20358
|
+
for (const member of block.members) {
|
|
20359
|
+
if (!member.key.startsWith("@rebasepro/")) continue;
|
|
20360
|
+
if (member.value.kind !== "string") {
|
|
20361
|
+
plan.skipped.push({
|
|
20362
|
+
file,
|
|
20363
|
+
name: member.key,
|
|
20364
|
+
field,
|
|
20365
|
+
spec: text.slice(member.value.start, member.value.end),
|
|
20366
|
+
reason: "not a string"
|
|
20367
|
+
});
|
|
20368
|
+
continue;
|
|
20369
|
+
}
|
|
20370
|
+
const spec = member.value.value;
|
|
20371
|
+
const cls = classifySpec(spec);
|
|
20372
|
+
if (cls.kind === "movable") {
|
|
20373
|
+
const to = `${cls.prefix}${target}`;
|
|
20374
|
+
if (to === spec) continue;
|
|
20375
|
+
edits.push({
|
|
20376
|
+
start: member.value.start,
|
|
20377
|
+
end: member.value.end,
|
|
20378
|
+
replacement: JSON.stringify(to)
|
|
20379
|
+
});
|
|
20380
|
+
setPath(expected, [field, member.key], to);
|
|
20381
|
+
plan.changed.push({
|
|
20382
|
+
file,
|
|
20383
|
+
name: member.key,
|
|
20384
|
+
field,
|
|
20385
|
+
from: spec,
|
|
20386
|
+
to
|
|
20387
|
+
});
|
|
20388
|
+
} else {
|
|
20389
|
+
const reason = cls.kind === "local" ? `a local ${cls.protocol} path, which points at a checkout on this machine rather than a release` : cls.reason;
|
|
20390
|
+
plan.skipped.push({
|
|
20391
|
+
file,
|
|
20392
|
+
name: member.key,
|
|
20393
|
+
field,
|
|
20394
|
+
spec,
|
|
20395
|
+
reason
|
|
20396
|
+
});
|
|
20397
|
+
}
|
|
20398
|
+
}
|
|
20399
|
+
}
|
|
20400
|
+
for (const blockPath of JSON_OVERRIDE_BLOCKS) {
|
|
20401
|
+
const block = memberValue(tree, blockPath);
|
|
20402
|
+
if (block?.kind !== "object") continue;
|
|
20403
|
+
const field = blockPath.join(".");
|
|
20404
|
+
for (const member of block.members) {
|
|
20405
|
+
if (!overrideTarget(member.key)) continue;
|
|
20406
|
+
if (member.value.kind !== "string") {
|
|
20407
|
+
plan.skipped.push({
|
|
20408
|
+
file,
|
|
20409
|
+
name: member.key,
|
|
20410
|
+
field,
|
|
20411
|
+
spec: text.slice(member.value.start, member.value.end),
|
|
20412
|
+
reason: "a nested override, which has no single version to move; edit it by hand"
|
|
20413
|
+
});
|
|
20414
|
+
continue;
|
|
20415
|
+
}
|
|
20416
|
+
const spec = member.value.value;
|
|
20417
|
+
const cls = classifySpec(spec);
|
|
20418
|
+
if (cls.kind === "movable") {
|
|
20419
|
+
const to = `${cls.prefix}${target}`;
|
|
20420
|
+
if (to === spec) continue;
|
|
20421
|
+
edits.push({
|
|
20422
|
+
start: member.value.start,
|
|
20423
|
+
end: member.value.end,
|
|
20424
|
+
replacement: JSON.stringify(to)
|
|
20425
|
+
});
|
|
20426
|
+
setPath(expected, [...blockPath, member.key], to);
|
|
20427
|
+
plan.overrides.push({
|
|
20428
|
+
file,
|
|
20429
|
+
name: member.key,
|
|
20430
|
+
spec,
|
|
20431
|
+
action: "bumped",
|
|
20432
|
+
to
|
|
20433
|
+
});
|
|
20434
|
+
} else if (cls.kind === "local") if (options.dropLocalOverrides) {
|
|
20435
|
+
removals.push([blockPath, member.key]);
|
|
20436
|
+
deletePath(expected, [...blockPath, member.key]);
|
|
20437
|
+
plan.overrides.push({
|
|
20438
|
+
file,
|
|
20439
|
+
name: member.key,
|
|
20440
|
+
spec,
|
|
20441
|
+
action: "removed-local"
|
|
20442
|
+
});
|
|
20443
|
+
} else plan.overrides.push({
|
|
20444
|
+
file,
|
|
20445
|
+
name: member.key,
|
|
20446
|
+
spec,
|
|
20447
|
+
action: "kept-local"
|
|
20448
|
+
});
|
|
20449
|
+
else plan.skipped.push({
|
|
20450
|
+
file,
|
|
20451
|
+
name: member.key,
|
|
20452
|
+
field,
|
|
20453
|
+
spec,
|
|
20454
|
+
reason: cls.reason
|
|
20455
|
+
});
|
|
20456
|
+
}
|
|
20457
|
+
}
|
|
20458
|
+
if (edits.length === 0 && removals.length === 0) return original;
|
|
20459
|
+
let next = applyEdits(text, edits);
|
|
20460
|
+
for (const [blockPath, key] of removals) next = removeJsonMember(next, blockPath, key);
|
|
20461
|
+
for (const blockPath of JSON_OVERRIDE_BLOCKS) {
|
|
20462
|
+
if (!removals.some(([removedFrom]) => removedFrom === blockPath)) continue;
|
|
20463
|
+
for (let depth = blockPath.length; depth > 0; depth--) {
|
|
20464
|
+
const containerPath = blockPath.slice(0, depth);
|
|
20465
|
+
const node = memberValue(scanJson(next), containerPath);
|
|
20466
|
+
if (node?.kind !== "object" || node.members.length > 0) break;
|
|
20467
|
+
next = removeJsonMember(next, containerPath.slice(0, -1), containerPath[containerPath.length - 1]);
|
|
20468
|
+
deletePath(expected, containerPath);
|
|
20469
|
+
}
|
|
20470
|
+
}
|
|
20471
|
+
let reparsed;
|
|
20472
|
+
try {
|
|
20473
|
+
reparsed = JSON.parse(next);
|
|
20474
|
+
} catch {
|
|
20475
|
+
reparsed = void 0;
|
|
20476
|
+
}
|
|
20477
|
+
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.");
|
|
20478
|
+
return bom + next;
|
|
20479
|
+
}
|
|
20480
|
+
/**
|
|
20481
|
+
* The structure of a JSON document, with where each value starts and ends.
|
|
20482
|
+
*
|
|
20483
|
+
* `JSON.parse` answers what a file says; this answers where it says it, which is
|
|
20484
|
+
* what a rewrite that leaves every other byte alone needs. Throws on anything
|
|
20485
|
+
* that is not JSON — callers have already parsed the same text, so that never
|
|
20486
|
+
* happens in practice.
|
|
20487
|
+
*/
|
|
20488
|
+
function scanJson(text) {
|
|
20489
|
+
let i = 0;
|
|
20490
|
+
const WHITESPACE = " \n\r";
|
|
20491
|
+
const skipWhitespace = () => {
|
|
20492
|
+
while (i < text.length && WHITESPACE.includes(text[i])) i++;
|
|
20493
|
+
};
|
|
20494
|
+
const unexpected = () => {
|
|
20495
|
+
throw new Error(`unexpected ${i < text.length ? `"${text[i]}"` : "end of input"} at offset ${i}`);
|
|
20496
|
+
};
|
|
20497
|
+
const readString = () => {
|
|
20498
|
+
const start = i;
|
|
20499
|
+
if (text[i] !== "\"") unexpected();
|
|
20500
|
+
i++;
|
|
20501
|
+
while (i < text.length && text[i] !== "\"") i += text[i] === "\\" ? 2 : 1;
|
|
20502
|
+
if (i >= text.length) unexpected();
|
|
20503
|
+
i++;
|
|
20504
|
+
return {
|
|
20505
|
+
kind: "string",
|
|
20506
|
+
start,
|
|
20507
|
+
end: i,
|
|
20508
|
+
value: JSON.parse(text.slice(start, i))
|
|
20509
|
+
};
|
|
20510
|
+
};
|
|
20511
|
+
const readValue = () => {
|
|
20512
|
+
skipWhitespace();
|
|
20513
|
+
const start = i;
|
|
20514
|
+
if (text[i] === "{") {
|
|
20515
|
+
i++;
|
|
20516
|
+
const members = [];
|
|
20517
|
+
skipWhitespace();
|
|
20518
|
+
if (text[i] === "}") {
|
|
20519
|
+
i++;
|
|
20520
|
+
return {
|
|
20521
|
+
kind: "object",
|
|
20522
|
+
start,
|
|
20523
|
+
end: i,
|
|
20524
|
+
members
|
|
20525
|
+
};
|
|
20526
|
+
}
|
|
20527
|
+
for (;;) {
|
|
20528
|
+
skipWhitespace();
|
|
20529
|
+
const key = readString();
|
|
20530
|
+
skipWhitespace();
|
|
20531
|
+
if (text[i] !== ":") unexpected();
|
|
20532
|
+
i++;
|
|
20533
|
+
members.push({
|
|
20534
|
+
key: key.value,
|
|
20535
|
+
keyStart: key.start,
|
|
20536
|
+
value: readValue()
|
|
20537
|
+
});
|
|
20538
|
+
skipWhitespace();
|
|
20539
|
+
if (text[i] === ",") {
|
|
20540
|
+
i++;
|
|
20541
|
+
continue;
|
|
20542
|
+
}
|
|
20543
|
+
if (text[i] === "}") {
|
|
20544
|
+
i++;
|
|
20545
|
+
return {
|
|
20546
|
+
kind: "object",
|
|
20547
|
+
start,
|
|
20548
|
+
end: i,
|
|
20549
|
+
members
|
|
20550
|
+
};
|
|
20551
|
+
}
|
|
20552
|
+
unexpected();
|
|
20553
|
+
}
|
|
20554
|
+
}
|
|
20555
|
+
if (text[i] === "[") {
|
|
20556
|
+
i++;
|
|
20557
|
+
skipWhitespace();
|
|
20558
|
+
if (text[i] === "]") {
|
|
20559
|
+
i++;
|
|
20560
|
+
return {
|
|
20561
|
+
kind: "array",
|
|
20562
|
+
start,
|
|
20563
|
+
end: i
|
|
20564
|
+
};
|
|
20565
|
+
}
|
|
20566
|
+
for (;;) {
|
|
20567
|
+
readValue();
|
|
20568
|
+
skipWhitespace();
|
|
20569
|
+
if (text[i] === ",") {
|
|
20570
|
+
i++;
|
|
20571
|
+
continue;
|
|
20572
|
+
}
|
|
20573
|
+
if (text[i] === "]") {
|
|
20574
|
+
i++;
|
|
20575
|
+
return {
|
|
20576
|
+
kind: "array",
|
|
20577
|
+
start,
|
|
20578
|
+
end: i
|
|
20579
|
+
};
|
|
20580
|
+
}
|
|
20581
|
+
unexpected();
|
|
20582
|
+
}
|
|
20583
|
+
}
|
|
20584
|
+
if (text[i] === "\"") return readString();
|
|
20585
|
+
while (i < text.length && !",:{}[]\"".includes(text[i]) && !WHITESPACE.includes(text[i])) i++;
|
|
20586
|
+
if (i === start) unexpected();
|
|
20587
|
+
return {
|
|
20588
|
+
kind: "scalar",
|
|
20589
|
+
start,
|
|
20590
|
+
end: i
|
|
20591
|
+
};
|
|
20592
|
+
};
|
|
20593
|
+
const root = readValue();
|
|
20594
|
+
skipWhitespace();
|
|
20595
|
+
if (i !== text.length) unexpected();
|
|
20596
|
+
return root;
|
|
20597
|
+
}
|
|
20598
|
+
/** The value at a path of object keys, or undefined. The last duplicate wins, as in `JSON.parse`. */
|
|
20599
|
+
function memberValue(root, keys) {
|
|
20600
|
+
let node = root;
|
|
20601
|
+
for (const key of keys) {
|
|
20602
|
+
if (node?.kind !== "object") return void 0;
|
|
20603
|
+
const members = node.members.filter((m) => m.key === key);
|
|
20604
|
+
node = members[members.length - 1]?.value;
|
|
20605
|
+
}
|
|
20606
|
+
return node;
|
|
20607
|
+
}
|
|
20608
|
+
/** Apply non-overlapping edits, last first so the earlier offsets stay valid. */
|
|
20609
|
+
function applyEdits(text, edits) {
|
|
20610
|
+
let out = text;
|
|
20611
|
+
for (const edit of [...edits].sort((a, b) => b.start - a.start)) out = out.slice(0, edit.start) + edit.replacement + out.slice(edit.end);
|
|
20612
|
+
return out;
|
|
20613
|
+
}
|
|
20614
|
+
/**
|
|
20615
|
+
* Remove one member from the object at `objectPath`, taking its comma with it.
|
|
20616
|
+
*
|
|
20617
|
+
* Rescans rather than trusting offsets from before an earlier edit. A middle or
|
|
20618
|
+
* first member takes everything up to the next key, so the next member inherits
|
|
20619
|
+
* its indentation; the last takes the comma and whitespace after the previous
|
|
20620
|
+
* value, so the closing brace keeps its line; an only member leaves `{}`.
|
|
20621
|
+
*/
|
|
20622
|
+
function removeJsonMember(text, objectPath, key) {
|
|
20623
|
+
const container = memberValue(scanJson(text), objectPath);
|
|
20624
|
+
if (container?.kind !== "object") return text;
|
|
20625
|
+
const index = container.members.findIndex((m) => m.key === key);
|
|
20626
|
+
if (index === -1) return text;
|
|
20627
|
+
const members = container.members;
|
|
20628
|
+
const member = members[index];
|
|
20629
|
+
if (members.length === 1) return text.slice(0, container.start + 1) + text.slice(container.end - 1);
|
|
20630
|
+
if (index < members.length - 1) return text.slice(0, member.keyStart) + text.slice(members[index + 1].keyStart);
|
|
20631
|
+
return text.slice(0, members[index - 1].value.end) + text.slice(member.value.end);
|
|
20632
|
+
}
|
|
20633
|
+
function isRecord(value) {
|
|
20634
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
20635
|
+
}
|
|
20636
|
+
function setPath(root, keys, value) {
|
|
20637
|
+
let node = root;
|
|
20638
|
+
for (const key of keys.slice(0, -1)) {
|
|
20639
|
+
const next = node[key];
|
|
20640
|
+
if (!isRecord(next)) return;
|
|
20641
|
+
node = next;
|
|
20642
|
+
}
|
|
20643
|
+
node[keys[keys.length - 1]] = value;
|
|
20644
|
+
}
|
|
20645
|
+
function deletePath(root, keys) {
|
|
20646
|
+
let node = root;
|
|
20647
|
+
for (const key of keys.slice(0, -1)) {
|
|
20648
|
+
const next = node[key];
|
|
20649
|
+
if (!isRecord(next)) return;
|
|
20650
|
+
node = next;
|
|
20651
|
+
}
|
|
20652
|
+
delete node[keys[keys.length - 1]];
|
|
20653
|
+
}
|
|
20654
|
+
/** Lines, each without its `\r`, and whether the file used them. */
|
|
20655
|
+
function splitLines(text) {
|
|
20656
|
+
return text.split("\n").map((line) => line.endsWith("\r") ? line.slice(0, -1) : line);
|
|
20657
|
+
}
|
|
20658
|
+
/**
|
|
20659
|
+
* A YAML scalar at the start of `rest`: its text, and where that text sits.
|
|
20660
|
+
*
|
|
20661
|
+
* Double- and single-quoted and plain scalars, which is every shape an override
|
|
20662
|
+
* value is written in. A double-quoted one with an escape in it is refused
|
|
20663
|
+
* rather than decoded: no version or path needs one, and getting the decoding
|
|
20664
|
+
* wrong would rewrite a value this does not understand.
|
|
20665
|
+
*/
|
|
20666
|
+
function readYamlScalar(rest) {
|
|
20667
|
+
if (rest.startsWith("\"")) {
|
|
20668
|
+
const close = rest.indexOf("\"", 1);
|
|
20669
|
+
if (close === -1 || rest.slice(1, close).includes("\\")) return null;
|
|
20670
|
+
if (!/^\s*(#.*)?$/.test(rest.slice(close + 1))) return null;
|
|
20671
|
+
return {
|
|
20672
|
+
value: rest.slice(1, close),
|
|
20673
|
+
start: 1,
|
|
20674
|
+
end: close
|
|
20675
|
+
};
|
|
20676
|
+
}
|
|
20677
|
+
if (rest.startsWith("'")) {
|
|
20678
|
+
const close = rest.indexOf("'", 1);
|
|
20679
|
+
if (close === -1 || rest[close + 1] === "'") return null;
|
|
20680
|
+
if (!/^\s*(#.*)?$/.test(rest.slice(close + 1))) return null;
|
|
20681
|
+
return {
|
|
20682
|
+
value: rest.slice(1, close),
|
|
20683
|
+
start: 1,
|
|
20684
|
+
end: close
|
|
20685
|
+
};
|
|
20686
|
+
}
|
|
20687
|
+
const comment = rest.search(/\s#/);
|
|
20688
|
+
const plain = (comment === -1 ? rest : rest.slice(0, comment)).trimEnd();
|
|
20689
|
+
if (plain === "" || /^[[{&*!|>%@`]/.test(plain)) return null;
|
|
20690
|
+
return {
|
|
20691
|
+
value: plain,
|
|
20692
|
+
start: 0,
|
|
20693
|
+
end: plain.length
|
|
20694
|
+
};
|
|
20695
|
+
}
|
|
20696
|
+
/**
|
|
20697
|
+
* The top-level `overrides:` block of a `pnpm-workspace.yaml`, read line by line.
|
|
20698
|
+
*
|
|
20699
|
+
* Deliberately narrow. The file is YAML, and the CLI carries no YAML parser; the
|
|
20700
|
+
* shape pnpm documents and every project writes is a block mapping of one
|
|
20701
|
+
* scalar per line, which a line reader handles exactly. Anything else in the
|
|
20702
|
+
* block is counted rather than guessed at, and an inline `overrides: { … }` is
|
|
20703
|
+
* reported as not handled.
|
|
20704
|
+
*/
|
|
20705
|
+
function readYamlOverrides(lines) {
|
|
20706
|
+
const keyLine = lines.findIndex((line) => /^overrides:\s*(#.*)?$/.test(line));
|
|
20707
|
+
if (keyLine === -1) return null;
|
|
20708
|
+
const block = {
|
|
20709
|
+
keyLine,
|
|
20710
|
+
lines: [],
|
|
20711
|
+
entries: [],
|
|
20712
|
+
otherContent: 0
|
|
20713
|
+
};
|
|
20714
|
+
for (let index = keyLine + 1; index < lines.length; index++) {
|
|
20715
|
+
const line = lines[index];
|
|
20716
|
+
if (line.trim() !== "" && !/^\s/.test(line) && !line.startsWith("#")) break;
|
|
20717
|
+
block.lines.push(index);
|
|
20718
|
+
if (line.trim() === "" || /^\s*#/.test(line)) continue;
|
|
20719
|
+
const entry = /^(\s+)("[^"]*"|'[^']*'|[^\s"'#][^:#]*?)\s*:(\s+|$)/.exec(line);
|
|
20720
|
+
const valueOffset = entry ? entry[0].length : -1;
|
|
20721
|
+
const scalar = entry ? readYamlScalar(line.slice(valueOffset)) : null;
|
|
20722
|
+
if (!entry || !scalar) {
|
|
20723
|
+
block.otherContent++;
|
|
20724
|
+
continue;
|
|
20725
|
+
}
|
|
20726
|
+
const rawKey = entry[2];
|
|
20727
|
+
const key = /^["']/.test(rawKey) ? rawKey.slice(1, -1) : rawKey;
|
|
20728
|
+
block.entries.push({
|
|
20729
|
+
line: index,
|
|
20730
|
+
key,
|
|
20731
|
+
value: scalar.value,
|
|
20732
|
+
valueStart: valueOffset + scalar.start,
|
|
20733
|
+
valueEnd: valueOffset + scalar.end
|
|
20734
|
+
});
|
|
20735
|
+
}
|
|
20736
|
+
return block;
|
|
20737
|
+
}
|
|
20738
|
+
function planWorkspaceYaml(file, original, target, options, plan) {
|
|
20739
|
+
const lines = splitLines(original);
|
|
20740
|
+
const crlf = original.includes("\r\n");
|
|
20741
|
+
const block = readYamlOverrides(lines);
|
|
20742
|
+
if (!block) {
|
|
20743
|
+
const inline = lines.find((line) => /^overrides:\s*[^\s#]/.test(line));
|
|
20744
|
+
if (inline && inline.includes("@rebasepro/")) plan.skipped.push({
|
|
20745
|
+
file,
|
|
20746
|
+
name: "overrides",
|
|
20747
|
+
field: "overrides",
|
|
20748
|
+
spec: inline.slice(10).trim(),
|
|
20749
|
+
reason: "an inline overrides map, which this does not rewrite; write it as a block, or edit it by hand"
|
|
20750
|
+
});
|
|
20751
|
+
return original;
|
|
20752
|
+
}
|
|
20753
|
+
const next = [...lines];
|
|
20754
|
+
const removed = /* @__PURE__ */ new Set();
|
|
20755
|
+
const expected = /* @__PURE__ */ new Map();
|
|
20756
|
+
for (const entry of block.entries) {
|
|
20757
|
+
if (!overrideTarget(entry.key)) {
|
|
20758
|
+
expected.set(entry.key, entry.value);
|
|
20759
|
+
continue;
|
|
20760
|
+
}
|
|
20761
|
+
const cls = classifySpec(entry.value);
|
|
20762
|
+
if (cls.kind === "movable") {
|
|
20763
|
+
const to = `${cls.prefix}${target}`;
|
|
20764
|
+
expected.set(entry.key, to);
|
|
20765
|
+
if (to === entry.value) continue;
|
|
20766
|
+
const line = lines[entry.line];
|
|
20767
|
+
next[entry.line] = line.slice(0, entry.valueStart) + to + line.slice(entry.valueEnd);
|
|
20768
|
+
plan.overrides.push({
|
|
20769
|
+
file,
|
|
20770
|
+
name: entry.key,
|
|
20771
|
+
spec: entry.value,
|
|
20772
|
+
action: "bumped",
|
|
20773
|
+
to
|
|
20774
|
+
});
|
|
20775
|
+
} else if (cls.kind === "local") if (options.dropLocalOverrides) {
|
|
20776
|
+
removed.add(entry.line);
|
|
20777
|
+
plan.overrides.push({
|
|
20778
|
+
file,
|
|
20779
|
+
name: entry.key,
|
|
20780
|
+
spec: entry.value,
|
|
20781
|
+
action: "removed-local"
|
|
20782
|
+
});
|
|
20783
|
+
} else {
|
|
20784
|
+
expected.set(entry.key, entry.value);
|
|
20785
|
+
plan.overrides.push({
|
|
20786
|
+
file,
|
|
20787
|
+
name: entry.key,
|
|
20788
|
+
spec: entry.value,
|
|
20789
|
+
action: "kept-local"
|
|
20790
|
+
});
|
|
20791
|
+
}
|
|
20792
|
+
else {
|
|
20793
|
+
expected.set(entry.key, entry.value);
|
|
20794
|
+
plan.skipped.push({
|
|
20795
|
+
file,
|
|
20796
|
+
name: entry.key,
|
|
20797
|
+
field: "overrides",
|
|
20798
|
+
spec: entry.value,
|
|
20799
|
+
reason: cls.reason
|
|
20800
|
+
});
|
|
20801
|
+
}
|
|
20802
|
+
}
|
|
20803
|
+
const survivors = block.entries.filter((entry) => !removed.has(entry.line)).length + block.otherContent;
|
|
20804
|
+
if (removed.size > 0 && survivors === 0) removed.add(block.keyLine);
|
|
20805
|
+
const content = next.filter((_, index) => !removed.has(index)).join(crlf ? "\r\n" : "\n");
|
|
20806
|
+
const after = readYamlOverrides(splitLines(content));
|
|
20807
|
+
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.");
|
|
20808
|
+
return content;
|
|
20809
|
+
}
|
|
20810
|
+
/** Lockfiles, in the order they are looked for in each directory. */
|
|
20811
|
+
var LOCKFILES = [
|
|
20812
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
20813
|
+
["package-lock.json", "npm"],
|
|
20814
|
+
["yarn.lock", "yarn"],
|
|
20815
|
+
["bun.lockb", "bun"],
|
|
20816
|
+
["bun.lock", "bun"]
|
|
20817
|
+
];
|
|
20818
|
+
/**
|
|
20819
|
+
* The package manager this project installs with, from what is on disk.
|
|
20820
|
+
*
|
|
20821
|
+
* A lockfile is the project's own statement, so it wins. The search starts at
|
|
20822
|
+
* the project root and walks up, because a project inside a workspace has its
|
|
20823
|
+
* lockfile at the workspace root; it stops at the repository's root, so a stray
|
|
20824
|
+
* lockfile in a home directory is never read as this project's choice. With no
|
|
20825
|
+
* lockfile anywhere, a `pnpm-workspace.yaml` still says pnpm; otherwise npm,
|
|
20826
|
+
* the one every Node install has.
|
|
20827
|
+
*/
|
|
20828
|
+
function detectInstaller(projectRoot) {
|
|
20829
|
+
let dir = path.resolve(projectRoot);
|
|
20830
|
+
let workspaceYaml = false;
|
|
20831
|
+
for (;;) {
|
|
20832
|
+
for (const [lockfile, installer] of LOCKFILES) if (fs.existsSync(path.join(dir, lockfile))) return installer;
|
|
20833
|
+
if (fs.existsSync(path.join(dir, "pnpm-workspace.yaml"))) workspaceYaml = true;
|
|
20834
|
+
const parent = path.dirname(dir);
|
|
20835
|
+
if (parent === dir || fs.existsSync(path.join(dir, ".git"))) break;
|
|
20836
|
+
dir = parent;
|
|
20837
|
+
}
|
|
20838
|
+
return workspaceYaml ? "pnpm" : "npm";
|
|
20839
|
+
}
|
|
20840
|
+
/**
|
|
20841
|
+
* The install command for each package manager — pnpm's and npm's from the
|
|
20842
|
+
* helpers every other command uses, yarn's and bun's spelled the same way.
|
|
20843
|
+
*/
|
|
20844
|
+
function installCommand(installer) {
|
|
20845
|
+
const [bin, ...args] = installer === "pnpm" || installer === "npm" ? getPMCommands(installer).install : [installer, "install"];
|
|
20846
|
+
return [bin, args];
|
|
20847
|
+
}
|
|
20848
|
+
/**
|
|
20849
|
+
* The exact version `--to` names.
|
|
20850
|
+
*
|
|
20851
|
+
* An exact version is used as written, with no network call: the control plane
|
|
20852
|
+
* passes one when it rebuilds a project, and that rebuild must not depend on a
|
|
20853
|
+
* registry lookup succeeding for anything but the install itself. Anything
|
|
20854
|
+
* else is a dist-tag, asked of the registry through `npmView` — which runs npm
|
|
20855
|
+
* in the project, so its `.npmrc` and the user's registry config apply.
|
|
20856
|
+
*/
|
|
20857
|
+
async function resolveTarget(requested, npmView) {
|
|
20858
|
+
const trimmed = requested.trim();
|
|
20859
|
+
const bare = trimmed.replace(/^v(?=\d)/, "");
|
|
20860
|
+
if (isExactVersion(bare)) return bare;
|
|
20861
|
+
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.");
|
|
20862
|
+
let answer;
|
|
20863
|
+
try {
|
|
20864
|
+
answer = (await npmView(`${RELEASE_PACKAGE}@${trimmed}`)).trim();
|
|
20865
|
+
} catch (err) {
|
|
20866
|
+
const detail = err instanceof Error ? firstLine(err.message) : String(err);
|
|
20867
|
+
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.");
|
|
20868
|
+
}
|
|
20869
|
+
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.");
|
|
20870
|
+
return answer;
|
|
20871
|
+
}
|
|
20872
|
+
function firstLine(text) {
|
|
20873
|
+
return text.split("\n").map((line) => line.trim()).filter(Boolean)[0] ?? "";
|
|
20874
|
+
}
|
|
20875
|
+
/**
|
|
20876
|
+
* Whether moving from `from` to `to` goes backwards, for the one-word note the
|
|
20877
|
+
* summary prints. Prerelease identifiers compare as SemVer orders them: a
|
|
20878
|
+
* release outranks its own prereleases.
|
|
20879
|
+
*/
|
|
20880
|
+
function isDowngrade(from, to) {
|
|
20881
|
+
const a = SEMVER.exec(from.replace(/^[\^~]/, ""));
|
|
20882
|
+
const b = SEMVER.exec(to.replace(/^[\^~]/, ""));
|
|
20883
|
+
if (!a || !b) return false;
|
|
20884
|
+
for (let i = 1; i <= 3; i++) {
|
|
20885
|
+
const diff = Number(a[i]) - Number(b[i]);
|
|
20886
|
+
if (diff !== 0) return diff > 0;
|
|
20887
|
+
}
|
|
20888
|
+
const preA = a[4];
|
|
20889
|
+
const preB = b[4];
|
|
20890
|
+
if (preA === preB) return false;
|
|
20891
|
+
if (preA === void 0) return true;
|
|
20892
|
+
if (preB === void 0) return false;
|
|
20893
|
+
const partsA = preA.split(".");
|
|
20894
|
+
const partsB = preB.split(".");
|
|
20895
|
+
for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
|
|
20896
|
+
const x = partsA[i];
|
|
20897
|
+
const y = partsB[i];
|
|
20898
|
+
if (x === void 0) return false;
|
|
20899
|
+
if (y === void 0) return true;
|
|
20900
|
+
if (x === y) continue;
|
|
20901
|
+
const nx = /^\d+$/.test(x) ? Number(x) : NaN;
|
|
20902
|
+
const ny = /^\d+$/.test(y) ? Number(y) : NaN;
|
|
20903
|
+
if (!Number.isNaN(nx) && !Number.isNaN(ny)) return nx > ny;
|
|
20904
|
+
if (!Number.isNaN(nx)) return false;
|
|
20905
|
+
if (!Number.isNaN(ny)) return true;
|
|
20906
|
+
return x > y;
|
|
20907
|
+
}
|
|
20908
|
+
return false;
|
|
20909
|
+
}
|
|
20910
|
+
//#endregion
|
|
20911
|
+
//#region src/commands/upgrade.ts
|
|
20912
|
+
/**
|
|
20913
|
+
* CLI command: rebase upgrade [--to <version|tag>]
|
|
20914
|
+
*
|
|
20915
|
+
* Moves every `@rebasepro/*` package the project pins to one release, then
|
|
20916
|
+
* installs. The file work — which specs move, the rewrite, what overrides say —
|
|
20917
|
+
* is `../upgrade.ts`; this adds the registry lookup for a tag, the install, and
|
|
20918
|
+
* the two ways of saying what happened.
|
|
20919
|
+
*/
|
|
20920
|
+
/** Every flag `rebase upgrade` accepts. Its help page and the docs verifier read this. */
|
|
20921
|
+
var UPGRADE_FLAGS = {
|
|
20922
|
+
"--to": String,
|
|
20923
|
+
"--drop-local-overrides": Boolean,
|
|
20924
|
+
"--no-install": Boolean,
|
|
20925
|
+
"--dry-run": Boolean,
|
|
20926
|
+
"--json": Boolean
|
|
20927
|
+
};
|
|
20928
|
+
function printHelp$1() {
|
|
20929
|
+
console.log(`
|
|
20930
|
+
${chalk.bold("rebase upgrade")} — move every @rebasepro package this project pins to one release
|
|
20931
|
+
|
|
20932
|
+
${chalk.bold("Usage")}
|
|
20933
|
+
rebase upgrade [--to <version|tag>] [options]
|
|
20934
|
+
|
|
20935
|
+
${chalk.bold("Options")}
|
|
20936
|
+
--to <version|tag> The release: an exact version (0.21.0) or a dist-tag
|
|
20937
|
+
(latest, canary). Default: latest
|
|
20938
|
+
--drop-local-overrides Remove link: and file: overrides of @rebasepro packages,
|
|
20939
|
+
which win over every pin
|
|
20940
|
+
--no-install Rewrite the package.json files, but do not install
|
|
20941
|
+
--dry-run Print what would change, and write nothing
|
|
20942
|
+
--json One JSON document on stdout
|
|
20943
|
+
-h, --help Show this help
|
|
20944
|
+
|
|
20945
|
+
${chalk.bold("What it changes")}
|
|
20946
|
+
Every @rebasepro/* entry in dependencies, devDependencies and optionalDependencies,
|
|
20947
|
+
in every package.json under the project (node_modules, dist* and hidden
|
|
20948
|
+
directories excepted). ^ and ~ are kept. peerDependencies are left alone, and so
|
|
20949
|
+
are workspace:, link:, file:, git and tag specs, which are listed with the reason.
|
|
20950
|
+
Overrides in pnpm-workspace.yaml and package.json are bumped the same way.
|
|
20951
|
+
|
|
20952
|
+
${chalk.bold("Examples")}
|
|
20953
|
+
rebase upgrade Move to the latest release and install
|
|
20954
|
+
rebase upgrade --to canary --dry-run See what the canary would change
|
|
20955
|
+
rebase upgrade --to 0.21.0 --no-install --json
|
|
20956
|
+
`.trim());
|
|
20957
|
+
}
|
|
20958
|
+
var defaultIo = {
|
|
20959
|
+
npmView: async (spec, cwd) => {
|
|
20960
|
+
try {
|
|
20961
|
+
return (await execa("npm", [
|
|
20962
|
+
"view",
|
|
20963
|
+
spec,
|
|
20964
|
+
"version"
|
|
20965
|
+
], { cwd })).stdout;
|
|
20966
|
+
} catch (err) {
|
|
20967
|
+
throw new Error(npmFailure(err));
|
|
20968
|
+
}
|
|
20969
|
+
},
|
|
20970
|
+
install: async ([bin, args], cwd, quietStdout) => {
|
|
20971
|
+
await execa(bin, args, {
|
|
20972
|
+
cwd,
|
|
20973
|
+
stdio: quietStdout ? [
|
|
20974
|
+
"ignore",
|
|
20975
|
+
2,
|
|
20976
|
+
"inherit"
|
|
20977
|
+
] : "inherit"
|
|
20978
|
+
});
|
|
20979
|
+
}
|
|
20980
|
+
};
|
|
20981
|
+
/**
|
|
20982
|
+
* The one line of a failed `npm view` worth printing.
|
|
20983
|
+
*
|
|
20984
|
+
* execa's own message is the command line that failed, which says nothing the
|
|
20985
|
+
* reader does not know. npm's stderr says why — `404 No match found for version
|
|
20986
|
+
* nosuchtag` — under an `npm error code E404` line that says less.
|
|
20987
|
+
*/
|
|
20988
|
+
function npmFailure(err) {
|
|
20989
|
+
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));
|
|
20990
|
+
}
|
|
20991
|
+
async function upgradeCommand(rawArgs, io = defaultIo) {
|
|
20992
|
+
if (wantsHelp(rawArgs)) {
|
|
20993
|
+
printHelp$1();
|
|
20994
|
+
return;
|
|
20995
|
+
}
|
|
20996
|
+
const { flags } = parseCommandArgs({
|
|
20997
|
+
spec: UPGRADE_FLAGS,
|
|
20998
|
+
rawArgs,
|
|
20999
|
+
commandWords: 1,
|
|
21000
|
+
command: "upgrade",
|
|
21001
|
+
maxPositionals: 0
|
|
21002
|
+
});
|
|
21003
|
+
const json = flags["--json"] === true;
|
|
21004
|
+
const dryRun = flags["--dry-run"] === true;
|
|
21005
|
+
const projectRoot = requireProjectRoot();
|
|
21006
|
+
/** Every refusal, in whichever of the two languages was asked for. */
|
|
21007
|
+
const refuse = (message, code, hint, issues) => {
|
|
21008
|
+
if (json) failAsJson(message, code, hint, issues);
|
|
21009
|
+
console.error(chalk.red(`✗ ${message}`));
|
|
21010
|
+
for (const issue of issues ?? []) console.error(` ${chalk.gray(issue.path ?? "")} ${issue.message}`);
|
|
21011
|
+
if (hint) console.error(chalk.gray(` ${hint}`));
|
|
21012
|
+
process.exit(1);
|
|
21013
|
+
};
|
|
21014
|
+
try {
|
|
21015
|
+
loadManifest(projectRoot);
|
|
21016
|
+
} catch (err) {
|
|
21017
|
+
if (err instanceof ManifestError) refuse(err.message, "manifest_invalid", "Fix rebase.json, then run this again.", err.issues.map((issue) => ({
|
|
21018
|
+
path: issue.path,
|
|
21019
|
+
message: issue.message
|
|
21020
|
+
})));
|
|
21021
|
+
throw err;
|
|
21022
|
+
}
|
|
21023
|
+
let plan;
|
|
21024
|
+
try {
|
|
21025
|
+
plan = planUpgrade(projectRoot, await resolveTarget(flags["--to"] ?? "latest", (spec) => io.npmView(spec, projectRoot)), { dropLocalOverrides: flags["--drop-local-overrides"] === true });
|
|
21026
|
+
} catch (err) {
|
|
21027
|
+
if (err instanceof UpgradeError) refuse(err.message, err.code, err.hint);
|
|
21028
|
+
throw err;
|
|
21029
|
+
}
|
|
21030
|
+
if (!dryRun) applyUpgradePlan(plan);
|
|
21031
|
+
const wrote = !dryRun && plan.writes.length > 0;
|
|
21032
|
+
const install = wrote && flags["--no-install"] !== true;
|
|
21033
|
+
const installer = detectInstaller(projectRoot);
|
|
21034
|
+
const [bin, args] = installCommand(installer);
|
|
21035
|
+
const installLine = [bin, ...args].join(" ");
|
|
21036
|
+
if (!json) printSummary(plan, dryRun);
|
|
21037
|
+
let installed = false;
|
|
21038
|
+
if (install) {
|
|
21039
|
+
if (!json) console.log(chalk.gray(` Installing with ${installer}...\n`));
|
|
21040
|
+
try {
|
|
21041
|
+
await io.install([bin, args], projectRoot, json);
|
|
21042
|
+
installed = true;
|
|
21043
|
+
} catch {
|
|
21044
|
+
refuse(`\`${installLine}\` failed.`, "install_failed", `The package.json edits were kept. Fix what the install reported and run \`${installLine}\` again, or revert the edits with git.`);
|
|
21045
|
+
}
|
|
21046
|
+
}
|
|
21047
|
+
const result = {
|
|
21048
|
+
target: plan.target,
|
|
21049
|
+
changed: plan.changed,
|
|
21050
|
+
skipped: plan.skipped,
|
|
21051
|
+
overrides: plan.overrides,
|
|
21052
|
+
unreadable: plan.unreadable,
|
|
21053
|
+
installed,
|
|
21054
|
+
dryRun
|
|
21055
|
+
};
|
|
21056
|
+
if (json) {
|
|
21057
|
+
console.log(JSON.stringify(result, null, 2));
|
|
21058
|
+
return;
|
|
21059
|
+
}
|
|
21060
|
+
printOutcome(plan, {
|
|
21061
|
+
dryRun,
|
|
21062
|
+
wrote,
|
|
21063
|
+
installed,
|
|
21064
|
+
installLine
|
|
21065
|
+
});
|
|
21066
|
+
}
|
|
21067
|
+
/** Changes grouped by file, then what was left alone and why, then overrides. */
|
|
21068
|
+
function printSummary(plan, dryRun) {
|
|
21069
|
+
console.log("");
|
|
21070
|
+
console.log(`${chalk.bold("Rebase")} — ${dryRun ? "what upgrading" : "upgrading"} @rebasepro packages to ${chalk.cyan(plan.target)}${dryRun ? " would change" : ""}`);
|
|
21071
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
21072
|
+
for (const pin of plan.changed) byFile.set(pin.file, [...byFile.get(pin.file) ?? [], pin]);
|
|
21073
|
+
const nameWidth = Math.max(0, ...plan.changed.map((pin) => pin.name.length));
|
|
21074
|
+
for (const [file, pins] of byFile) {
|
|
21075
|
+
console.log("");
|
|
21076
|
+
console.log(` ${chalk.bold(file)}`);
|
|
21077
|
+
for (const pin of pins) {
|
|
21078
|
+
const field = pin.field === "dependencies" ? "" : chalk.gray(` ${pin.field}`);
|
|
21079
|
+
const older = isDowngrade(pin.from, pin.to) ? chalk.yellow(" (older)") : "";
|
|
21080
|
+
console.log(` ${pin.name.padEnd(nameWidth)} ${chalk.gray(pin.from)} → ${chalk.green(pin.to)}${field}${older}`);
|
|
21081
|
+
}
|
|
21082
|
+
}
|
|
21083
|
+
if (plan.skipped.length > 0) {
|
|
21084
|
+
console.log("");
|
|
21085
|
+
console.log(` ${chalk.bold("Left alone")}`);
|
|
21086
|
+
for (const skip of plan.skipped) {
|
|
21087
|
+
console.log(` ${skip.name} ${chalk.gray(skip.spec)} ${chalk.gray(`(${skip.file}, ${skip.field})`)}`);
|
|
21088
|
+
console.log(chalk.gray(` ${skip.reason}`));
|
|
21089
|
+
}
|
|
21090
|
+
}
|
|
21091
|
+
if (plan.overrides.length > 0) {
|
|
21092
|
+
console.log("");
|
|
21093
|
+
console.log(` ${chalk.bold("Overrides")}`);
|
|
21094
|
+
for (const override of plan.overrides) {
|
|
21095
|
+
const where = chalk.gray(`(${override.file})`);
|
|
21096
|
+
if (override.action === "bumped") console.log(` ${override.name} ${chalk.gray(override.spec)} → ${chalk.green(override.to ?? "")} ${where}`);
|
|
21097
|
+
else if (override.action === "removed-local") console.log(` ${override.name} ${chalk.gray(override.spec)} ${where} ${chalk.green(dryRun ? "would be removed" : "removed")}`);
|
|
21098
|
+
else {
|
|
21099
|
+
console.log(` ${chalk.yellow("⚠")} ${override.name} → ${override.spec} ${where}`);
|
|
21100
|
+
console.log(chalk.yellow(" local override — wins over every pin, the upgrade will not reach these packages"));
|
|
21101
|
+
}
|
|
21102
|
+
}
|
|
21103
|
+
if (plan.overrides.some((o) => o.action === "kept-local")) console.log(chalk.gray(" Run again with --drop-local-overrides to remove them."));
|
|
21104
|
+
}
|
|
21105
|
+
if (plan.unreadable.length > 0) {
|
|
21106
|
+
console.log("");
|
|
21107
|
+
console.log(` ${chalk.bold("Not read")}`);
|
|
21108
|
+
for (const entry of plan.unreadable) console.log(` ${entry.file} ${chalk.gray(entry.reason)}`);
|
|
21109
|
+
}
|
|
21110
|
+
console.log("");
|
|
21111
|
+
}
|
|
21112
|
+
function printOutcome(plan, outcome) {
|
|
21113
|
+
const moved = plan.changed.length + plan.overrides.filter((o) => o.action !== "kept-local").length;
|
|
21114
|
+
if (outcome.dryRun) {
|
|
21115
|
+
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."));
|
|
21116
|
+
console.log("");
|
|
21117
|
+
return;
|
|
21118
|
+
}
|
|
21119
|
+
if (!outcome.wrote) {
|
|
21120
|
+
console.log(chalk.green(`✓ This project is already on ${plan.target}.`));
|
|
21121
|
+
console.log("");
|
|
21122
|
+
return;
|
|
21123
|
+
}
|
|
21124
|
+
const files = plan.writes.length;
|
|
21125
|
+
console.log(chalk.green(`✓ Moved ${moved} entr${moved === 1 ? "y" : "ies"} in ${files} file${files === 1 ? "" : "s"} to ${plan.target}${outcome.installed ? " and installed" : ""}.`));
|
|
21126
|
+
if (!outcome.installed) console.log(chalk.gray(` Install with \`${outcome.installLine}\`, then run \`rebase build\`, then \`rebase cloud deploy\`.`));
|
|
21127
|
+
else console.log(chalk.gray(" Run `rebase build`, then `rebase cloud deploy`."));
|
|
21128
|
+
console.log("");
|
|
21129
|
+
}
|
|
21130
|
+
//#endregion
|
|
19632
21131
|
//#region src/cli.ts
|
|
19633
21132
|
/**
|
|
19634
21133
|
* Silence dotenv's own banner, for this process and everything it spawns.
|
|
@@ -19697,7 +21196,8 @@ async function entry(args) {
|
|
|
19697
21196
|
"generate-sdk",
|
|
19698
21197
|
"telemetry",
|
|
19699
21198
|
"resources",
|
|
19700
|
-
"status"
|
|
21199
|
+
"status",
|
|
21200
|
+
"upgrade"
|
|
19701
21201
|
];
|
|
19702
21202
|
if (!command || parsedArgs["--help"] && !namespacedCommands.includes(command)) {
|
|
19703
21203
|
printHelp();
|
|
@@ -19776,6 +21276,9 @@ async function dispatch(command, effectiveSubcommand, args, parsedArgs, namespac
|
|
|
19776
21276
|
case "start":
|
|
19777
21277
|
await startCommand(args);
|
|
19778
21278
|
break;
|
|
21279
|
+
case "upgrade":
|
|
21280
|
+
await upgradeCommand(args);
|
|
21281
|
+
break;
|
|
19779
21282
|
case "apps":
|
|
19780
21283
|
await appsCommand(effectiveSubcommand, args);
|
|
19781
21284
|
break;
|
|
@@ -19823,6 +21326,7 @@ ${chalk.green.bold("Commands")}
|
|
|
19823
21326
|
${chalk.blue.bold("build")} Build the apps declared in rebase.json into a bundle
|
|
19824
21327
|
${chalk.blue.bold("normalize-imports")} Complete compiled output's relative imports for Node ESM
|
|
19825
21328
|
${chalk.blue.bold("start")} Start the backend server ${chalk.gray("(production)")}
|
|
21329
|
+
${chalk.blue.bold("upgrade")} Move every @rebasepro package to one release, then install
|
|
19826
21330
|
${chalk.blue.bold("apps list")} Show the apps this repository declares
|
|
19827
21331
|
|
|
19828
21332
|
${chalk.green.bold("Schema")}
|