@strapi/upgrade 5.9.0 → 5.10.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.
@@ -0,0 +1,1649 @@
1
+ 'use strict';
2
+
3
+ var path$1 = require('node:path');
4
+ var assert = require('node:assert');
5
+ var semver = require('semver');
6
+ var chalk = require('chalk');
7
+ var utils = require('@strapi/utils');
8
+ var fse = require('fs-extra');
9
+ var fastglob = require('fast-glob');
10
+ var Runner = require('jscodeshift/src/Runner');
11
+ var fp = require('lodash/fp');
12
+ var node = require('esbuild-register/dist/node');
13
+ var simpleGit = require('simple-git');
14
+ var CliTable3 = require('cli-table3');
15
+
16
+ class Timer {
17
+ get elapsedMs() {
18
+ const { start, end } = this.interval;
19
+ return end ? end - start : Date.now() - start;
20
+ }
21
+ get end() {
22
+ return this.interval.end;
23
+ }
24
+ get start() {
25
+ return this.interval.start;
26
+ }
27
+ stop() {
28
+ this.interval.end = Date.now();
29
+ return this.elapsedMs;
30
+ }
31
+ reset() {
32
+ this.interval = {
33
+ start: Date.now(),
34
+ end: null
35
+ };
36
+ return this;
37
+ }
38
+ constructor(){
39
+ this.reset();
40
+ }
41
+ }
42
+ const timerFactory = ()=>new Timer();
43
+
44
+ const ONE_SECOND_MS = 1000;
45
+
46
+ var constants$4 = /*#__PURE__*/Object.freeze({
47
+ __proto__: null,
48
+ ONE_SECOND_MS: ONE_SECOND_MS
49
+ });
50
+
51
+ const path = (path)=>chalk.blue(path);
52
+ const version = (version)=>{
53
+ return chalk.italic.yellow(`v${version}`);
54
+ };
55
+ const codemodUID = (uid)=>{
56
+ return chalk.bold.cyan(uid);
57
+ };
58
+ const projectDetails = (project)=>{
59
+ return `Project: TYPE=${projectType(project.type)}; CWD=${path(project.cwd)}; PATHS=${project.paths.map(path)}`;
60
+ };
61
+ const projectType = (type)=>chalk.cyan(type);
62
+ const versionRange = (range)=>chalk.italic.yellow(range.raw);
63
+ const transform = (transformFilePath)=>chalk.cyan(transformFilePath);
64
+ const highlight = (arg)=>chalk.bold.underline(arg);
65
+ const upgradeStep = (text, step)=>{
66
+ return chalk.bold(`(${step[0]}/${step[1]}) ${text}...`);
67
+ };
68
+ const reports = (reports)=>{
69
+ const rows = reports.map(({ codemod, report }, i)=>{
70
+ const fIndex = chalk.grey(i);
71
+ const fVersion = chalk.magenta(codemod.version);
72
+ const fKind = chalk.yellow(codemod.kind);
73
+ const fFormattedTransformPath = chalk.cyan(codemod.format());
74
+ const fTimeElapsed = i === 0 ? `${report.timeElapsed}s ${chalk.dim.italic('(cold start)')}` : `${report.timeElapsed}s`;
75
+ const fAffected = report.ok > 0 ? chalk.green(report.ok) : chalk.grey(0);
76
+ const fUnchanged = report.ok === 0 ? chalk.red(report.nochange) : chalk.grey(report.nochange);
77
+ return [
78
+ fIndex,
79
+ fVersion,
80
+ fKind,
81
+ fFormattedTransformPath,
82
+ fAffected,
83
+ fUnchanged,
84
+ fTimeElapsed
85
+ ];
86
+ });
87
+ const table = new CliTable3({
88
+ style: {
89
+ compact: true
90
+ },
91
+ head: [
92
+ chalk.bold.grey('N°'),
93
+ chalk.bold.magenta('Version'),
94
+ chalk.bold.yellow('Kind'),
95
+ chalk.bold.cyan('Name'),
96
+ chalk.bold.green('Affected'),
97
+ chalk.bold.red('Unchanged'),
98
+ chalk.bold.blue('Duration')
99
+ ]
100
+ });
101
+ table.push(...rows);
102
+ return table.toString();
103
+ };
104
+ const codemodList = (codemods)=>{
105
+ const rows = codemods.map((codemod, index)=>{
106
+ const fIndex = chalk.grey(index);
107
+ const fVersion = chalk.magenta(codemod.version);
108
+ const fKind = chalk.yellow(codemod.kind);
109
+ const fName = chalk.blue(codemod.format());
110
+ const fUID = codemodUID(codemod.uid);
111
+ return [
112
+ fIndex,
113
+ fVersion,
114
+ fKind,
115
+ fName,
116
+ fUID
117
+ ];
118
+ });
119
+ const table = new CliTable3({
120
+ style: {
121
+ compact: true
122
+ },
123
+ head: [
124
+ chalk.bold.grey('N°'),
125
+ chalk.bold.magenta('Version'),
126
+ chalk.bold.yellow('Kind'),
127
+ chalk.bold.blue('Name'),
128
+ chalk.bold.cyan('UID')
129
+ ]
130
+ });
131
+ table.push(...rows);
132
+ return table.toString();
133
+ };
134
+ const durationMs = (elapsedMs)=>{
135
+ const elapsedSeconds = (elapsedMs / ONE_SECOND_MS).toFixed(3);
136
+ return `${elapsedSeconds}s`;
137
+ };
138
+
139
+ const NPM_REGISTRY_URL = 'https://registry.npmjs.org';
140
+
141
+ exports.ReleaseType = void 0;
142
+ (function(ReleaseType) {
143
+ // Classic
144
+ ReleaseType["Major"] = "major";
145
+ ReleaseType["Minor"] = "minor";
146
+ ReleaseType["Patch"] = "patch";
147
+ // Other
148
+ ReleaseType["Latest"] = "latest";
149
+ })(exports.ReleaseType || (exports.ReleaseType = {}));
150
+
151
+ var types = /*#__PURE__*/Object.freeze({
152
+ __proto__: null,
153
+ get ReleaseType () { return exports.ReleaseType; }
154
+ });
155
+
156
+ const semVerFactory = (version)=>{
157
+ return new semver.SemVer(version);
158
+ };
159
+ const isLiteralSemVer = (str)=>{
160
+ const tokens = str.split('.');
161
+ return tokens.length === 3 && tokens.every((token)=>!Number.isNaN(+token) && Number.isInteger(+token));
162
+ };
163
+ const isValidSemVer = (str)=>semver.valid(str) !== null;
164
+ const isSemverInstance = (value)=>{
165
+ return value instanceof semver.SemVer;
166
+ };
167
+ const isSemVerReleaseType = (str)=>{
168
+ return Object.values(exports.ReleaseType).includes(str);
169
+ };
170
+
171
+ const rangeFactory = (range)=>{
172
+ return new semver.Range(range);
173
+ };
174
+ const rangeFromReleaseType = (current, identifier)=>{
175
+ switch(identifier){
176
+ case exports.ReleaseType.Latest:
177
+ {
178
+ // Match anything greater than the current version
179
+ return rangeFactory(`>${current.raw}`);
180
+ }
181
+ case exports.ReleaseType.Major:
182
+ {
183
+ // For example, 4.15.4 returns 5.0.0
184
+ const nextMajor = semVerFactory(current.raw).inc('major');
185
+ // Using only the major version as the upper limit allows any minor,
186
+ // patch, or build version to be taken in the range.
187
+ //
188
+ // For example, if the current version is "4.15.4", incrementing the
189
+ // major version would result in "5.0.0".
190
+ // The generated rule is ">4.15.4 <=5", allowing any version
191
+ // greater than "4.15.4" but less than "6.0.0-0".
192
+ return rangeFactory(`>${current.raw} <=${nextMajor.major}`);
193
+ }
194
+ case exports.ReleaseType.Minor:
195
+ {
196
+ // For example, 4.15.4 returns 5.0.0
197
+ const nextMajor = semVerFactory(current.raw).inc('major');
198
+ // Using the <major>.<minor>.<patch> version as the upper limit allows any minor,
199
+ // patch, or build versions to be taken in the range.
200
+ //
201
+ // For example, if the current version is "4.15.4", incrementing the
202
+ // major version would result in "5.0.0".
203
+ // The generated rule is ">4.15.4 <5.0.0", allowing any version
204
+ // greater than "4.15.4" but less than "5.0.0".
205
+ return rangeFactory(`>${current.raw} <${nextMajor.raw}`);
206
+ }
207
+ case exports.ReleaseType.Patch:
208
+ {
209
+ // For example, 4.15.4 returns 4.16.0
210
+ const nextMinor = semVerFactory(current.raw).inc('minor');
211
+ // Using only the minor version as the upper limit allows any patch
212
+ // or build versions to be taken in the range.
213
+ //
214
+ // For example, if the current version is "4.15.4", incrementing the
215
+ // minor version would result in "4.16.0".
216
+ // The generated rule is ">4.15.4 <4.16.0", allowing any version
217
+ // greater than "4.15.4" but less than "4.16.0".
218
+ return rangeFactory(`>${current.raw} <${nextMinor.raw}`);
219
+ }
220
+ default:
221
+ {
222
+ throw new Error('Not implemented');
223
+ }
224
+ }
225
+ };
226
+ const rangeFromVersions = (currentVersion, target)=>{
227
+ if (isSemverInstance(target)) {
228
+ return rangeFactory(`>${currentVersion.raw} <=${target.raw}`);
229
+ }
230
+ if (isSemVerReleaseType(target)) {
231
+ return rangeFromReleaseType(currentVersion, target);
232
+ }
233
+ throw new Error(`Invalid target set: ${target}`); // TODO: better errors
234
+ };
235
+ const isValidStringifiedRange = (str)=>semver.validRange(str) !== null;
236
+ const isRangeInstance = (range)=>{
237
+ return range instanceof semver.Range;
238
+ };
239
+
240
+ class Package {
241
+ get isLoaded() {
242
+ return this.npmPackage !== null;
243
+ }
244
+ assertPackageIsLoaded(npmPackage) {
245
+ assert(this.isLoaded, 'The package is not loaded yet');
246
+ }
247
+ getVersionsDict() {
248
+ this.assertPackageIsLoaded(this.npmPackage);
249
+ return this.npmPackage.versions;
250
+ }
251
+ getVersionsAsList() {
252
+ this.assertPackageIsLoaded(this.npmPackage);
253
+ return Object.values(this.npmPackage.versions);
254
+ }
255
+ findVersionsInRange(range) {
256
+ const versions = this.getVersionsAsList();
257
+ return versions// Only select versions matching the upgrade range
258
+ .filter((v)=>range.test(v.version))// Only select supported version format (x.x.x)
259
+ .filter((v)=>isLiteralSemVer(v.version))// Sort in ascending order
260
+ .sort((v1, v2)=>semver.compare(v1.version, v2.version));
261
+ }
262
+ findVersion(version) {
263
+ const versions = this.getVersionsAsList();
264
+ return versions.find((npmVersion)=>semver.eq(npmVersion.version, version));
265
+ }
266
+ async refresh() {
267
+ const response = await fetch(this.packageURL);
268
+ // TODO: Use a validation library to make sure the response structure is correct
269
+ assert(response.ok, `Request failed for ${this.packageURL}`);
270
+ this.npmPackage = await response.json();
271
+ return this;
272
+ }
273
+ versionExists(version) {
274
+ return this.findVersion(version) !== undefined;
275
+ }
276
+ constructor(name){
277
+ this.name = name;
278
+ this.packageURL = `${NPM_REGISTRY_URL}/${name}`;
279
+ this.npmPackage = null;
280
+ }
281
+ }
282
+ const npmPackageFactory = (name)=>new Package(name);
283
+
284
+ class FileScanner {
285
+ scan(patterns) {
286
+ // we use fastglob instead of glob because it supports negation patterns
287
+ const filenames = fastglob.sync(patterns, {
288
+ cwd: this.cwd
289
+ });
290
+ // Resolve the full paths for every filename
291
+ return filenames.map((filename)=>path$1.join(this.cwd, filename));
292
+ }
293
+ constructor(cwd){
294
+ this.cwd = cwd;
295
+ }
296
+ }
297
+ const fileScannerFactory = (cwd)=>new FileScanner(cwd);
298
+
299
+ class AbstractRunner {
300
+ async run(codemod, configuration) {
301
+ const isValidCodemod = this.valid(codemod);
302
+ if (!isValidCodemod) {
303
+ throw new Error(`Invalid codemod provided to the runner: ${codemod.filename}`);
304
+ }
305
+ const runConfiguration = {
306
+ ...this.configuration,
307
+ ...configuration
308
+ };
309
+ return this.runner(codemod.path, this.paths, runConfiguration);
310
+ }
311
+ constructor(paths, configuration){
312
+ this.paths = paths;
313
+ this.configuration = configuration;
314
+ }
315
+ }
316
+
317
+ class CodeRunner extends AbstractRunner {
318
+ valid(codemod) {
319
+ return codemod.kind === 'code';
320
+ }
321
+ constructor(...args){
322
+ super(...args);
323
+ this.runner = Runner.run;
324
+ }
325
+ }
326
+ const codeRunnerFactory = (paths, configuration)=>{
327
+ return new CodeRunner(paths, configuration);
328
+ };
329
+
330
+ class JSONTransformAPI {
331
+ get(path, defaultValue) {
332
+ if (!path) {
333
+ return this.root();
334
+ }
335
+ return fp.cloneDeep(fp.get(path, this.json) ?? defaultValue);
336
+ }
337
+ has(path) {
338
+ return fp.has(path, this.json);
339
+ }
340
+ merge(other) {
341
+ this.json = fp.merge(other, this.json);
342
+ return this;
343
+ }
344
+ root() {
345
+ return fp.cloneDeep(this.json);
346
+ }
347
+ set(path, value) {
348
+ this.json = fp.set(path, value, this.json);
349
+ return this;
350
+ }
351
+ remove(path) {
352
+ this.json = fp.omit(path, this.json);
353
+ return this;
354
+ }
355
+ constructor(json){
356
+ this.json = fp.cloneDeep(json);
357
+ }
358
+ }
359
+ const createJSONTransformAPI = (object)=>new JSONTransformAPI(object);
360
+
361
+ const readJSON = async (path)=>{
362
+ const buffer = await fse.readFile(path);
363
+ return JSON.parse(buffer.toString());
364
+ };
365
+ const saveJSON = async (path, json)=>{
366
+ const jsonAsString = `${JSON.stringify(json, null, 2)}\n`;
367
+ await fse.writeFile(path, jsonAsString);
368
+ };
369
+
370
+ const transformJSON = async (codemodPath, paths, config)=>{
371
+ const { dry } = config;
372
+ const startTime = process.hrtime();
373
+ const report = {
374
+ ok: 0,
375
+ nochange: 0,
376
+ skip: 0,
377
+ error: 0,
378
+ timeElapsed: '',
379
+ stats: {}
380
+ };
381
+ /**
382
+ * Why do we need to include node_modules (hookIgnoreNodeModules) and specify a matcher (hookMatcher) to esbuild?
383
+ *
384
+ * When using tools like npx or dlx, the execution context is different from when running the program in a local
385
+ * project. npx and dlx run the commands in a temporary installation, which is isolated from local project files.
386
+ *
387
+ * When hookIgnoreNodeModules is not specified (or set to true), esbuild-register instructs
388
+ * Pirates (https://github.com/danez/pirates) to not transpile any files that come from node_modules.
389
+ *
390
+ * Now, when using npx or dlx to run a script, its location can be seen as "external" because it's not part of
391
+ * the temporary environment where npx or dlx execute. Therefore, it's considered to be part of node_modules.
392
+ *
393
+ * Due to this, if hookIgnoreNodeModules is set to true or left unspecified,
394
+ * esbuild-register won't try to compile them upon require.
395
+ *
396
+ * hookMatcher is added to make sure we're not matching anything else than our codemod in external directories.
397
+ */ const esbuildOptions = {
398
+ extensions: [
399
+ '.js',
400
+ '.mjs',
401
+ '.ts'
402
+ ],
403
+ hookIgnoreNodeModules: false,
404
+ hookMatcher: fp.isEqual(codemodPath)
405
+ };
406
+ const { unregister } = node.register(esbuildOptions);
407
+ const module = require(codemodPath);
408
+ unregister();
409
+ const codemod = typeof module.default === 'function' ? module.default : module;
410
+ assert(typeof codemod === 'function', `Codemod must be a function. Found ${typeof codemod}`);
411
+ for (const path of paths){
412
+ try {
413
+ const json = await readJSON(path);
414
+ // Make sure the JSON value is a JSON object
415
+ assert(typeof json === 'object' && !Array.isArray(json) && json !== null);
416
+ // TODO: Optimize the API to limit parse/stringify operations
417
+ const file = {
418
+ path,
419
+ json
420
+ };
421
+ const params = {
422
+ cwd: config.cwd,
423
+ json: createJSONTransformAPI
424
+ };
425
+ const out = await codemod(file, params);
426
+ if (out === undefined) {
427
+ report.error += 1;
428
+ } else if (!fp.isEqual(json, out)) {
429
+ if (!dry) {
430
+ await saveJSON(path, out);
431
+ }
432
+ report.ok += 1;
433
+ } else {
434
+ report.nochange += 1;
435
+ }
436
+ } catch {
437
+ report.error += 1;
438
+ }
439
+ }
440
+ const endTime = process.hrtime(startTime);
441
+ report.timeElapsed = (endTime[0] + endTime[1] / 1e9).toFixed(3);
442
+ return report;
443
+ };
444
+
445
+ class JSONRunner extends AbstractRunner {
446
+ valid(codemod) {
447
+ return codemod.kind === 'json';
448
+ }
449
+ constructor(...args){
450
+ super(...args);
451
+ this.runner = transformJSON;
452
+ }
453
+ }
454
+ const jsonRunnerFactory = (paths, configuration)=>{
455
+ return new JSONRunner(paths, configuration);
456
+ };
457
+
458
+ const PROJECT_PACKAGE_JSON = 'package.json';
459
+ const PROJECT_APP_ALLOWED_ROOT_PATHS = [
460
+ 'src',
461
+ 'config',
462
+ 'public'
463
+ ];
464
+ const PROJECT_PLUGIN_ALLOWED_ROOT_PATHS = [
465
+ 'admin',
466
+ 'server'
467
+ ];
468
+ const PROJECT_PLUGIN_ROOT_FILES = [
469
+ 'strapi-admin.js',
470
+ 'strapi-server.js'
471
+ ];
472
+ const PROJECT_CODE_EXTENSIONS = [
473
+ // Source files
474
+ 'js',
475
+ 'mjs',
476
+ 'ts',
477
+ // React files
478
+ 'jsx',
479
+ 'tsx'
480
+ ];
481
+ const PROJECT_JSON_EXTENSIONS = [
482
+ 'json'
483
+ ];
484
+ const PROJECT_ALLOWED_EXTENSIONS = [
485
+ ...PROJECT_CODE_EXTENSIONS,
486
+ ...PROJECT_JSON_EXTENSIONS
487
+ ];
488
+ const SCOPED_STRAPI_PACKAGE_PREFIX = '@strapi/';
489
+ const STRAPI_DEPENDENCY_NAME = `${SCOPED_STRAPI_PACKAGE_PREFIX}strapi`;
490
+
491
+ var constants$3 = /*#__PURE__*/Object.freeze({
492
+ __proto__: null,
493
+ PROJECT_ALLOWED_EXTENSIONS: PROJECT_ALLOWED_EXTENSIONS,
494
+ PROJECT_APP_ALLOWED_ROOT_PATHS: PROJECT_APP_ALLOWED_ROOT_PATHS,
495
+ PROJECT_CODE_EXTENSIONS: PROJECT_CODE_EXTENSIONS,
496
+ PROJECT_JSON_EXTENSIONS: PROJECT_JSON_EXTENSIONS,
497
+ PROJECT_PACKAGE_JSON: PROJECT_PACKAGE_JSON,
498
+ PROJECT_PLUGIN_ALLOWED_ROOT_PATHS: PROJECT_PLUGIN_ALLOWED_ROOT_PATHS,
499
+ PROJECT_PLUGIN_ROOT_FILES: PROJECT_PLUGIN_ROOT_FILES,
500
+ SCOPED_STRAPI_PACKAGE_PREFIX: SCOPED_STRAPI_PACKAGE_PREFIX,
501
+ STRAPI_DEPENDENCY_NAME: STRAPI_DEPENDENCY_NAME
502
+ });
503
+
504
+ class Project {
505
+ getFilesByExtensions(extensions) {
506
+ return this.files.filter((filePath)=>{
507
+ const fileExtension = path$1.extname(filePath);
508
+ return extensions.includes(fileExtension);
509
+ });
510
+ }
511
+ refresh() {
512
+ this.refreshPackageJSON();
513
+ this.refreshProjectFiles();
514
+ return this;
515
+ }
516
+ async runCodemods(codemods, options) {
517
+ const runners = this.createProjectCodemodsRunners(options.dry);
518
+ const reports = [];
519
+ for (const codemod of codemods){
520
+ for (const runner of runners){
521
+ if (runner.valid(codemod)) {
522
+ const report = await runner.run(codemod);
523
+ reports.push({
524
+ codemod,
525
+ report
526
+ });
527
+ }
528
+ }
529
+ }
530
+ return reports;
531
+ }
532
+ createProjectCodemodsRunners(dry = false) {
533
+ const jsonExtensions = PROJECT_JSON_EXTENSIONS.map((ext)=>`.${ext}`);
534
+ const codeExtensions = PROJECT_CODE_EXTENSIONS.map((ext)=>`.${ext}`);
535
+ const jsonFiles = this.getFilesByExtensions(jsonExtensions);
536
+ const codeFiles = this.getFilesByExtensions(codeExtensions);
537
+ const codeRunner = codeRunnerFactory(codeFiles, {
538
+ dry,
539
+ parser: 'ts',
540
+ runInBand: true,
541
+ babel: true,
542
+ extensions: PROJECT_CODE_EXTENSIONS.join(','),
543
+ // Don't output any log coming from the runner
544
+ print: false,
545
+ silent: true,
546
+ verbose: 0
547
+ });
548
+ const jsonRunner = jsonRunnerFactory(jsonFiles, {
549
+ dry,
550
+ cwd: this.cwd
551
+ });
552
+ return [
553
+ codeRunner,
554
+ jsonRunner
555
+ ];
556
+ }
557
+ refreshPackageJSON() {
558
+ const packageJSONPath = path$1.join(this.cwd, PROJECT_PACKAGE_JSON);
559
+ try {
560
+ fse.accessSync(packageJSONPath);
561
+ } catch {
562
+ throw new Error(`Could not find a ${PROJECT_PACKAGE_JSON} file in ${this.cwd}`);
563
+ }
564
+ const packageJSONBuffer = fse.readFileSync(packageJSONPath);
565
+ this.packageJSONPath = packageJSONPath;
566
+ this.packageJSON = JSON.parse(packageJSONBuffer.toString());
567
+ }
568
+ refreshProjectFiles() {
569
+ const scanner = fileScannerFactory(this.cwd);
570
+ this.files = scanner.scan(this.paths);
571
+ }
572
+ constructor(cwd, config){
573
+ if (!fse.pathExistsSync(cwd)) {
574
+ throw new Error(`ENOENT: no such file or directory, access '${cwd}'`);
575
+ }
576
+ this.cwd = cwd;
577
+ this.paths = config.paths;
578
+ this.refresh();
579
+ }
580
+ }
581
+ class AppProject extends Project {
582
+ /**
583
+ * Returns an array of allowed file paths for a Strapi application
584
+ *
585
+ * The resulting paths include app default files and the root package.json file.
586
+ */ static get paths() {
587
+ const allowedRootPaths = formatGlobCollectionPattern(PROJECT_APP_ALLOWED_ROOT_PATHS);
588
+ const allowedExtensions = formatGlobCollectionPattern(PROJECT_ALLOWED_EXTENSIONS);
589
+ return [
590
+ // App default files
591
+ `./${allowedRootPaths}/**/*.${allowedExtensions}`,
592
+ `!./**/node_modules/**/*`,
593
+ `!./**/dist/**/*`,
594
+ // Root package.json file
595
+ PROJECT_PACKAGE_JSON
596
+ ];
597
+ }
598
+ refresh() {
599
+ super.refresh();
600
+ this.refreshStrapiVersion();
601
+ return this;
602
+ }
603
+ refreshStrapiVersion() {
604
+ this.strapiVersion = // First try to get the strapi version from the package.json dependencies
605
+ this.findStrapiVersionFromProjectPackageJSON() ?? // If the version found is not a valid SemVer, get the Strapi version from the installed package
606
+ this.findLocallyInstalledStrapiVersion();
607
+ }
608
+ findStrapiVersionFromProjectPackageJSON() {
609
+ const projectName = this.packageJSON.name;
610
+ const version = this.packageJSON.dependencies?.[STRAPI_DEPENDENCY_NAME];
611
+ if (version === undefined) {
612
+ throw new Error(`No version of ${STRAPI_DEPENDENCY_NAME} was found in ${projectName}. Are you in a valid Strapi project?`);
613
+ }
614
+ const isValidSemVer = isLiteralSemVer(version) && semver.valid(version) === version;
615
+ // We return undefined only if a strapi/strapi version is found, but it's not semver compliant
616
+ return isValidSemVer ? semVerFactory(version) : undefined;
617
+ }
618
+ findLocallyInstalledStrapiVersion() {
619
+ const packageSearchText = `${STRAPI_DEPENDENCY_NAME}/package.json`;
620
+ let strapiPackageJSONPath;
621
+ let strapiPackageJSON;
622
+ try {
623
+ strapiPackageJSONPath = require.resolve(packageSearchText, {
624
+ paths: [
625
+ this.cwd
626
+ ]
627
+ });
628
+ strapiPackageJSON = require(strapiPackageJSONPath);
629
+ assert(typeof strapiPackageJSON === 'object');
630
+ } catch {
631
+ throw new Error(`Cannot resolve module "${STRAPI_DEPENDENCY_NAME}" from paths [${this.cwd}]`);
632
+ }
633
+ const strapiVersion = strapiPackageJSON.version;
634
+ if (!isValidSemVer(strapiVersion)) {
635
+ throw new Error(`Invalid ${STRAPI_DEPENDENCY_NAME} version found in ${strapiPackageJSONPath} (${strapiVersion})`);
636
+ }
637
+ return semVerFactory(strapiVersion);
638
+ }
639
+ constructor(cwd){
640
+ super(cwd, {
641
+ paths: AppProject.paths
642
+ });
643
+ this.type = 'application';
644
+ this.refreshStrapiVersion();
645
+ }
646
+ }
647
+ const formatGlobCollectionPattern = (collection)=>{
648
+ assert(collection.length > 0, 'Invalid pattern provided, the given collection needs at least 1 element');
649
+ return collection.length === 1 ? collection[0] : `{${collection}}`;
650
+ };
651
+ class PluginProject extends Project {
652
+ /**
653
+ * Returns an array of allowed file paths for a Strapi plugin
654
+ *
655
+ * The resulting paths include plugin default files, the root package.json file, and plugin-specific files.
656
+ */ static get paths() {
657
+ const allowedRootPaths = formatGlobCollectionPattern(PROJECT_PLUGIN_ALLOWED_ROOT_PATHS);
658
+ const allowedExtensions = formatGlobCollectionPattern(PROJECT_ALLOWED_EXTENSIONS);
659
+ return [
660
+ // Plugin default files
661
+ `./${allowedRootPaths}/**/*.${allowedExtensions}`,
662
+ `!./**/node_modules/**/*`,
663
+ `!./**/dist/**/*`,
664
+ // Root package.json file
665
+ PROJECT_PACKAGE_JSON,
666
+ // Plugin root files
667
+ ...PROJECT_PLUGIN_ROOT_FILES
668
+ ];
669
+ }
670
+ constructor(cwd){
671
+ super(cwd, {
672
+ paths: PluginProject.paths
673
+ });
674
+ this.type = 'plugin';
675
+ }
676
+ }
677
+ const isPlugin = (cwd)=>{
678
+ const packageJSONPath = path$1.join(cwd, PROJECT_PACKAGE_JSON);
679
+ try {
680
+ fse.accessSync(packageJSONPath);
681
+ } catch {
682
+ throw new Error(`Could not find a ${PROJECT_PACKAGE_JSON} file in ${cwd}`);
683
+ }
684
+ const packageJSONBuffer = fse.readFileSync(packageJSONPath);
685
+ const packageJSON = JSON.parse(packageJSONBuffer.toString());
686
+ return packageJSON?.strapi?.kind === 'plugin';
687
+ };
688
+ // TODO: make this async so we can use async file methods
689
+ const projectFactory = (cwd)=>{
690
+ fse.accessSync(cwd);
691
+ return isPlugin(cwd) ? new PluginProject(cwd) : new AppProject(cwd);
692
+ };
693
+
694
+ const isPluginProject = (project)=>{
695
+ return project instanceof PluginProject;
696
+ };
697
+ function assertPluginProject(project) {
698
+ if (!isPluginProject(project)) {
699
+ throw new Error('Project is not a plugin');
700
+ }
701
+ }
702
+ const isApplicationProject = (project)=>{
703
+ return project instanceof AppProject;
704
+ };
705
+ function assertAppProject(project) {
706
+ if (!isApplicationProject(project)) {
707
+ throw new Error('Project is not an application');
708
+ }
709
+ }
710
+
711
+ class UnexpectedError extends Error {
712
+ constructor(){
713
+ super('Unexpected Error');
714
+ }
715
+ }
716
+ class NPMCandidateNotFoundError extends Error {
717
+ constructor(target, message = `Couldn't find a valid NPM candidate for "${target}"`){
718
+ super(message);
719
+ this.target = target;
720
+ }
721
+ }
722
+ class AbortedError extends Error {
723
+ constructor(message = 'Upgrade aborted'){
724
+ super(message);
725
+ }
726
+ }
727
+ const unknownToError = (e)=>{
728
+ if (e instanceof Error) {
729
+ return e;
730
+ }
731
+ if (typeof e === 'string') {
732
+ return new Error(e);
733
+ }
734
+ return new UnexpectedError();
735
+ };
736
+
737
+ const CODEMOD_CODE_SUFFIX = 'code';
738
+ const CODEMOD_JSON_SUFFIX = 'json';
739
+ const CODEMOD_ALLOWED_SUFFIXES = [
740
+ CODEMOD_CODE_SUFFIX,
741
+ CODEMOD_JSON_SUFFIX
742
+ ];
743
+ const CODEMOD_EXTENSION = 'ts';
744
+ const CODEMOD_FILE_REGEXP = new RegExp(`^.+[.](${CODEMOD_ALLOWED_SUFFIXES.join('|')})[.]${CODEMOD_EXTENSION}$`);
745
+
746
+ var constants$2 = /*#__PURE__*/Object.freeze({
747
+ __proto__: null,
748
+ CODEMOD_ALLOWED_SUFFIXES: CODEMOD_ALLOWED_SUFFIXES,
749
+ CODEMOD_CODE_SUFFIX: CODEMOD_CODE_SUFFIX,
750
+ CODEMOD_EXTENSION: CODEMOD_EXTENSION,
751
+ CODEMOD_FILE_REGEXP: CODEMOD_FILE_REGEXP,
752
+ CODEMOD_JSON_SUFFIX: CODEMOD_JSON_SUFFIX
753
+ });
754
+
755
+ class Codemod {
756
+ createUID() {
757
+ const name = this.format({
758
+ stripExtension: true,
759
+ stripKind: true,
760
+ stripHyphens: false
761
+ });
762
+ const kind = this.kind;
763
+ const version = this.version.raw;
764
+ return `${version}-${name}-${kind}`;
765
+ }
766
+ format(options) {
767
+ const { stripExtension = true, stripKind = true, stripHyphens = true } = options ?? {};
768
+ let formatted = this.filename;
769
+ if (stripExtension) {
770
+ formatted = formatted.replace(new RegExp(`\\.${CODEMOD_EXTENSION}$`, 'i'), '');
771
+ }
772
+ if (stripKind) {
773
+ formatted = formatted.replace(`.${CODEMOD_CODE_SUFFIX}`, '').replace(`.${CODEMOD_JSON_SUFFIX}`, '');
774
+ }
775
+ if (stripHyphens) {
776
+ formatted = formatted.replaceAll('-', ' ');
777
+ }
778
+ return formatted;
779
+ }
780
+ constructor(options){
781
+ this.kind = options.kind;
782
+ this.version = options.version;
783
+ this.baseDirectory = options.baseDirectory;
784
+ this.filename = options.filename;
785
+ this.path = path$1.join(this.baseDirectory, this.version.raw, this.filename);
786
+ this.uid = this.createUID();
787
+ }
788
+ }
789
+ const codemodFactory = (options)=>new Codemod(options);
790
+
791
+ const INTERNAL_CODEMODS_DIRECTORY = path$1.join(__dirname, '..', '..', 'resources', 'codemods' // upgrade/resources/codemods
792
+ );
793
+
794
+ var constants$1 = /*#__PURE__*/Object.freeze({
795
+ __proto__: null,
796
+ INTERNAL_CODEMODS_DIRECTORY: INTERNAL_CODEMODS_DIRECTORY
797
+ });
798
+
799
+ class CodemodRepository {
800
+ refresh() {
801
+ this.refreshAvailableVersions();
802
+ this.refreshAvailableFiles();
803
+ return this;
804
+ }
805
+ count(version) {
806
+ return this.findByVersion(version).length;
807
+ }
808
+ versionExists(version) {
809
+ return version.raw in this.groups;
810
+ }
811
+ has(uid) {
812
+ const result = this.find({
813
+ uids: [
814
+ uid
815
+ ]
816
+ });
817
+ if (result.length !== 1) {
818
+ return false;
819
+ }
820
+ const { codemods } = result[0];
821
+ return codemods.length === 1 && codemods[0].uid === uid;
822
+ }
823
+ find(q) {
824
+ const entries = Object.entries(this.groups);
825
+ return entries// Filter by range if provided in the query
826
+ .filter(maybeFilterByRange)// Transform version/codemods tuples into regular objects
827
+ .map(([version, codemods])=>({
828
+ version: semVerFactory(version),
829
+ // Filter by UID if provided in the query
830
+ codemods: codemods.filter(maybeFilterByUIDs)
831
+ }))// Only return groups with at least 1 codemod
832
+ .filter(({ codemods })=>codemods.length > 0);
833
+ function maybeFilterByRange([version]) {
834
+ if (!isRangeInstance(q.range)) {
835
+ return true;
836
+ }
837
+ return q.range.test(version);
838
+ }
839
+ function maybeFilterByUIDs(codemod) {
840
+ if (q.uids === undefined) {
841
+ return true;
842
+ }
843
+ return q.uids.includes(codemod.uid);
844
+ }
845
+ }
846
+ findByVersion(version) {
847
+ const literalVersion = version.raw;
848
+ const codemods = this.groups[literalVersion];
849
+ return codemods ?? [];
850
+ }
851
+ findAll() {
852
+ const entries = Object.entries(this.groups);
853
+ return entries.map(([version, codemods])=>({
854
+ version: semVerFactory(version),
855
+ codemods
856
+ }));
857
+ }
858
+ refreshAvailableVersions() {
859
+ this.versions = fse.readdirSync(this.cwd) // Only keep root directories
860
+ .filter((filename)=>fse.statSync(path$1.join(this.cwd, filename)).isDirectory())// Paths should be valid semver
861
+ .filter((filename)=>semver.valid(filename) !== null)// Transform files names to SemVer instances
862
+ .map((version)=>semVerFactory(version))// Sort versions in ascending order
863
+ .sort(semver.compare);
864
+ return this;
865
+ }
866
+ refreshAvailableFiles() {
867
+ this.groups = {};
868
+ for (const version of this.versions){
869
+ this.refreshAvailableFilesForVersion(version);
870
+ }
871
+ }
872
+ refreshAvailableFilesForVersion(version) {
873
+ const literalVersion = version.raw;
874
+ const versionDirectory = path$1.join(this.cwd, literalVersion);
875
+ // Ignore obsolete versions
876
+ if (!fse.existsSync(versionDirectory)) {
877
+ return;
878
+ }
879
+ this.groups[literalVersion] = fse.readdirSync(versionDirectory)// Make sure the filenames are valid codemod files
880
+ .filter((filename)=>fse.statSync(path$1.join(versionDirectory, filename)).isFile()).filter((filename)=>CODEMOD_FILE_REGEXP.test(filename))// Transform the filenames into Codemod instances
881
+ .map((filename)=>{
882
+ const kind = parseCodemodKindFromFilename(filename);
883
+ const baseDirectory = this.cwd;
884
+ return codemodFactory({
885
+ kind,
886
+ baseDirectory,
887
+ version,
888
+ filename
889
+ });
890
+ });
891
+ }
892
+ constructor(cwd){
893
+ assert(fse.existsSync(cwd), `Invalid codemods directory provided "${cwd}"`);
894
+ this.cwd = cwd;
895
+ this.groups = {};
896
+ this.versions = [];
897
+ }
898
+ }
899
+ const parseCodemodKindFromFilename = (filename)=>{
900
+ const kind = filename.split('.').at(-2);
901
+ assert(kind !== undefined);
902
+ assert(CODEMOD_ALLOWED_SUFFIXES.includes(kind));
903
+ return kind;
904
+ };
905
+ const codemodRepositoryFactory = (cwd = INTERNAL_CODEMODS_DIRECTORY)=>{
906
+ return new CodemodRepository(cwd);
907
+ };
908
+
909
+ class CodemodRunner {
910
+ setRange(range) {
911
+ this.range = range;
912
+ return this;
913
+ }
914
+ setLogger(logger) {
915
+ this.logger = logger;
916
+ return this;
917
+ }
918
+ onSelectCodemods(callback) {
919
+ this.selectCodemodsCallback = callback;
920
+ return this;
921
+ }
922
+ dry(enabled = true) {
923
+ this.isDry = enabled;
924
+ return this;
925
+ }
926
+ createRepository(codemodsDirectory) {
927
+ const repository = codemodRepositoryFactory(codemodsDirectory ?? INTERNAL_CODEMODS_DIRECTORY);
928
+ // Make sure we have access to the latest snapshots of codemods on the system
929
+ repository.refresh();
930
+ return repository;
931
+ }
932
+ async safeRunAndReport(codemods) {
933
+ if (this.isDry) {
934
+ this.logger?.warn?.('Running the codemods in dry mode. No files will be modified during the process.');
935
+ }
936
+ try {
937
+ const reports$1 = await this.project.runCodemods(codemods, {
938
+ dry: this.isDry
939
+ });
940
+ this.logger?.raw?.(reports(reports$1));
941
+ if (!this.isDry) {
942
+ const nbAffectedTotal = reports$1.flatMap((report)=>report.report.ok).reduce((acc, nb)=>acc + nb, 0);
943
+ this.logger?.debug?.(`Successfully ran ${highlight(codemods.length)} codemod(s), ${highlight(nbAffectedTotal)} change(s) have been detected`);
944
+ }
945
+ return successReport$1();
946
+ } catch (e) {
947
+ return erroredReport$1(unknownToError(e));
948
+ }
949
+ }
950
+ async runByUID(uid, codemodsDirectory) {
951
+ const repository = this.createRepository(codemodsDirectory);
952
+ if (!repository.has(uid)) {
953
+ throw new Error(`Unknown codemod UID provided: ${uid}`);
954
+ }
955
+ // Note: Ignore the range when running with a UID
956
+ const codemods = repository.find({
957
+ uids: [
958
+ uid
959
+ ]
960
+ }).flatMap(({ codemods })=>codemods);
961
+ return this.safeRunAndReport(codemods);
962
+ }
963
+ async run(codemodsDirectory) {
964
+ const repository = this.createRepository(codemodsDirectory);
965
+ // Find codemods matching the given range
966
+ const codemodsInRange = repository.find({
967
+ range: this.range
968
+ });
969
+ // If a selection callback is set, use it, else keep every codemods found
970
+ const selectedCodemods = this.selectCodemodsCallback ? await this.selectCodemodsCallback(codemodsInRange) : codemodsInRange;
971
+ // If no codemods have been selected (either manually or automatically)
972
+ // Then ignore and return a successful report
973
+ if (selectedCodemods.length === 0) {
974
+ this.logger?.debug?.(`Found no codemods to run for ${versionRange(this.range)}`);
975
+ return successReport$1();
976
+ }
977
+ // Flatten the collection to a single list of codemods, the original list should already be sorted by version
978
+ const codemods = selectedCodemods.flatMap(({ codemods })=>codemods);
979
+ // Log (debug) the codemods by version
980
+ const codemodsByVersion = fp.groupBy('version', codemods);
981
+ const fRange = versionRange(this.range);
982
+ this.logger?.debug?.(`Found ${highlight(codemods.length)} codemods for ${highlight(fp.size(codemodsByVersion))} version(s) using ${fRange}`);
983
+ for (const [version$1, codemods] of Object.entries(codemodsByVersion)){
984
+ this.logger?.debug?.(`- ${version(semVerFactory(version$1))} (${codemods.length})`);
985
+ }
986
+ return this.safeRunAndReport(codemods);
987
+ }
988
+ constructor(project, range){
989
+ this.project = project;
990
+ this.range = range;
991
+ this.isDry = false;
992
+ this.logger = null;
993
+ this.selectCodemodsCallback = null;
994
+ }
995
+ }
996
+ const codemodRunnerFactory = (project, range)=>{
997
+ return new CodemodRunner(project, range);
998
+ };
999
+ const successReport$1 = ()=>({
1000
+ success: true,
1001
+ error: null
1002
+ });
1003
+ const erroredReport$1 = (error)=>({
1004
+ success: false,
1005
+ error
1006
+ });
1007
+
1008
+ class Upgrader {
1009
+ getNPMPackage() {
1010
+ return this.npmPackage;
1011
+ }
1012
+ getProject() {
1013
+ return this.project;
1014
+ }
1015
+ getTarget() {
1016
+ return semVerFactory(this.target.raw);
1017
+ }
1018
+ setRequirements(requirements) {
1019
+ this.requirements = requirements;
1020
+ return this;
1021
+ }
1022
+ setTarget(target) {
1023
+ this.target = target;
1024
+ return this;
1025
+ }
1026
+ syncCodemodsTarget() {
1027
+ // Extract the <major>.<minor>.<patch> version from the target and assign it to the codemods target
1028
+ //
1029
+ // This is useful when dealing with alphas, betas or release candidates:
1030
+ // e.g. "5.0.0-beta.951" becomes "5.0.0"
1031
+ //
1032
+ // For experimental versions (e.g. "0.0.0-experimental.hex"), it is necessary to
1033
+ // override the codemods target manually in order to run the appropriate ones.
1034
+ this.codemodsTarget = semVerFactory(`${this.target.major}.${this.target.minor}.${this.target.patch}`);
1035
+ this.logger?.debug?.(`The codemods target has been synced with the upgrade target. The codemod runner will now look for ${version(this.codemodsTarget)}`);
1036
+ return this;
1037
+ }
1038
+ overrideCodemodsTarget(target) {
1039
+ this.codemodsTarget = target;
1040
+ this.logger?.debug?.(`Overriding the codemods target. The codemod runner will now look for ${version(target)}`);
1041
+ return this;
1042
+ }
1043
+ setLogger(logger) {
1044
+ this.logger = logger;
1045
+ return this;
1046
+ }
1047
+ onConfirm(callback) {
1048
+ this.confirmationCallback = callback;
1049
+ return this;
1050
+ }
1051
+ dry(enabled = true) {
1052
+ this.isDry = enabled;
1053
+ return this;
1054
+ }
1055
+ addRequirement(requirement) {
1056
+ this.requirements.push(requirement);
1057
+ const fRequired = requirement.isRequired ? '(required)' : '(optional)';
1058
+ this.logger?.debug?.(`Added a new requirement to the upgrade: ${highlight(requirement.name)} ${fRequired}`);
1059
+ return this;
1060
+ }
1061
+ async upgrade() {
1062
+ this.logger?.info?.(`Upgrading from ${version(this.project.strapiVersion)} to ${version(this.target)}`);
1063
+ if (this.isDry) {
1064
+ this.logger?.warn?.('Running the upgrade in dry mode. No files will be modified during the process.');
1065
+ }
1066
+ const range = rangeFromVersions(this.project.strapiVersion, this.target);
1067
+ const codemodsRange = rangeFromVersions(this.project.strapiVersion, this.codemodsTarget);
1068
+ const npmVersionsMatches = this.npmPackage?.findVersionsInRange(range) ?? [];
1069
+ this.logger?.debug?.(`Found ${highlight(npmVersionsMatches.length)} versions satisfying ${versionRange(range)}`);
1070
+ try {
1071
+ this.logger?.info?.(upgradeStep('Checking requirement', [
1072
+ 1,
1073
+ 4
1074
+ ]));
1075
+ await this.checkRequirements(this.requirements, {
1076
+ npmVersionsMatches,
1077
+ project: this.project,
1078
+ target: this.target
1079
+ });
1080
+ this.logger?.info?.(upgradeStep('Applying the latest code modifications', [
1081
+ 2,
1082
+ 4
1083
+ ]));
1084
+ await this.runCodemods(codemodsRange);
1085
+ // We need to refresh the project files to make sure we have
1086
+ // the latest version of each file (including package.json) for the next steps
1087
+ this.logger?.debug?.('Refreshing project information...');
1088
+ this.project.refresh();
1089
+ this.logger?.info?.(upgradeStep('Upgrading Strapi dependencies', [
1090
+ 3,
1091
+ 4
1092
+ ]));
1093
+ await this.updateDependencies();
1094
+ this.logger?.info?.(upgradeStep('Installing dependencies', [
1095
+ 4,
1096
+ 4
1097
+ ]));
1098
+ await this.installDependencies();
1099
+ } catch (e) {
1100
+ return erroredReport(unknownToError(e));
1101
+ }
1102
+ return successReport();
1103
+ }
1104
+ async confirm(message) {
1105
+ if (typeof this.confirmationCallback !== 'function') {
1106
+ return true;
1107
+ }
1108
+ return this.confirmationCallback(message);
1109
+ }
1110
+ async checkRequirements(requirements, context) {
1111
+ for (const requirement of requirements){
1112
+ const { pass, error } = await requirement.test(context);
1113
+ if (pass) {
1114
+ await this.onSuccessfulRequirement(requirement, context);
1115
+ } else {
1116
+ await this.onFailedRequirement(requirement, error);
1117
+ }
1118
+ }
1119
+ }
1120
+ async onSuccessfulRequirement(requirement, context) {
1121
+ const hasChildren = requirement.children.length > 0;
1122
+ if (hasChildren) {
1123
+ await this.checkRequirements(requirement.children, context);
1124
+ }
1125
+ }
1126
+ async onFailedRequirement(requirement, originalError) {
1127
+ const errorMessage = `Requirement failed: ${originalError.message} (${highlight(requirement.name)})`;
1128
+ const warningMessage = originalError.message;
1129
+ const confirmationMessage = `Ignore optional requirement "${highlight(requirement.name)}" ?`;
1130
+ const error = new Error(errorMessage);
1131
+ if (requirement.isRequired) {
1132
+ throw error;
1133
+ }
1134
+ this.logger?.warn?.(warningMessage);
1135
+ const response = await this.confirmationCallback?.(confirmationMessage);
1136
+ if (!response) {
1137
+ throw error;
1138
+ }
1139
+ }
1140
+ async updateDependencies() {
1141
+ const { packageJSON, packageJSONPath } = this.project;
1142
+ const json = createJSONTransformAPI(packageJSON);
1143
+ const dependencies = json.get('dependencies', {});
1144
+ const strapiDependencies = this.getScopedStrapiDependencies(dependencies);
1145
+ this.logger?.debug?.(`Found ${highlight(strapiDependencies.length)} dependency(ies) to update`);
1146
+ strapiDependencies.forEach((dependency)=>this.logger?.debug?.(`- ${dependency[0]} (${dependency[1]} -> ${this.target})`));
1147
+ if (strapiDependencies.length === 0) {
1148
+ return;
1149
+ }
1150
+ strapiDependencies.forEach(([name])=>json.set(`dependencies.${name}`, this.target.raw));
1151
+ const updatedPackageJSON = json.root();
1152
+ if (this.isDry) {
1153
+ this.logger?.debug?.(`Skipping dependencies update (${chalk.italic('dry mode')})`);
1154
+ return;
1155
+ }
1156
+ await saveJSON(packageJSONPath, updatedPackageJSON);
1157
+ }
1158
+ getScopedStrapiDependencies(dependencies) {
1159
+ const { strapiVersion } = this.project;
1160
+ const strapiDependencies = [];
1161
+ // Find all @strapi/* packages matching the current Strapi version
1162
+ for (const [name, version] of Object.entries(dependencies)){
1163
+ const isScopedStrapiPackage = name.startsWith(SCOPED_STRAPI_PACKAGE_PREFIX);
1164
+ const isOnCurrentStrapiVersion = isValidSemVer(version) && version === strapiVersion.raw;
1165
+ if (isScopedStrapiPackage && isOnCurrentStrapiVersion) {
1166
+ strapiDependencies.push([
1167
+ name,
1168
+ semVerFactory(version)
1169
+ ]);
1170
+ }
1171
+ }
1172
+ return strapiDependencies;
1173
+ }
1174
+ async installDependencies() {
1175
+ const projectPath = this.project.cwd;
1176
+ const packageManagerName = await utils.packageManager.getPreferred(projectPath);
1177
+ this.logger?.debug?.(`Using ${highlight(packageManagerName)} as package manager`);
1178
+ if (this.isDry) {
1179
+ this.logger?.debug?.(`Skipping dependencies installation (${chalk.italic('dry mode')})`);
1180
+ return;
1181
+ }
1182
+ await utils.packageManager.installDependencies(projectPath, packageManagerName, {
1183
+ stdout: this.logger?.stdout,
1184
+ stderr: this.logger?.stderr
1185
+ });
1186
+ }
1187
+ async runCodemods(range) {
1188
+ const codemodRunner = codemodRunnerFactory(this.project, range);
1189
+ codemodRunner.dry(this.isDry);
1190
+ if (this.logger) {
1191
+ codemodRunner.setLogger(this.logger);
1192
+ }
1193
+ await codemodRunner.run();
1194
+ }
1195
+ constructor(project, target, npmPackage){
1196
+ this.project = project;
1197
+ this.npmPackage = npmPackage;
1198
+ this.target = target;
1199
+ this.syncCodemodsTarget();
1200
+ this.isDry = false;
1201
+ this.requirements = [];
1202
+ this.logger = null;
1203
+ this.confirmationCallback = null;
1204
+ }
1205
+ }
1206
+ /**
1207
+ * Resolves the NPM target version based on the given project, target, and NPM package.
1208
+ * If target is a SemVer, it directly finds it. If it's a release type (major, minor, patch),
1209
+ * it calculates the range of versions for this release type and returns the latest version within this range.
1210
+ */ const resolveNPMTarget = (project, target, npmPackage)=>{
1211
+ // Semver
1212
+ if (isSemverInstance(target)) {
1213
+ const version = npmPackage.findVersion(target);
1214
+ if (!version) {
1215
+ throw new NPMCandidateNotFoundError(target);
1216
+ }
1217
+ return version;
1218
+ }
1219
+ // Release Types
1220
+ if (isSemVerReleaseType(target)) {
1221
+ const range = rangeFromVersions(project.strapiVersion, target);
1222
+ const npmVersionsMatches = npmPackage.findVersionsInRange(range);
1223
+ // The targeted version is the latest one that matches the given range
1224
+ const version = npmVersionsMatches.at(-1);
1225
+ if (!version) {
1226
+ throw new NPMCandidateNotFoundError(range, `The project is already up-to-date (${target})`);
1227
+ }
1228
+ return version;
1229
+ }
1230
+ throw new NPMCandidateNotFoundError(target);
1231
+ };
1232
+ const upgraderFactory = (project, target, npmPackage)=>{
1233
+ const npmTarget = resolveNPMTarget(project, target, npmPackage);
1234
+ const semverTarget = semVerFactory(npmTarget.version);
1235
+ if (semver.eq(semverTarget, project.strapiVersion)) {
1236
+ throw new Error(`The project is already using v${semverTarget}`);
1237
+ }
1238
+ return new Upgrader(project, semverTarget, npmPackage);
1239
+ };
1240
+ const successReport = ()=>({
1241
+ success: true,
1242
+ error: null
1243
+ });
1244
+ const erroredReport = (error)=>({
1245
+ success: false,
1246
+ error
1247
+ });
1248
+
1249
+ const STRAPI_PACKAGE_NAME = '@strapi/strapi';
1250
+
1251
+ var constants = /*#__PURE__*/Object.freeze({
1252
+ __proto__: null,
1253
+ STRAPI_PACKAGE_NAME: STRAPI_PACKAGE_NAME
1254
+ });
1255
+
1256
+ class Requirement {
1257
+ setChildren(children) {
1258
+ this.children = children;
1259
+ return this;
1260
+ }
1261
+ addChild(child) {
1262
+ this.children.push(child);
1263
+ return this;
1264
+ }
1265
+ asOptional() {
1266
+ const newInstance = requirementFactory(this.name, this.testCallback, false);
1267
+ newInstance.setChildren(this.children);
1268
+ return newInstance;
1269
+ }
1270
+ asRequired() {
1271
+ const newInstance = requirementFactory(this.name, this.testCallback, true);
1272
+ newInstance.setChildren(this.children);
1273
+ return newInstance;
1274
+ }
1275
+ async test(context) {
1276
+ try {
1277
+ await this.testCallback?.(context);
1278
+ return ok();
1279
+ } catch (e) {
1280
+ if (e instanceof Error) {
1281
+ return errored(e);
1282
+ }
1283
+ if (typeof e === 'string') {
1284
+ return errored(new Error(e));
1285
+ }
1286
+ return errored(new Error('Unknown error'));
1287
+ }
1288
+ }
1289
+ constructor(name, testCallback, isRequired){
1290
+ this.name = name;
1291
+ this.testCallback = testCallback;
1292
+ this.isRequired = isRequired ?? true;
1293
+ this.children = [];
1294
+ }
1295
+ }
1296
+ const ok = ()=>({
1297
+ pass: true,
1298
+ error: null
1299
+ });
1300
+ const errored = (error)=>({
1301
+ pass: false,
1302
+ error
1303
+ });
1304
+ const requirementFactory = (name, testCallback, isRequired)=>new Requirement(name, testCallback, isRequired);
1305
+
1306
+ const REQUIRE_AVAILABLE_NEXT_MAJOR = requirementFactory('REQUIRE_AVAILABLE_NEXT_MAJOR', (context)=>{
1307
+ const { project, target } = context;
1308
+ const currentMajor = project.strapiVersion.major;
1309
+ const targetedMajor = target.major;
1310
+ if (targetedMajor === currentMajor) {
1311
+ throw new Error(`You're already on the latest major version (v${currentMajor})`);
1312
+ }
1313
+ });
1314
+ const REQUIRE_LATEST_FOR_CURRENT_MAJOR = requirementFactory('REQUIRE_LATEST_FOR_CURRENT_MAJOR', (context)=>{
1315
+ const { project, target, npmVersionsMatches } = context;
1316
+ const { major: currentMajor } = project.strapiVersion;
1317
+ const invalidMatches = npmVersionsMatches.filter((match)=>semVerFactory(match.version).major === currentMajor);
1318
+ if (invalidMatches.length > 0) {
1319
+ const invalidVersions = invalidMatches.map((match)=>match.version);
1320
+ const invalidVersionsCount = invalidVersions.length;
1321
+ throw new Error(`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.`);
1322
+ }
1323
+ });
1324
+
1325
+ const REQUIRE_GIT_CLEAN_REPOSITORY = requirementFactory('REQUIRE_GIT_CLEAN_REPOSITORY', async (context)=>{
1326
+ const git = simpleGit({
1327
+ baseDir: context.project.cwd
1328
+ });
1329
+ const status = await git.status();
1330
+ if (!status.isClean()) {
1331
+ throw new Error('Repository is not clean. Please commit or stash any changes before upgrading');
1332
+ }
1333
+ });
1334
+ const REQUIRE_GIT_REPOSITORY = requirementFactory('REQUIRE_GIT_REPOSITORY', async (context)=>{
1335
+ const git = simpleGit({
1336
+ baseDir: context.project.cwd
1337
+ });
1338
+ const isRepo = await git.checkIsRepo();
1339
+ if (!isRepo) {
1340
+ throw new Error('Not a git repository (or any of the parent directories)');
1341
+ }
1342
+ }).addChild(REQUIRE_GIT_CLEAN_REPOSITORY.asOptional());
1343
+ const REQUIRE_GIT_INSTALLED = requirementFactory('REQUIRE_GIT_INSTALLED', async (context)=>{
1344
+ const git = simpleGit({
1345
+ baseDir: context.project.cwd
1346
+ });
1347
+ try {
1348
+ await git.version();
1349
+ } catch {
1350
+ throw new Error('Git is not installed');
1351
+ }
1352
+ }).addChild(REQUIRE_GIT_REPOSITORY.asOptional());
1353
+ const REQUIRE_GIT = requirementFactory('REQUIRE_GIT', null).addChild(REQUIRE_GIT_INSTALLED.asOptional());
1354
+
1355
+ /**
1356
+ * Handles the upgrade prompts when using the latest tag.
1357
+ *
1358
+ * - checks if an upgrade involves a major bump, warning and asking for user confirmation before proceeding
1359
+ */ const latest = async (upgrader, options)=>{
1360
+ // Exit if the upgrade target isn't the latest tag
1361
+ if (options.target !== exports.ReleaseType.Latest) {
1362
+ return;
1363
+ }
1364
+ // Retrieve utilities from the upgrader instance
1365
+ const npmPackage = upgrader.getNPMPackage();
1366
+ const target = upgrader.getTarget();
1367
+ const project = upgrader.getProject();
1368
+ const { strapiVersion: current } = project;
1369
+ // Pre-formatted strings used in logs
1370
+ const fTargetMajor = highlight(`v${target.major}`);
1371
+ const fCurrentMajor = highlight(`v${current.major}`);
1372
+ const fTarget = version(target);
1373
+ const fCurrent = version(current);
1374
+ // Flags
1375
+ const isMajorUpgrade = target.major > current.major;
1376
+ // Handle potential major upgrade, warns, and asks for confirmation to proceed
1377
+ if (isMajorUpgrade) {
1378
+ options.logger.warn(`Detected a major upgrade for the "${highlight(exports.ReleaseType.Latest)}" tag: ${fCurrent} > ${fTarget}`);
1379
+ // Find the latest release in between the current one and the next major
1380
+ const newerPackageRelease = npmPackage.findVersionsInRange(rangeFactory(`>${current.raw} <${target.major}`)).at(-1);
1381
+ // If the project isn't on the latest version for the current major, emit a warning
1382
+ if (newerPackageRelease) {
1383
+ const fLatest = version(semVerFactory(newerPackageRelease.version));
1384
+ options.logger.warn(`It's recommended to first upgrade to the latest version of ${fCurrentMajor} (${fLatest}) before upgrading to ${fTargetMajor}.`);
1385
+ }
1386
+ const proceedAnyway = await upgrader.confirm(`I know what I'm doing. Proceed anyway!`);
1387
+ if (!proceedAnyway) {
1388
+ throw new AbortedError();
1389
+ }
1390
+ }
1391
+ };
1392
+
1393
+ const upgrade = async (options)=>{
1394
+ const timer = timerFactory();
1395
+ const { logger, codemodsTarget } = options;
1396
+ // Resolves the correct working directory based on the given input
1397
+ const cwd = path$1.resolve(options.cwd ?? process.cwd());
1398
+ const project = projectFactory(cwd);
1399
+ logger.debug(projectDetails(project));
1400
+ if (!isApplicationProject(project)) {
1401
+ throw new Error(`The "${options.target}" upgrade can only be run on a Strapi project; for plugins, please use "codemods".`);
1402
+ }
1403
+ logger.debug(`Application: VERSION=${version(project.packageJSON.version)}; STRAPI_VERSION=${version(project.strapiVersion)}`);
1404
+ const npmPackage = npmPackageFactory(STRAPI_PACKAGE_NAME);
1405
+ // Load all available versions from the NPM registry
1406
+ await npmPackage.refresh();
1407
+ // Initialize the upgrade instance
1408
+ // Throws during initialization if the provided target is incompatible with the current version
1409
+ const upgrader = upgraderFactory(project, options.target, npmPackage).dry(options.dry ?? false).onConfirm(options.confirm ?? null).setLogger(logger);
1410
+ // Manually override the target version for codemods if it's explicitly provided
1411
+ if (codemodsTarget !== undefined) {
1412
+ upgrader.overrideCodemodsTarget(codemodsTarget);
1413
+ }
1414
+ // Prompt user for confirmation details before upgrading
1415
+ await runUpgradePrompts(upgrader, options);
1416
+ // Add specific requirements before upgrading
1417
+ addUpgradeRequirements(upgrader, options);
1418
+ // Actually run the upgrade process once configured,
1419
+ // The response contains information about the final status: success/error
1420
+ const upgradeReport = await upgrader.upgrade();
1421
+ if (!upgradeReport.success) {
1422
+ throw upgradeReport.error;
1423
+ }
1424
+ timer.stop();
1425
+ logger.info(`Completed in ${durationMs(timer.elapsedMs)}ms`);
1426
+ };
1427
+ const runUpgradePrompts = async (upgrader, options)=>{
1428
+ if (options.target === exports.ReleaseType.Latest) {
1429
+ await latest(upgrader, options);
1430
+ }
1431
+ };
1432
+ const addUpgradeRequirements = (upgrader, options)=>{
1433
+ // Don't add the same requirements when manually targeting a major upgrade
1434
+ // using a semver as it's implied that the users know what they're doing
1435
+ if (options.target === exports.ReleaseType.Major) {
1436
+ upgrader.addRequirement(REQUIRE_AVAILABLE_NEXT_MAJOR).addRequirement(REQUIRE_LATEST_FOR_CURRENT_MAJOR);
1437
+ }
1438
+ // Make sure the git repository is in an optimal state before running the upgrade
1439
+ // Mainly used to ease rollbacks in case the upgrade is corrupted
1440
+ upgrader.addRequirement(REQUIRE_GIT.asOptional());
1441
+ };
1442
+
1443
+ const resolvePath = (cwd)=>path$1.resolve(cwd ?? process.cwd());
1444
+ const getRangeFromTarget = (currentVersion, target)=>{
1445
+ if (isSemverInstance(target)) {
1446
+ return rangeFactory(target);
1447
+ }
1448
+ const { major, minor, patch } = currentVersion;
1449
+ switch(target){
1450
+ case exports.ReleaseType.Latest:
1451
+ throw new Error("Can't use <latest> to create a codemods range: not implemented");
1452
+ case exports.ReleaseType.Major:
1453
+ return rangeFactory(`${major}`);
1454
+ case exports.ReleaseType.Minor:
1455
+ return rangeFactory(`${major}.${minor}`);
1456
+ case exports.ReleaseType.Patch:
1457
+ return rangeFactory(`${major}.${minor}.${patch}`);
1458
+ default:
1459
+ throw new Error(`Invalid target set: ${target}`);
1460
+ }
1461
+ };
1462
+ const findRangeFromTarget = (project, target)=>{
1463
+ // If a range is manually defined, use it
1464
+ if (isRangeInstance(target)) {
1465
+ return target;
1466
+ }
1467
+ // If the current project is a Strapi application
1468
+ // Get the range from the given target
1469
+ if (isApplicationProject(project)) {
1470
+ return getRangeFromTarget(project.strapiVersion, target);
1471
+ }
1472
+ // Else, if the project is a Strapi plugin or anything else
1473
+ // Set the range to match any version
1474
+ return rangeFactory('*');
1475
+ };
1476
+
1477
+ const runCodemods = async (options)=>{
1478
+ const timer = timerFactory();
1479
+ const { logger, uid } = options;
1480
+ // Make sure we're resolving the correct working directory based on the given input
1481
+ const cwd = resolvePath(options.cwd);
1482
+ const project = projectFactory(cwd);
1483
+ const range = findRangeFromTarget(project, options.target);
1484
+ logger.debug(projectDetails(project));
1485
+ logger.debug(`Range: set to ${versionRange(range)}`);
1486
+ const codemodRunner = codemodRunnerFactory(project, range).dry(options.dry ?? false).onSelectCodemods(options.selectCodemods ?? null).setLogger(logger);
1487
+ let report;
1488
+ // If uid is defined, only run the selected codemod
1489
+ if (uid !== undefined) {
1490
+ logger.debug(`Running a single codemod: ${codemodUID(uid)}`);
1491
+ report = await codemodRunner.runByUID(uid);
1492
+ } else {
1493
+ report = await codemodRunner.run();
1494
+ }
1495
+ if (!report.success) {
1496
+ throw report.error;
1497
+ }
1498
+ timer.stop();
1499
+ logger.info(`Completed in ${timer.elapsedMs}`);
1500
+ };
1501
+
1502
+ const listCodemods = async (options)=>{
1503
+ const { logger, target } = options;
1504
+ const cwd = resolvePath(options.cwd);
1505
+ const project = projectFactory(cwd);
1506
+ const range = findRangeFromTarget(project, target);
1507
+ logger.debug(projectDetails(project));
1508
+ logger.debug(`Range: set to ${versionRange(range)}`);
1509
+ // Create a codemod repository targeting the default location of the codemods
1510
+ const repo = codemodRepositoryFactory();
1511
+ // Make sure all the codemods are loaded
1512
+ repo.refresh();
1513
+ // Find groups of codemods matching the given range
1514
+ const groups = repo.find({
1515
+ range
1516
+ });
1517
+ // Flatten the groups into a simple codemod array
1518
+ const codemods = groups.flatMap((collection)=>collection.codemods);
1519
+ // Debug
1520
+ logger.debug(`Found ${highlight(codemods.length)} codemods`);
1521
+ // Don't log an empty table
1522
+ if (codemods.length === 0) {
1523
+ logger.info(`Found no codemods matching ${versionRange(range)}`);
1524
+ return;
1525
+ }
1526
+ // Format the list to a pretty table
1527
+ const fCodemods = codemodList(codemods);
1528
+ logger.raw(fCodemods);
1529
+ };
1530
+
1531
+ class Logger {
1532
+ get isNotSilent() {
1533
+ return !this.isSilent;
1534
+ }
1535
+ get errors() {
1536
+ return this.nbErrorsCalls;
1537
+ }
1538
+ get warnings() {
1539
+ return this.nbWarningsCalls;
1540
+ }
1541
+ get stdout() {
1542
+ return this.isSilent ? undefined : process.stdout;
1543
+ }
1544
+ get stderr() {
1545
+ return this.isSilent ? undefined : process.stderr;
1546
+ }
1547
+ setDebug(debug) {
1548
+ this.isDebug = debug;
1549
+ return this;
1550
+ }
1551
+ setSilent(silent) {
1552
+ this.isSilent = silent;
1553
+ return this;
1554
+ }
1555
+ debug(...args) {
1556
+ const isDebugEnabled = this.isNotSilent && this.isDebug;
1557
+ if (isDebugEnabled) {
1558
+ console.log(chalk.cyan(`[DEBUG]\t[${nowAsISO()}]`), ...args);
1559
+ }
1560
+ return this;
1561
+ }
1562
+ error(...args) {
1563
+ this.nbErrorsCalls += 1;
1564
+ if (this.isNotSilent) {
1565
+ console.error(chalk.red(`[ERROR]\t[${nowAsISO()}]`), ...args);
1566
+ }
1567
+ return this;
1568
+ }
1569
+ info(...args) {
1570
+ if (this.isNotSilent) {
1571
+ console.info(chalk.blue(`[INFO]\t[${new Date().toISOString()}]`), ...args);
1572
+ }
1573
+ return this;
1574
+ }
1575
+ raw(...args) {
1576
+ if (this.isNotSilent) {
1577
+ console.log(...args);
1578
+ }
1579
+ return this;
1580
+ }
1581
+ warn(...args) {
1582
+ this.nbWarningsCalls += 1;
1583
+ if (this.isNotSilent) {
1584
+ console.warn(chalk.yellow(`[WARN]\t[${new Date().toISOString()}]`), ...args);
1585
+ }
1586
+ return this;
1587
+ }
1588
+ constructor(options = {}){
1589
+ // Set verbosity options
1590
+ this.isDebug = options.debug ?? false;
1591
+ this.isSilent = options.silent ?? false;
1592
+ // Initialize counters
1593
+ this.nbErrorsCalls = 0;
1594
+ this.nbWarningsCalls = 0;
1595
+ }
1596
+ }
1597
+ const nowAsISO = ()=>new Date().toISOString();
1598
+ const loggerFactory = (options = {})=>new Logger(options);
1599
+
1600
+ exports.AbortedError = AbortedError;
1601
+ exports.NPMCandidateNotFoundError = NPMCandidateNotFoundError;
1602
+ exports.UnexpectedError = UnexpectedError;
1603
+ exports.assertAppProject = assertAppProject;
1604
+ exports.assertPluginProject = assertPluginProject;
1605
+ exports.codeRunnerFactory = codeRunnerFactory;
1606
+ exports.codemodFactory = codemodFactory;
1607
+ exports.codemodList = codemodList;
1608
+ exports.codemodRepositoryFactory = codemodRepositoryFactory;
1609
+ exports.codemodUID = codemodUID;
1610
+ exports.constants = constants$4;
1611
+ exports.constants$1 = constants$3;
1612
+ exports.constants$2 = constants$2;
1613
+ exports.constants$3 = constants$1;
1614
+ exports.constants$4 = constants;
1615
+ exports.durationMs = durationMs;
1616
+ exports.fileScannerFactory = fileScannerFactory;
1617
+ exports.highlight = highlight;
1618
+ exports.isApplicationProject = isApplicationProject;
1619
+ exports.isLiteralSemVer = isLiteralSemVer;
1620
+ exports.isPluginProject = isPluginProject;
1621
+ exports.isRangeInstance = isRangeInstance;
1622
+ exports.isSemVerReleaseType = isSemVerReleaseType;
1623
+ exports.isSemverInstance = isSemverInstance;
1624
+ exports.isValidSemVer = isValidSemVer;
1625
+ exports.isValidStringifiedRange = isValidStringifiedRange;
1626
+ exports.jsonRunnerFactory = jsonRunnerFactory;
1627
+ exports.listCodemods = listCodemods;
1628
+ exports.loggerFactory = loggerFactory;
1629
+ exports.path = path;
1630
+ exports.projectDetails = projectDetails;
1631
+ exports.projectFactory = projectFactory;
1632
+ exports.projectType = projectType;
1633
+ exports.rangeFactory = rangeFactory;
1634
+ exports.rangeFromReleaseType = rangeFromReleaseType;
1635
+ exports.rangeFromVersions = rangeFromVersions;
1636
+ exports.reports = reports;
1637
+ exports.requirementFactory = requirementFactory;
1638
+ exports.runCodemods = runCodemods;
1639
+ exports.semVerFactory = semVerFactory;
1640
+ exports.timerFactory = timerFactory;
1641
+ exports.transform = transform;
1642
+ exports.types = types;
1643
+ exports.unknownToError = unknownToError;
1644
+ exports.upgrade = upgrade;
1645
+ exports.upgradeStep = upgradeStep;
1646
+ exports.upgraderFactory = upgraderFactory;
1647
+ exports.version = version;
1648
+ exports.versionRange = versionRange;
1649
+ //# sourceMappingURL=logger-DGi224NW.js.map