@strapi/upgrade 5.0.5 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -5,26 +5,26 @@ const commander = require("commander");
5
5
  const prompts = require("prompts");
6
6
  const semver = require("semver");
7
7
  const path$1 = require("node:path");
8
- const simpleGit = require("simple-git");
9
- const utils = require("@strapi/utils");
10
- const fp = require("lodash/fp");
11
- const fse = require("fs-extra");
8
+ const CliTable3 = require("cli-table3");
12
9
  const assert = require("node:assert");
10
+ const fse = require("fs-extra");
13
11
  const fastglob = require("fast-glob");
14
12
  const Runner = require("jscodeshift/src/Runner");
13
+ const fp = require("lodash/fp");
15
14
  const node = require("esbuild-register/dist/node");
16
- const CliTable3 = require("cli-table3");
15
+ const utils = require("@strapi/utils");
16
+ const simpleGit = require("simple-git");
17
17
  const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
18
18
  const os__default = /* @__PURE__ */ _interopDefault(os);
19
19
  const chalk__default = /* @__PURE__ */ _interopDefault(chalk);
20
20
  const prompts__default = /* @__PURE__ */ _interopDefault(prompts);
21
21
  const semver__default = /* @__PURE__ */ _interopDefault(semver);
22
22
  const path__default = /* @__PURE__ */ _interopDefault(path$1);
23
- const simpleGit__default = /* @__PURE__ */ _interopDefault(simpleGit);
24
- const fse__default = /* @__PURE__ */ _interopDefault(fse);
23
+ const CliTable3__default = /* @__PURE__ */ _interopDefault(CliTable3);
25
24
  const assert__default = /* @__PURE__ */ _interopDefault(assert);
25
+ const fse__default = /* @__PURE__ */ _interopDefault(fse);
26
26
  const fastglob__default = /* @__PURE__ */ _interopDefault(fastglob);
