diff --git a/packages/providers/upload-local/package.json b/packages/providers/upload-local/package.json index 3049904c8d..4d95d155a0 100644 --- a/packages/providers/upload-local/package.json +++ b/packages/providers/upload-local/package.json @@ -49,8 +49,10 @@ }, "devDependencies": { "@strapi/pack-up": "4.15.5", + "@strapi/plugin-upload": "4.15.5", "@types/jest": "29.5.2", "eslint-config-custom": "4.15.5", + "memfs": "4.6.0", "tsconfig": "4.15.5" }, "engines": { diff --git a/packages/providers/upload-local/src/__tests__/upload-local.test.ts b/packages/providers/upload-local/src/__tests__/upload-local.test.ts index 6c56d3d3fd..1e11a3faf8 100644 --- a/packages/providers/upload-local/src/__tests__/upload-local.test.ts +++ b/packages/providers/upload-local/src/__tests__/upload-local.test.ts @@ -1,22 +1,20 @@ +/* eslint-disable import/first */ +import { fs } from 'memfs'; + +jest.mock('fs', () => fs); + +import fse from 'fs-extra'; + import type { File } from '@strapi/plugin-upload'; + import localProvider from '../index'; -jest.mock('fs', () => { - return { - writeFile: jest.fn((_path, _buffer, callback) => callback()), - }; -}); - -jest.mock('fs-extra', () => { - return { - pathExistsSync: jest.fn(() => true), - }; -}); - describe('Local provider', () => { beforeAll(() => { globalThis.strapi = {}; globalThis.strapi.dirs = { static: { public: '' } }; + + fse.ensureDirSync('uploads'); }); afterAll(() => { diff --git a/packages/utils/upgrade/src/cli/commands/upgrade.ts b/packages/utils/upgrade/src/cli/commands/upgrade.ts index c1b269faff..1c99a627e1 100644 --- a/packages/utils/upgrade/src/cli/commands/upgrade.ts +++ b/packages/utils/upgrade/src/cli/commands/upgrade.ts @@ -8,12 +8,28 @@ import type { Command } from '../types'; export const upgrade: Command = async (options) => { try { - const logger = loggerFactory({ silent: options.silent, debug: options.debug }); + const { silent, debug, yes } = options; + const logger = loggerFactory({ silent, debug }); logger.warn( "Please make sure you've created a backup of your codebase and files before upgrading" ); + const confirm = async (message: string) => { + if (yes) { + return true; + } + + const { confirm } = await prompts({ + name: 'confirm', + type: 'confirm', + message, + }); + + // If confirm is undefined (Ctrl + C), default to false + return confirm ?? false; + }; + await tasks.upgrade({ logger, confirm, @@ -25,14 +41,3 @@ export const upgrade: Command = async (options) => { handleError(err); } }; - -const confirm = async (message: string) => { - const { confirm } = await prompts({ - name: 'confirm', - type: 'confirm', - message, - }); - - // If confirm is undefined (Ctrl + C), default to false - return confirm ?? false; -}; diff --git a/packages/utils/upgrade/src/cli/index.ts b/packages/utils/upgrade/src/cli/index.ts index e77c39a484..c79149160a 100644 --- a/packages/utils/upgrade/src/cli/index.ts +++ b/packages/utils/upgrade/src/cli/index.ts @@ -15,6 +15,11 @@ const addReleaseUpgradeCommand = (releaseType: Version.ReleaseType, description: .option('-n, --dry', 'Simulate the upgrade without updating any files', false) .option('-d, --debug', 'Get more logs in debug mode', false) .option('-s, --silent', "Don't log anything", false) + .option( + '-y, --yes', + 'Automatically answer "yes" to any prompts that the CLI might print on the command line.', + false + ) .action(async (options: CLIOptions) => { const { upgrade } = await import('./commands/upgrade.js'); diff --git a/packages/utils/upgrade/src/cli/types.ts b/packages/utils/upgrade/src/cli/types.ts index 02621228c6..654dcb40c7 100644 --- a/packages/utils/upgrade/src/cli/types.ts +++ b/packages/utils/upgrade/src/cli/types.ts @@ -5,6 +5,7 @@ export interface CLIOptions { dry: boolean; debug: boolean; silent: boolean; + yes: boolean; projectPath?: string; } diff --git a/packages/utils/upgrade/src/modules/format/formats.ts b/packages/utils/upgrade/src/modules/format/formats.ts index 2dc872ef34..4816a857f7 100644 --- a/packages/utils/upgrade/src/modules/format/formats.ts +++ b/packages/utils/upgrade/src/modules/format/formats.ts @@ -5,19 +5,22 @@ import { constants as timerConstants } from '../timer'; import type { Version } from '../version'; import type { Report } from '../report'; -import { isSemVer } from '../version'; export const path = (path: string) => chalk.blue(path); export const version = (version: Version.LiteralVersion | Version.SemVer) => { - return chalk.italic.yellow(isSemVer(version) ? version.raw : version); + return chalk.italic.yellow(`v${version}`); }; -export const versionRange = (range: string) => chalk.bold.green(range); +export const versionRange = (range: Version.Range) => chalk.italic.yellow(range); export const transform = (transformFilePath: string) => chalk.cyan(transformFilePath); -export const highlight = (text: string) => chalk.bold.underline(text); +export const highlight = (arg: unknown) => chalk.bold.underline(arg); + +export const upgradeStep = (text: string, step: [current: number, total: number]) => { + return chalk.bold(`(${step[0]}/${step[1]}) ${text}...`); +}; export const reports = (reports: Report.CodemodReport[]) => { const rows = reports.map(({ codemod, report }, i) => { diff --git a/packages/utils/upgrade/src/modules/upgrader/upgrader.ts b/packages/utils/upgrade/src/modules/upgrader/upgrader.ts index c224eed37b..e340152eee 100644 --- a/packages/utils/upgrade/src/modules/upgrader/upgrader.ts +++ b/packages/utils/upgrade/src/modules/upgrader/upgrader.ts @@ -1,4 +1,5 @@ import assert from 'node:assert'; +import chalk from 'chalk'; import { packageManager } from '@strapi/utils'; import { @@ -21,9 +22,9 @@ import type { Project } from '../project'; type DependenciesEntries = Array<[name: string, version: Version.LiteralSemVer]>; export class Upgrader implements UpgraderInterface { - private project: Project; + private readonly project: Project; - private npmPackage: NPM.Package; + private readonly npmPackage: NPM.Package; private target: Version.SemVer; @@ -79,19 +80,37 @@ export class Upgrader implements UpgraderInterface { } async upgrade(): Promise { + if (this.isDry) { + this.logger?.warn( + 'Running the upgrade in dry mode. No files will be modified during the process.' + ); + } + this.logger?.debug( + `Upgrading from ${f.version(this.project.strapiVersion)} to ${f.version(this.target)}` + ); + const range = rangeFromVersions(this.project.strapiVersion, this.target); const npmVersionsMatches = this.npmPackage?.findVersionsInRange(range) ?? []; + this.logger?.debug( + `Found ${f.highlight(npmVersionsMatches.length)} versions satisfying ${f.versionRange(range)}` + ); + try { + this.logger?.info(f.upgradeStep('Checking requirement', [1, 4])); await this.checkRequirements(this.requirements, { npmVersionsMatches, project: this.project, target: this.target, }); + this.logger?.info(f.upgradeStep('Upgrading Strapi dependencies', [2, 4])); await this.updateDependencies(); + + this.logger?.info(f.upgradeStep('Installing dependencies', [3, 4])); await this.installDependencies(); + this.logger?.info(f.upgradeStep('Applying the latest code modifications', [4, 4])); await this.runCodemods(range); } catch (e) { return erroredReport(unknownToError(e)); @@ -130,8 +149,11 @@ export class Upgrader implements UpgraderInterface { requirement: Requirement.Requirement, originalError: Error ): Promise { - const errorMessage = `Upgrade requirement "${requirement.name}" failed: ${originalError.message}`; - const confirmationMessage = `Optional requirement "${requirement.name}" failed with "${originalError.message}", do you want to proceed anyway?`; + const errorMessage = `Requirement failed: ${originalError.message} (${f.highlight( + requirement.name + )})`; + const warningMessage = originalError.message; + const confirmationMessage = `Ignore optional requirement "${f.highlight(requirement.name)}" ?`; const error = new Error(errorMessage); @@ -139,13 +161,13 @@ export class Upgrader implements UpgraderInterface { throw error; } + this.logger?.warn(warningMessage); + const response = await this.confirmationCallback?.(confirmationMessage); if (!response) { throw error; } - - this.logger?.warn(errorMessage); } private async updateDependencies(): Promise { @@ -156,6 +178,11 @@ export class Upgrader implements UpgraderInterface { const dependencies = json.get>('dependencies', {}); const strapiDependencies = this.getScopedStrapiDependencies(dependencies); + this.logger?.debug(`Found ${f.highlight(strapiDependencies.length)} dependency(ies) to update`); + strapiDependencies.forEach((dependency) => + this.logger?.debug(`- ${dependency[0]} (${dependency[1]} -> ${this.target})`) + ); + if (strapiDependencies.length === 0) { return; } @@ -164,9 +191,12 @@ export class Upgrader implements UpgraderInterface { const updatedPackageJSON = json.root(); - if (!this.isDry) { - await saveJSON(packageJSONPath, updatedPackageJSON); + if (this.isDry) { + this.logger?.debug(`Skipping dependencies update (${chalk.italic('dry mode')}`); + return; } + + await saveJSON(packageJSONPath, updatedPackageJSON); } private getScopedStrapiDependencies(dependencies: Record): DependenciesEntries { @@ -192,6 +222,13 @@ export class Upgrader implements UpgraderInterface { const packageManagerName = await packageManager.getPreferred(projectPath); + this.logger?.debug(`Using ${f.highlight(packageManagerName)} as package manager`); + + if (this.isDry) { + this.logger?.debug(`Skipping dependencies installation (${chalk.italic('dry mode')}`); + return; + } + await packageManager.installDependencies(projectPath, packageManagerName, { stdout: this.logger?.stdout, stderr: this.logger?.stderr, @@ -210,10 +247,15 @@ export class Upgrader implements UpgraderInterface { const hasCodemodsToRun = versionedCodemods.length > 0; if (!hasCodemodsToRun) { - this.logger?.debug(`Found no codemods to run for ${this.target}`); + this.logger?.debug(`Found no codemods to run for ${f.version(this.target)}`); return; } + this.logger?.debug(`Found codemods for ${f.highlight(versionedCodemods.length)} version(s)`); + versionedCodemods.forEach(({ version, codemods }) => + this.logger?.debug(`- ${f.version(version)} (${codemods.length})`) + ); + // Flatten the collection to a single list of codemods, the original list should already be sorted const codemods = versionedCodemods.map(({ codemods }) => codemods).flat(); @@ -234,12 +276,14 @@ export const upgraderFactory = ( // The targeted version is the latest one that matches the given range const targetedNPMVersion = npmVersionsMatches.at(-1); - assert(targetedNPMVersion, `No available version found for ${range}`); + assert(targetedNPMVersion, `Could not find any version in the range ${f.versionRange(range)}`); // Make sure the latest version matched in the range is the same as the targeted one (only if target is a semver) if (isSemVer(target) && target.raw !== targetedNPMVersion.version) { throw new Error( - `${target} doesn't exist on the registry. Closest one found is ${targetedNPMVersion.version}` + `${f.version(target)} doesn't exist on the registry. Closest version found is ${ + targetedNPMVersion.version + }` ); } diff --git a/packages/utils/upgrade/src/tasks/upgrade/upgrade.ts b/packages/utils/upgrade/src/tasks/upgrade/upgrade.ts index 723a436734..1b825e16b8 100644 --- a/packages/utils/upgrade/src/tasks/upgrade/upgrade.ts +++ b/packages/utils/upgrade/src/tasks/upgrade/upgrade.ts @@ -28,8 +28,8 @@ export const upgrade = async (options: UpgradeOptions) => { if (options.target === Version.ReleaseType.Major) { upgrader - .addRequirement(requirements.major.REQUIRE_AVAILABLE_NEXT_MAJOR.asOptional()) - .addRequirement(requirements.major.REQUIRE_LATEST_FOR_CURRENT_MAJOR.asOptional()); + .addRequirement(requirements.major.REQUIRE_AVAILABLE_NEXT_MAJOR) + .addRequirement(requirements.major.REQUIRE_LATEST_FOR_CURRENT_MAJOR); } upgrader.addRequirement(requirements.common.REQUIRE_GIT.asOptional()); diff --git a/yarn.lock b/yarn.lock index f55131bed2..564e558c5d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9790,10 +9790,12 @@ __metadata: resolution: "@strapi/provider-upload-local@workspace:packages/providers/upload-local" dependencies: "@strapi/pack-up": "npm:4.15.5" + "@strapi/plugin-upload": "npm:4.15.5" "@strapi/utils": "npm:4.15.5" "@types/jest": "npm:29.5.2" eslint-config-custom: "npm:4.15.5" fs-extra: "npm:10.1.0" + memfs: "npm:4.6.0" tsconfig: "npm:4.15.5" languageName: unknown linkType: soft