27
- const CliTable3__default = /* @__PURE__ */ _interopDefault(CliTable3);
27
+ const simpleGit__default = /* @__PURE__ */ _interopDefault(simpleGit);
28
28
  class Logger {
29
29
  isDebug;
30
30
  isSilent;
@@ -99,6 +99,7 @@ var ReleaseType = /* @__PURE__ */ ((ReleaseType2) => {
99
99
  ReleaseType2["Major"] = "major";
100
100
  ReleaseType2["Minor"] = "minor";
101
101
  ReleaseType2["Patch"] = "patch";
102
+ ReleaseType2["Latest"] = "latest";
102
103
  return ReleaseType2;
103
104
  })(ReleaseType || {});
104
105
  const semVerFactory = (version2) => {
@@ -120,17 +121,20 @@ const rangeFactory = (range) => {
120
121
  };
121
122
  const rangeFromReleaseType = (current, identifier) => {
122
123
  switch (identifier) {
123
- case ReleaseType.Major: {
124
- const nextMajor = semver__default.default.inc(current, "major");
125
- return rangeFactory(`>${current.raw} <=${nextMajor}`);
124
+ case ReleaseType.Latest: {
125
+ return rangeFactory(`>${current.raw}`);
126
126
  }
127
- case ReleaseType.Patch: {
128
- const minor = semver__default.default.inc(current, "minor");
129
- return rangeFactory(`>${current.raw} <${minor}`);
127
+ case ReleaseType.Major: {
128
+ const nextMajor = semVerFactory(current.raw).inc("major");
129
+ return rangeFactory(`>${current.raw} <=${nextMajor.major}`);
130
130
  }
131
131
  case ReleaseType.Minor: {
132
- const major = semver__default.default.inc(current, "major");
133
- return rangeFactory(`>${current.raw} <${major}`);
132
+ const nextMajor = semVerFactory(current.raw).inc("major");
133
+ return rangeFactory(`>${current.raw} <${nextMajor.raw}`);
134
+ }
135
+ case ReleaseType.Patch: {
136
+ const nextMinor = semVerFactory(current.raw).inc("minor");
137
+ return rangeFactory(`>${current.raw} <${nextMinor.raw}`);
134
138
  }
135
139
  default: {
136
140
  throw new Error("Not implemented");
@@ -150,7 +154,36 @@ const isValidStringifiedRange = (str) => semver__default.default.validRange(str)
150
154
  const isRangeInstance = (range) => {
151
155
  return range instanceof semver__default.default.Range;
152
156
  };
157
+ class UnexpectedError extends Error {
158
+ constructor() {
159
+ super("Unexpected Error");
160
+ }
161
+ }
162
+ class NPMCandidateNotFoundError extends Error {
163
+ target;
164
+ constructor(target, message = `Couldn't find a valid NPM candidate for "${target}"`) {
165
+ super(message);
166
+ this.target = target;
167
+ }
168
+ }
169
+ class AbortedError extends Error {
170
+ constructor(message = "Upgrade aborted") {
171
+ super(message);
172
+ }
173
+ }
174
+ const unknownToError = (e) => {
175
+ if (e instanceof Error) {
176
+ return e;
177
+ }
178
+ if (typeof e === "string") {
179
+ return new Error(e);
180
+ }
181
+ return new UnexpectedError();
182
+ };
153
183
  const handleError = (err, isSilent) => {
184
+ if (err instanceof AbortedError) {
185
+ process.exit(0);
186
+ }
154
187
  if (!isSilent) {
155
188
  console.error(
156
189
  chalk__default.default.red(`[ERROR] [${(/* @__PURE__ */ new Date()).toISOString()}]`),
@@ -159,115 +192,6 @@ const handleError = (err, isSilent) => {
159
192
  }
160
193
  process.exit(1);
161
194
  };
162
- class Requirement {
163
- isRequired;
164
- name;
165
- testCallback;
166
- children;
167
- constructor(name, testCallback, isRequired) {
168
- this.name = name;
169
- this.testCallback = testCallback;
170
- this.isRequired = isRequired ?? true;
171
- this.children = [];
172
- }
173
- setChildren(children) {
174
- this.children = children;
175
- return this;
176
- }
177
- addChild(child) {
178
- this.children.push(child);
179
- return this;
180
- }
181
- asOptional() {
182
- const newInstance = requirementFactory(this.name, this.testCallback, false);
183
- newInstance.setChildren(this.children);
184
- return newInstance;
185
- }
186
- asRequired() {
187
- const newInstance = requirementFactory(this.name, this.testCallback, true);
188
- newInstance.setChildren(this.children);
189
- return newInstance;
190
- }
191
- async test(context) {
192
- try {
193
- await this.testCallback?.(context);
194
- return ok();
195
- } catch (e) {
196
- if (e instanceof Error) {
197
- return errored(e);
198
- }
199
- if (typeof e === "string") {
200
- return errored(new Error(e));
201
- }
202
- return errored(new Error("Unknown error"));
203
- }
204
- }
205
- }
206
- const ok = () => ({ pass: true, error: null });
207
- const errored = (error) => ({ pass: false, error });
208
- const requirementFactory = (name, testCallback, isRequired) => new Requirement(name, testCallback, isRequired);
209
- const REQUIRE_AVAILABLE_NEXT_MAJOR = requirementFactory(
210
- "REQUIRE_AVAILABLE_NEXT_MAJOR",
211
- (context) => {
212
- const { project, target } = context;
213
- const currentMajor = project.strapiVersion.major;
214
- const targetedMajor = target.major;
215
- if (targetedMajor === currentMajor) {
216
- throw new Error(`You're already on the latest major version (v${currentMajor})`);
217
- }
218
- }
219
- );
220
- const REQUIRE_LATEST_FOR_CURRENT_MAJOR = requirementFactory(
221
- "REQUIRE_LATEST_FOR_CURRENT_MAJOR",
222
- (context) => {
223
- const { project, target, npmVersionsMatches } = context;
224
- if (npmVersionsMatches.length !== 1) {
225
- const invalidVersions = npmVersionsMatches.slice(0, -1);
226
- const invalidVersionsAsSemVer = invalidVersions.map((v) => v.version);
227
- const nbInvalidVersions = npmVersionsMatches.length;
228
- const currentMajor = project.strapiVersion.major;
229
- throw new Error(
230
- `Doing a major upgrade requires to be on the latest v${currentMajor} version, but found ${nbInvalidVersions} versions between the current one and ${target}: ${invalidVersionsAsSemVer}`
231
- );
232
- }
233
- }
234
- );
235
- const REQUIRE_GIT_CLEAN_REPOSITORY = requirementFactory(
236
- "REQUIRE_GIT_CLEAN_REPOSITORY",
237
- async (context) => {
238
- const git = simpleGit__default.default({ baseDir: context.project.cwd });
239
- const status = await git.status();
240
- if (!status.isClean()) {
241
- throw new Error(
242
- "Repository is not clean. Please commit or stash any changes before upgrading"
243
- );
244
- }
245
- }
246
- );
247
- const REQUIRE_GIT_REPOSITORY = requirementFactory(
248
- "REQUIRE_GIT_REPOSITORY",
249
- async (context) => {
250
- const git = simpleGit__default.default({ baseDir: context.project.cwd });
251
- const isRepo = await git.checkIsRepo();
252
- if (!isRepo) {
253
- throw new Error("Not a git repository (or any of the parent directories)");
254
- }
255
- }
256
- ).addChild(REQUIRE_GIT_CLEAN_REPOSITORY.asOptional());
257
- const REQUIRE_GIT_INSTALLED = requirementFactory(
258
- "REQUIRE_GIT_INSTALLED",
259
- async (context) => {
260
- const git = simpleGit__default.default({ baseDir: context.project.cwd });
261
- try {
262
- await git.version();
263
- } catch {
264
- throw new Error("Git is not installed");
265
- }
266
- }
267
- ).addChild(REQUIRE_GIT_REPOSITORY.asOptional());
268
- const REQUIRE_GIT = requirementFactory("REQUIRE_GIT", null).addChild(
269
- REQUIRE_GIT_INSTALLED.asOptional()
270
- );
271
195
  class Timer {
272
196
  interval;
273
197
  constructor() {
@@ -294,46 +218,117 @@ class Timer {
294
218
  }
295
219
  const timerFactory = () => new Timer();
296
220
  const ONE_SECOND_MS = 1e3;
297
- class JSONTransformAPI {
298
- json;
299
- constructor(json) {
300
- this.json = fp.cloneDeep(json);
221
+ const path = (path2) => chalk__default.default.blue(path2);
222
+ const version$1 = (version2) => {
223
+ return chalk__default.default.italic.yellow(`v${version2}`);
224
+ };
225
+ const codemodUID = (uid) => {
226
+ return chalk__default.default.bold.cyan(uid);
227
+ };
228
+ const projectDetails = (project) => {
229
+ return `Project: TYPE=${projectType(project.type)}; CWD=${path(project.cwd)}; PATHS=${project.paths.map(path)}`;
230
+ };
231
+ const projectType = (type) => chalk__default.default.cyan(type);
232
+ const versionRange = (range) => chalk__default.default.italic.yellow(range.raw);
233
+ const highlight = (arg) => chalk__default.default.bold.underline(arg);
234
+ const upgradeStep = (text, step) => {
235
+ return chalk__default.default.bold(`(${step[0]}/${step[1]}) ${text}...`);
236
+ };
237
+ const reports = (reports2) => {
238
+ const rows = reports2.map(({ codemod, report }, i) => {
239
+ const fIndex = chalk__default.default.grey(i);
240
+ const fVersion = chalk__default.default.magenta(codemod.version);
241
+ const fKind = chalk__default.default.yellow(codemod.kind);
242
+ const fFormattedTransformPath = chalk__default.default.cyan(codemod.format());
243
+ const fTimeElapsed = i === 0 ? `${report.timeElapsed}s ${chalk__default.default.dim.italic("(cold start)")}` : `${report.timeElapsed}s`;
244
+ const fAffected = report.ok > 0 ? chalk__default.default.green(report.ok) : chalk__default.default.grey(0);
245
+ const fUnchanged = report.ok === 0 ? chalk__default.default.red(report.nochange) : chalk__default.default.grey(report.nochange);
246
+ return [fIndex, fVersion, fKind, fFormattedTransformPath, fAffected, fUnchanged, fTimeElapsed];
247
+ });
248
+ const table = new CliTable3__default.default({
249
+ style: { compact: true },
250
+ head: [
251
+ chalk__default.default.bold.grey("N°"),
252
+ chalk__default.default.bold.magenta("Version"),
253
+ chalk__default.default.bold.yellow("Kind"),
254
+ chalk__default.default.bold.cyan("Name"),
255
+ chalk__default.default.bold.green("Affected"),
256
+ chalk__default.default.bold.red("Unchanged"),
257
+ chalk__default.default.bold.blue("Duration")
258
+ ]
259
+ });
260
+ table.push(...rows);
261
+ return table.toString();
262
+ };
263
+ const codemodList = (codemods) => {
264
+ const rows = codemods.map((codemod, index) => {
265
+ const fIndex = chalk__default.default.grey(index);
266
+ const fVersion = chalk__default.default.magenta(codemod.version);
267
+ const fKind = chalk__default.default.yellow(codemod.kind);
268
+ const fName = chalk__default.default.blue(codemod.format());
269
+ const fUID = codemodUID(codemod.uid);
270
+ return [fIndex, fVersion, fKind, fName, fUID];
271
+ });
272
+ const table = new CliTable3__default.default({
273
+ style: { compact: true },
274
+ head: [
275
+ chalk__default.default.bold.grey("N°"),
276
+ chalk__default.default.bold.magenta("Version"),
277
+ chalk__default.default.bold.yellow("Kind"),
278
+ chalk__default.default.bold.blue("Name"),
279
+ chalk__default.default.bold.cyan("UID")
280
+ ]
281
+ });
282
+ table.push(...rows);
283
+ return table.toString();
284
+ };
285
+ const durationMs = (elapsedMs) => {
286
+ const elapsedSeconds = (elapsedMs / ONE_SECOND_MS).toFixed(3);
287
+ return `${elapsedSeconds}s`;
288
+ };
289
+ const NPM_REGISTRY_URL = "https://registry.npmjs.org";
290
+ class Package {
291
+ name;
292
+ packageURL;
293
+ npmPackage;
294
+ constructor(name) {
295
+ this.name = name;
296
+ this.packageURL = `${NPM_REGISTRY_URL}/${name}`;
297
+ this.npmPackage = null;
301
298
  }
302
- get(path2, defaultValue) {
303
- if (!path2) {
304
- return this.root();
305
- }
306
- return fp.cloneDeep(fp.get(path2, this.json) ?? defaultValue);
299
+ get isLoaded() {
300
+ return this.npmPackage !== null;
307
301
  }
308
- has(path2) {
309
- return fp.has(path2, this.json);
302
+ assertPackageIsLoaded(npmPackage) {
303
+ assert__default.default(this.isLoaded, "The package is not loaded yet");
310
304
  }
311
- merge(other) {
312
- this.json = fp.merge(other, this.json);
313
- return this;
305
+ getVersionsDict() {
306
+ this.assertPackageIsLoaded(this.npmPackage);
307
+ return this.npmPackage.versions;
314
308
  }
315
- root() {
316
- return fp.cloneDeep(this.json);
309
+ getVersionsAsList() {
310
+ this.assertPackageIsLoaded(this.npmPackage);
311
+ return Object.values(this.npmPackage.versions);
317
312
  }
318
- set(path2, value) {
319
- this.json = fp.set(path2, value, this.json);
320
- return this;
313
+ findVersionsInRange(range) {
314
+ const versions = this.getVersionsAsList();
315
+ return versions.filter((v) => range.test(v.version)).filter((v) => isLiteralSemVer(v.version)).sort((v1, v2) => semver__default.default.compare(v1.version, v2.version));
321
316
  }
322
- remove(path2) {
323
- this.json = fp.omit(path2, this.json);
317
+ findVersion(version2) {
318
+ const versions = this.getVersionsAsList();
319
+ return versions.find((npmVersion) => semver__default.default.eq(npmVersion.version, version2));
320
+ }
321
+ async refresh() {
322
+ const response = await fetch(this.packageURL);
323
+ assert__default.default(response.ok, `Request failed for ${this.packageURL}`);
324
+ this.npmPackage = await response.json();
324
325
  return this;
325
326
  }
327
+ versionExists(version2) {
328
+ return this.findVersion(version2) !== void 0;
329
+ }
326
330
  }
327
- const createJSONTransformAPI = (object) => new JSONTransformAPI(object);
328
- const readJSON = async (path2) => {
329
- const buffer = await fse__default.default.readFile(path2);
330
- return JSON.parse(buffer.toString());
331
- };
332
- const saveJSON = async (path2, json) => {
333
- const jsonAsString = `${JSON.stringify(json, null, 2)}
334
- `;
335
- await fse__default.default.writeFile(path2, jsonAsString);
336
- };
331
+ const npmPackageFactory = (name) => new Package(name);
337
332
  class FileScanner {
338
333
  cwd;
339
334
  constructor(cwd) {
@@ -362,15 +357,55 @@ class AbstractRunner {
362
357
  const runConfiguration = { ...this.configuration, ...configuration };
363
358
  return this.runner(codemod.path, this.paths, runConfiguration);
364
359
  }
365
- }
366
- class CodeRunner extends AbstractRunner {
367
- runner = Runner.run;
368
- valid(codemod) {
369
- return codemod.kind === "code";
360
+ }
361
+ class CodeRunner extends AbstractRunner {
362
+ runner = Runner.run;
363
+ valid(codemod) {
364
+ return codemod.kind === "code";
365
+ }
366
+ }
367
+ const codeRunnerFactory = (paths, configuration) => {
368
+ return new CodeRunner(paths, configuration);
369
+ };
370
+ class JSONTransformAPI {
371
+ json;
372
+ constructor(json) {
373
+ this.json = fp.cloneDeep(json);
374
+ }
375
+ get(path2, defaultValue) {
376
+ if (!path2) {
377
+ return this.root();
378
+ }
379
+ return fp.cloneDeep(fp.get(path2, this.json) ?? defaultValue);
380
+ }
381
+ has(path2) {
382
+ return fp.has(path2, this.json);
383
+ }
384
+ merge(other) {
385
+ this.json = fp.merge(other, this.json);
386
+ return this;
387
+ }
388
+ root() {
389
+ return fp.cloneDeep(this.json);
390
+ }
391
+ set(path2, value) {
392
+ this.json = fp.set(path2, value, this.json);
393
+ return this;
394
+ }
395
+ remove(path2) {
396
+ this.json = fp.omit(path2, this.json);
397
+ return this;
370
398
  }
371
399
  }
372
- const codeRunnerFactory = (paths, configuration) => {
373
- return new CodeRunner(paths, configuration);
400
+ const createJSONTransformAPI = (object) => new JSONTransformAPI(object);
401
+ const readJSON = async (path2) => {
402
+ const buffer = await fse__default.default.readFile(path2);
403
+ return JSON.parse(buffer.toString());
404
+ };
405
+ const saveJSON = async (path2, json) => {
406
+ const jsonAsString = `${JSON.stringify(json, null, 2)}
407
+ `;
408
+ await fse__default.default.writeFile(path2, jsonAsString);
374
409
  };
375
410
  const transformJSON = async (codemodPath, paths, config) => {
376
411
  const { dry } = config;
@@ -637,88 +672,6 @@ const projectFactory = (cwd) => {
637
672
  const isApplicationProject = (project) => {
638
673
  return project instanceof AppProject;
639
674
  };
640
- class UnexpectedError extends Error {
641
- constructor() {
642
- super("Unexpected Error");
643
- }
644
- }
645
- const unknownToError = (e) => {
646
- if (e instanceof Error) {
647
- return e;
648
- }
649
- if (typeof e === "string") {
650
- return new Error(e);
651
- }
652
- return new UnexpectedError();
653
- };
654
- const path = (path2) => chalk__default.default.blue(path2);
655
- const version$1 = (version2) => {
656
- return chalk__default.default.italic.yellow(`v${version2}`);
657
- };
658
- const codemodUID = (uid) => {
659
- return chalk__default.default.bold.cyan(uid);
660
- };
661
- const projectDetails = (project) => {
662
- return `Project: TYPE=${projectType(project.type)}; CWD=${path(project.cwd)}; PATHS=${project.paths.map(path)}`;
663
- };
664
- const projectType = (type) => chalk__default.default.cyan(type);
665
- const versionRange = (range) => chalk__default.default.italic.yellow(range.raw);
666
- const highlight = (arg) => chalk__default.default.bold.underline(arg);
667
- const upgradeStep = (text, step) => {
668
- return chalk__default.default.bold(`(${step[0]}/${step[1]}) ${text}...`);
669
- };
670
- const reports = (reports2) => {
671
- const rows = reports2.map(({ codemod, report }, i) => {
672
- const fIndex = chalk__default.default.grey(i);
673
- const fVersion = chalk__default.default.magenta(codemod.version);
674
- const fKind = chalk__default.default.yellow(codemod.kind);
675
- const fFormattedTransformPath = chalk__default.default.cyan(codemod.format());
676
- const fTimeElapsed = i === 0 ? `${report.timeElapsed}s ${chalk__default.default.dim.italic("(cold start)")}` : `${report.timeElapsed}s`;
677
- const fAffected = report.ok > 0 ? chalk__default.default.green(report.ok) : chalk__default.default.grey(0);
678
- const fUnchanged = report.ok === 0 ? chalk__default.default.red(report.nochange) : chalk__default.default.grey(report.nochange);
679
- return [fIndex, fVersion, fKind, fFormattedTransformPath, fAffected, fUnchanged, fTimeElapsed];
680
- });
681
- const table = new CliTable3__default.default({
682
- style: { compact: true },
683
- head: [
684
- chalk__default.default.bold.grey("N°"),
685
- chalk__default.default.bold.magenta("Version"),
686
- chalk__default.default.bold.yellow("Kind"),
687
- chalk__default.default.bold.cyan("Name"),
688
- chalk__default.default.bold.green("Affected"),
689
- chalk__default.default.bold.red("Unchanged"),
690
- chalk__default.default.bold.blue("Duration")
691
- ]
692
- });
693
- table.push(...rows);
694
- return table.toString();
695
- };
696
- const codemodList = (codemods) => {
697
- const rows = codemods.map((codemod, index) => {
698
- const fIndex = chalk__default.default.grey(index);
699
- const fVersion = chalk__default.default.magenta(codemod.version);
700
- const fKind = chalk__default.default.yellow(codemod.kind);
701
- const fName = chalk__default.default.blue(codemod.format());
702
- const fUID = codemodUID(codemod.uid);
703
- return [fIndex, fVersion, fKind, fName, fUID];
704
- });
705
- const table = new CliTable3__default.default({
706
- style: { compact: true },
707
- head: [
708
- chalk__default.default.bold.grey("N°"),
709
- chalk__default.default.bold.magenta("Version"),
710
- chalk__default.default.bold.yellow("Kind"),
711
- chalk__default.default.bold.blue("Name"),
712
- chalk__default.default.bold.cyan("UID")
713
- ]
714
- });
715
- table.push(...rows);
716
- return table.toString();
717
- };
718
- const durationMs = (elapsedMs) => {
719
- const elapsedSeconds = (elapsedMs / ONE_SECOND_MS).toFixed(3);
720
- return `${elapsedSeconds}s`;
721
- };
722
675
  const CODEMOD_CODE_SUFFIX = "code";
723
676
  const CODEMOD_JSON_SUFFIX = "json";
724
677
  const CODEMOD_ALLOWED_SUFFIXES = [CODEMOD_CODE_SUFFIX, CODEMOD_JSON_SUFFIX];
@@ -974,6 +927,15 @@ class Upgrader {
974
927
  this.logger = null;
975
928
  this.confirmationCallback = null;
976
929
  }
930
+ getNPMPackage() {
931
+ return this.npmPackage;
932
+ }
933
+ getProject() {
934
+ return this.project;
935
+ }
936
+ getTarget() {
937
+ return semVerFactory(this.target.raw);
938
+ }
977
939
  setRequirements(requirements) {
978
940
  this.requirements = requirements;
979
941
  return this;
@@ -1055,6 +1017,12 @@ class Upgrader {
1055
1017
  }
1056
1018
  return successReport();
1057
1019
  }
1020
+ async confirm(message) {
1021
+ if (typeof this.confirmationCallback !== "function") {
1022
+ return true;
1023
+ }
1024
+ return this.confirmationCallback(message);
1025
+ }
1058
1026
  async checkRequirements(requirements, context) {
1059
1027
  for (const requirement of requirements) {
1060
1028
  const { pass, error } = await requirement.test(context);
@@ -1126,7 +1094,7 @@ class Upgrader {
1126
1094
  const packageManagerName = await utils.packageManager.getPreferred(projectPath);
1127
1095
  this.logger?.debug?.(`Using ${highlight(packageManagerName)} as package manager`);
1128
1096
  if (this.isDry) {
1129
- this.logger?.debug?.(`Skipping dependencies installation (${chalk__default.default.italic("dry mode")}`);
1097
+ this.logger?.debug?.(`Skipping dependencies installation (${chalk__default.default.italic("dry mode")})`);
1130
1098
  return;
1131
1099
  }
1132
1100
  await utils.packageManager.installDependencies(projectPath, packageManagerName, {
@@ -1145,72 +1113,175 @@ class Upgrader {
1145
1113
  }
1146
1114
  const resolveNPMTarget = (project, target, npmPackage) => {
1147
1115
  if (isSemverInstance(target)) {
1148
- return npmPackage.findVersion(target);
1116
+ const version2 = npmPackage.findVersion(target);
1117
+ if (!version2) {
1118
+ throw new NPMCandidateNotFoundError(target);
1119
+ }
1120
+ return version2;
1149
1121
  }
1150
1122
  if (isSemVerReleaseType(target)) {
1151
1123
  const range = rangeFromVersions(project.strapiVersion, target);
1152
1124
  const npmVersionsMatches = npmPackage.findVersionsInRange(range);
1153
- return npmVersionsMatches.at(-1);
1125
+ const version2 = npmVersionsMatches.at(-1);
1126
+ if (!version2) {
1127
+ throw new NPMCandidateNotFoundError(range, `The project is already up-to-date (${target})`);
1128
+ }
1129
+ return version2;
1154
1130
  }
1155
- return void 0;
1131
+ throw new NPMCandidateNotFoundError(target);
1156
1132
  };
1157
1133
  const upgraderFactory = (project, target, npmPackage) => {
1158
- const targetedNPMVersion = resolveNPMTarget(project, target, npmPackage);
1159
- if (!targetedNPMVersion) {
1160
- throw new Error(`Couldn't find a matching version in the NPM registry for "${target}"`);
1161
- }
1162
- const semverTarget = semVerFactory(targetedNPMVersion.version);
1134
+ const npmTarget = resolveNPMTarget(project, target, npmPackage);
1135
+ const semverTarget = semVerFactory(npmTarget.version);
1163
1136
  if (semver__default.default.eq(semverTarget, project.strapiVersion)) {
1164
- throw new Error(`The project is already on ${version$1(semverTarget)}`);
1137
+ throw new Error(`The project is already using v${semverTarget}`);
1165
1138
  }
1166
1139
  return new Upgrader(project, semverTarget, npmPackage);
1167
1140
  };
1168
1141
  const successReport = () => ({ success: true, error: null });
1169
1142
  const erroredReport = (error) => ({ success: false, error });
1170
1143
  const STRAPI_PACKAGE_NAME = "@strapi/strapi";
1171
- const NPM_REGISTRY_URL = "https://registry.npmjs.org";
1172
- class Package {
1144
+ class Requirement {
1145
+ isRequired;
1173
1146
  name;
1174
- packageURL;
1175
- npmPackage;
1176
- constructor(name) {
1147
+ testCallback;
1148
+ children;
1149
+ constructor(name, testCallback, isRequired) {
1177
1150
  this.name = name;
1178
- this.packageURL = `${NPM_REGISTRY_URL}/${name}`;
1179
- this.npmPackage = null;
1151
+ this.testCallback = testCallback;
1152
+ this.isRequired = isRequired ?? true;
1153
+ this.children = [];
1180
1154
  }
1181
- get isLoaded() {
1182
- return this.npmPackage !== null;
1155
+ setChildren(children) {
1156
+ this.children = children;
1157
+ return this;
1183
1158
  }
1184
- assertPackageIsLoaded(npmPackage) {
1185
- assert__default.default(this.isLoaded, "The package is not loaded yet");
1159
+ addChild(child) {
1160
+ this.children.push(child);
1161
+ return this;
1186
1162
  }
1187
- getVersionsDict() {
1188
- this.assertPackageIsLoaded(this.npmPackage);
1189
- return this.npmPackage.versions;
1163
+ asOptional() {
1164
+ const newInstance = requirementFactory(this.name, this.testCallback, false);
1165
+ newInstance.setChildren(this.children);
1166
+ return newInstance;
1190
1167
  }
1191
- getVersionsAsList() {
1192
- this.assertPackageIsLoaded(this.npmPackage);
1193
- return Object.values(this.npmPackage.versions);
1168
+ asRequired() {
1169
+ const newInstance = requirementFactory(this.name, this.testCallback, true);
1170
+ newInstance.setChildren(this.children);
1171
+ return newInstance;
1194
1172
  }
1195
- findVersionsInRange(range) {
1196
- const versions = this.getVersionsAsList();
1197
- return versions.filter((v) => range.test(v.version)).filter((v) => isLiteralSemVer(v.version)).sort((v1, v2) => semver__default.default.compare(v1.version, v2.version));
1173
+ async test(context) {
1174
+ try {
1175
+ await this.testCallback?.(context);
1176
+ return ok();
1177
+ } catch (e) {
1178
+ if (e instanceof Error) {
1179
+ return errored(e);
1180
+ }
1181
+ if (typeof e === "string") {
1182
+ return errored(new Error(e));
1183
+ }
1184
+ return errored(new Error("Unknown error"));
1185
+ }
1198
1186
  }
1199
- findVersion(version2) {
1200
- const versions = this.getVersionsAsList();
1201
- return versions.find((npmVersion) => semver__default.default.eq(npmVersion.version, version2));
1187
+ }
1188
+ const ok = () => ({ pass: true, error: null });
1189
+ const errored = (error) => ({ pass: false, error });
1190
+ const requirementFactory = (name, testCallback, isRequired) => new Requirement(name, testCallback, isRequired);
1191
+ const REQUIRE_AVAILABLE_NEXT_MAJOR = requirementFactory(
1192
+ "REQUIRE_AVAILABLE_NEXT_MAJOR",
1193
+ (context) => {
1194
+ const { project, target } = context;
1195
+ const currentMajor = project.strapiVersion.major;
1196
+ const targetedMajor = target.major;
1197
+ if (targetedMajor === currentMajor) {
1198
+ throw new Error(`You're already on the latest major version (v${currentMajor})`);
1199
+ }
1202
1200
  }
1203
- async refresh() {
1204
- const response = await fetch(this.packageURL);
1205
- assert__default.default(response.ok, `Request failed for ${this.packageURL}`);
1206
- this.npmPackage = await response.json();
1207
- return this;
1201
+ );
1202
+ const REQUIRE_LATEST_FOR_CURRENT_MAJOR = requirementFactory(
1203
+ "REQUIRE_LATEST_FOR_CURRENT_MAJOR",
1204
+ (context) => {
1205
+ const { project, target, npmVersionsMatches } = context;
1206
+ const { major: currentMajor } = project.strapiVersion;
1207
+ const invalidMatches = npmVersionsMatches.filter(
1208
+ (match) => semVerFactory(match.version).major === currentMajor
1209
+ );
1210
+ if (invalidMatches.length > 0) {
1211
+ const invalidVersions = invalidMatches.map((match) => match.version);
1212
+ const invalidVersionsCount = invalidVersions.length;
1213
+ throw new Error(
1214
+ `Doing a major upgrade requires to be on the latest v${currentMajor} version, but found ${invalidVersionsCount} versions between the current one and ${target}. Please upgrade to ${invalidVersions.at(-1)} and try again.`
1215
+ );
1216
+ }
1208
1217
  }
1209
- versionExists(version2) {
1210
- return this.findVersion(version2) !== void 0;
1218
+ );
1219
+ const REQUIRE_GIT_CLEAN_REPOSITORY = requirementFactory(
1220
+ "REQUIRE_GIT_CLEAN_REPOSITORY",
1221
+ async (context) => {
1222
+ const git = simpleGit__default.default({ baseDir: context.project.cwd });
1223
+ const status = await git.status();
1224
+ if (!status.isClean()) {
1225
+ throw new Error(
1226
+ "Repository is not clean. Please commit or stash any changes before upgrading"
1227
+ );
1228
+ }
1211
1229
  }
1212
- }
1213
- const npmPackageFactory = (name) => new Package(name);
1230
+ );
1231
+ const REQUIRE_GIT_REPOSITORY = requirementFactory(
1232
+ "REQUIRE_GIT_REPOSITORY",
1233
+ async (context) => {
1234
+ const git = simpleGit__default.default({ baseDir: context.project.cwd });
1235
+ const isRepo = await git.checkIsRepo();
1236
+ if (!isRepo) {
1237
+ throw new Error("Not a git repository (or any of the parent directories)");
1238
+ }
1239
+ }
1240
+ ).addChild(REQUIRE_GIT_CLEAN_REPOSITORY.asOptional());
1241
+ const REQUIRE_GIT_INSTALLED = requirementFactory(
1242
+ "REQUIRE_GIT_INSTALLED",
1243
+ async (context) => {
1244
+ const git = simpleGit__default.default({ baseDir: context.project.cwd });
1245
+ try {
1246
+ await git.version();
1247
+ } catch {
1248
+ throw new Error("Git is not installed");
1249
+ }
1250
+ }
1251
+ ).addChild(REQUIRE_GIT_REPOSITORY.asOptional());
1252
+ const REQUIRE_GIT = requirementFactory("REQUIRE_GIT", null).addChild(
1253
+ REQUIRE_GIT_INSTALLED.asOptional()
1254
+ );
1255
+ const latest = async (upgrader, options) => {
1256
+ if (options.target !== ReleaseType.Latest) {
1257
+ return;
1258
+ }
1259
+ const npmPackage = upgrader.getNPMPackage();
1260
+ const target = upgrader.getTarget();
1261
+ const project = upgrader.getProject();
1262
+ const { strapiVersion: current } = project;
1263
+ const fTargetMajor = highlight(`v${target.major}`);
1264
+ const fCurrentMajor = highlight(`v${current.major}`);
1265
+ const fTarget = version$1(target);
1266
+ const fCurrent = version$1(current);
1267
+ const isMajorUpgrade = target.major > current.major;
1268
+ if (isMajorUpgrade) {
1269
+ options.logger.warn(
1270
+ `Detected a major upgrade for the "${highlight(ReleaseType.Latest)}" tag: ${fCurrent} > ${fTarget}`
1271
+ );
1272
+ const newerPackageRelease = npmPackage.findVersionsInRange(rangeFactory(`>${current.raw} <${target.major}`)).at(-1);
1273
+ if (newerPackageRelease) {
1274
+ const fLatest = version$1(semVerFactory(newerPackageRelease.version));
1275
+ options.logger.warn(
1276
+ `It's recommended to first upgrade to the latest version of ${fCurrentMajor} (${fLatest}) before upgrading to ${fTargetMajor}.`
1277
+ );
1278
+ }
1279
+ const proceedAnyway = await upgrader.confirm(`I know what I'm doing. Proceed anyway!`);
1280
+ if (!proceedAnyway) {
1281
+ throw new AbortedError();
1282
+ }
1283
+ }
1284
+ };
1214
1285
  const upgrade$1 = async (options) => {
1215
1286
  const timer = timerFactory();
1216
1287
  const { logger, codemodsTarget } = options;
@@ -1222,22 +1293,34 @@ const upgrade$1 = async (options) => {
1222
1293
  `The "${options.target}" upgrade can only be run on a Strapi project; for plugins, please use "codemods".`
1223
1294
  );
1224
1295
  }
1296
+ logger.debug(
1297
+ `Application: VERSION=${version$1(project.packageJSON.version)}; STRAPI_VERSION=${version$1(project.strapiVersion)}`
1298
+ );
1225
1299
  const npmPackage = npmPackageFactory(STRAPI_PACKAGE_NAME);
1226
1300
  await npmPackage.refresh();
1227
1301
  const upgrader = upgraderFactory(project, options.target, npmPackage).dry(options.dry ?? false).onConfirm(options.confirm ?? null).setLogger(logger);
1228
1302
  if (codemodsTarget !== void 0) {
1229
1303
  upgrader.overrideCodemodsTarget(codemodsTarget);
1230
1304
  }
1231
- if (options.target === ReleaseType.Major) {
1232
- upgrader.addRequirement(REQUIRE_AVAILABLE_NEXT_MAJOR).addRequirement(REQUIRE_LATEST_FOR_CURRENT_MAJOR);
1233
- }
1234
- upgrader.addRequirement(REQUIRE_GIT.asOptional());
1305
+ await runUpgradePrompts(upgrader, options);
1306
+ addUpgradeRequirements(upgrader, options);
1235
1307
  const upgradeReport = await upgrader.upgrade();
1236
1308
  if (!upgradeReport.success) {
1237
1309
  throw upgradeReport.error;
1238
1310
  }
1239
1311
  timer.stop();
1240
- logger.info(`Completed in ${durationMs(timer.elapsedMs)}`);
1312
+ logger.info(`Completed in ${durationMs(timer.elapsedMs)}ms`);
1313
+ };
1314
+ const runUpgradePrompts = async (upgrader, options) => {
1315
+ if (options.target === ReleaseType.Latest) {
1316
+ await latest(upgrader, options);
1317
+ }
1318
+ };
1319
+ const addUpgradeRequirements = (upgrader, options) => {
1320
+ if (options.target === ReleaseType.Major) {
1321
+ upgrader.addRequirement(REQUIRE_AVAILABLE_NEXT_MAJOR).addRequirement(REQUIRE_LATEST_FOR_CURRENT_MAJOR);
1322
+ }
1323
+ upgrader.addRequirement(REQUIRE_GIT.asOptional());
1241
1324
  };
1242
1325
  const resolvePath = (cwd) => path__default.default.resolve(cwd ?? process.cwd());
1243
1326
  const getRangeFromTarget = (currentVersion, target) => {
@@ -1246,6 +1329,8 @@ const getRangeFromTarget = (currentVersion, target) => {
1246
1329
  }
1247
1330
  const { major, minor, patch } = currentVersion;
1248
1331
  switch (target) {
1332
+ case ReleaseType.Latest:
1333
+ throw new Error("Can't use <latest> to create a codemods range: not implemented");
1249
1334
  case ReleaseType.Major:
1250
1335
  return rangeFactory(`${major}`);
1251
1336
  case ReleaseType.Minor:
@@ -1365,6 +1450,10 @@ const register$1 = (program) => {
1365
1450
  return upgrade({ ...options, target: releaseType });
1366
1451
  });
1367
1452
  };
1453
+ addReleaseUpgradeCommand(
1454
+ ReleaseType.Latest,
1455
+ "Upgrade to the latest available version of Strapi"
1456
+ );
1368
1457
  addReleaseUpgradeCommand(
1369
1458
  ReleaseType.Major,
1370
1459
  "Upgrade to the next available major version of Strapi"
@@ -1477,7 +1566,7 @@ When executed on a Strapi plugin project, it shows every codemods.
1477
1566
  return listCodemods(options);
1478
1567
  });
1479
1568
  };
1480
- const version = "5.0.5";
1569
+ const version = "5.1.0";
1481
1570
  register$1(commander.program);
1482
1571
  register(commander.program);
1483
1572
  commander.program.usage("<command> [options]").on("command:*", ([invalidCmd]) => {