Merge pull request #19053 from strapi/upgrade-tool/chores

This commit is contained in:
Jean-Sébastien Herbaux 2023-12-15 15:22:57 +01:00 committed by GitHub
commit 9fbf22643f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 101 additions and 41 deletions

View File

@ -49,8 +49,10 @@
}, },
"devDependencies": { "devDependencies": {
"@strapi/pack-up": "4.15.5", "@strapi/pack-up": "4.15.5",
"@strapi/plugin-upload": "4.15.5",
"@types/jest": "29.5.2", "@types/jest": "29.5.2",
"eslint-config-custom": "4.15.5", "eslint-config-custom": "4.15.5",
"memfs": "4.6.0",
"tsconfig": "4.15.5" "tsconfig": "4.15.5"
}, },
"engines": { "engines": {

View File

@ -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 type { File } from '@strapi/plugin-upload';
import localProvider from '../index'; 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', () => { describe('Local provider', () => {
beforeAll(() => { beforeAll(() => {
globalThis.strapi = {}; globalThis.strapi = {};
globalThis.strapi.dirs = { static: { public: '' } }; globalThis.strapi.dirs = { static: { public: '' } };
fse.ensureDirSync('uploads');
}); });
afterAll(() => { afterAll(() => {

View File

@ -8,12 +8,28 @@ import type { Command } from '../types';
export const upgrade: Command = async (options) => { export const upgrade: Command = async (options) => {
try { try {
const logger = loggerFactory({ silent: options.silent, debug: options.debug }); const { silent, debug, yes } = options;
const logger = loggerFactory({ silent, debug });
logger.warn( logger.warn(
"Please make sure you've created a backup of your codebase and files before upgrading" "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({ await tasks.upgrade({
logger, logger,
confirm, confirm,
@ -25,14 +41,3 @@ export const upgrade: Command = async (options) => {
handleError(err); 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;
};

View File

@ -15,6 +15,11 @@ const addReleaseUpgradeCommand = (releaseType: Version.ReleaseType, description:
.option('-n, --dry', 'Simulate the upgrade without updating any files', false) .option('-n, --dry', 'Simulate the upgrade without updating any files', false)
.option('-d, --debug', 'Get more logs in debug mode', false) .option('-d, --debug', 'Get more logs in debug mode', false)
.option('-s, --silent', "Don't log anything", 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) => { .action(async (options: CLIOptions) => {
const { upgrade } = await import('./commands/upgrade.js'); const { upgrade } = await import('./commands/upgrade.js');

View File

@ -5,6 +5,7 @@ export interface CLIOptions {
dry: boolean; dry: boolean;
debug: boolean; debug: boolean;
silent: boolean; silent: boolean;
yes: boolean;
projectPath?: string; projectPath?: string;
} }

View File

@ -5,19 +5,22 @@ import { constants as timerConstants } from '../timer';
import type { Version } from '../version'; import type { Version } from '../version';
import type { Report } from '../report'; import type { Report } from '../report';
import { isSemVer } from '../version';
export const path = (path: string) => chalk.blue(path); export const path = (path: string) => chalk.blue(path);
export const version = (version: Version.LiteralVersion | Version.SemVer) => { 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 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[]) => { export const reports = (reports: Report.CodemodReport[]) => {
const rows = reports.map(({ codemod, report }, i) => { const rows = reports.map(({ codemod, report }, i) => {

View File

@ -1,4 +1,5 @@
import assert from 'node:assert'; import assert from 'node:assert';
import chalk from 'chalk';
import { packageManager } from '@strapi/utils'; import { packageManager } from '@strapi/utils';
import { import {
@ -21,9 +22,9 @@ import type { Project } from '../project';
type DependenciesEntries = Array<[name: string, version: Version.LiteralSemVer]>; type DependenciesEntries = Array<[name: string, version: Version.LiteralSemVer]>;
export class Upgrader implements UpgraderInterface { 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; private target: Version.SemVer;
@ -79,19 +80,37 @@ export class Upgrader implements UpgraderInterface {
} }
async upgrade(): Promise<UpgradeReport> { async upgrade(): Promise<UpgradeReport> {
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 range = rangeFromVersions(this.project.strapiVersion, this.target);
const npmVersionsMatches = this.npmPackage?.findVersionsInRange(range) ?? []; const npmVersionsMatches = this.npmPackage?.findVersionsInRange(range) ?? [];
this.logger?.debug(
`Found ${f.highlight(npmVersionsMatches.length)} versions satisfying ${f.versionRange(range)}`
);
try { try {
this.logger?.info(f.upgradeStep('Checking requirement', [1, 4]));
await this.checkRequirements(this.requirements, { await this.checkRequirements(this.requirements, {
npmVersionsMatches, npmVersionsMatches,
project: this.project, project: this.project,
target: this.target, target: this.target,
}); });
this.logger?.info(f.upgradeStep('Upgrading Strapi dependencies', [2, 4]));
await this.updateDependencies(); await this.updateDependencies();
this.logger?.info(f.upgradeStep('Installing dependencies', [3, 4]));
await this.installDependencies(); await this.installDependencies();
this.logger?.info(f.upgradeStep('Applying the latest code modifications', [4, 4]));
await this.runCodemods(range); await this.runCodemods(range);
} catch (e) { } catch (e) {
return erroredReport(unknownToError(e)); return erroredReport(unknownToError(e));
@ -130,8 +149,11 @@ export class Upgrader implements UpgraderInterface {
requirement: Requirement.Requirement, requirement: Requirement.Requirement,
originalError: Error originalError: Error
): Promise<void> { ): Promise<void> {
const errorMessage = `Upgrade requirement "${requirement.name}" failed: ${originalError.message}`; const errorMessage = `Requirement failed: ${originalError.message} (${f.highlight(
const confirmationMessage = `Optional requirement "${requirement.name}" failed with "${originalError.message}", do you want to proceed anyway?`; requirement.name
)})`;
const warningMessage = originalError.message;
const confirmationMessage = `Ignore optional requirement "${f.highlight(requirement.name)}" ?`;
const error = new Error(errorMessage); const error = new Error(errorMessage);
@ -139,13 +161,13 @@ export class Upgrader implements UpgraderInterface {
throw error; throw error;
} }
this.logger?.warn(warningMessage);
const response = await this.confirmationCallback?.(confirmationMessage); const response = await this.confirmationCallback?.(confirmationMessage);
if (!response) { if (!response) {
throw error; throw error;
} }
this.logger?.warn(errorMessage);
} }
private async updateDependencies(): Promise<void> { private async updateDependencies(): Promise<void> {
@ -156,6 +178,11 @@ export class Upgrader implements UpgraderInterface {
const dependencies = json.get<Record<string, string>>('dependencies', {}); const dependencies = json.get<Record<string, string>>('dependencies', {});
const strapiDependencies = this.getScopedStrapiDependencies(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) { if (strapiDependencies.length === 0) {
return; return;
} }
@ -164,9 +191,12 @@ export class Upgrader implements UpgraderInterface {
const updatedPackageJSON = json.root(); const updatedPackageJSON = json.root();
if (!this.isDry) { if (this.isDry) {
await saveJSON(packageJSONPath, updatedPackageJSON); this.logger?.debug(`Skipping dependencies update (${chalk.italic('dry mode')}`);
return;
} }
await saveJSON(packageJSONPath, updatedPackageJSON);
} }
private getScopedStrapiDependencies(dependencies: Record<string, string>): DependenciesEntries { private getScopedStrapiDependencies(dependencies: Record<string, string>): DependenciesEntries {
@ -192,6 +222,13 @@ export class Upgrader implements UpgraderInterface {
const packageManagerName = await packageManager.getPreferred(projectPath); 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, { await packageManager.installDependencies(projectPath, packageManagerName, {
stdout: this.logger?.stdout, stdout: this.logger?.stdout,
stderr: this.logger?.stderr, stderr: this.logger?.stderr,
@ -210,10 +247,15 @@ export class Upgrader implements UpgraderInterface {
const hasCodemodsToRun = versionedCodemods.length > 0; const hasCodemodsToRun = versionedCodemods.length > 0;
if (!hasCodemodsToRun) { 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; 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 // Flatten the collection to a single list of codemods, the original list should already be sorted
const codemods = versionedCodemods.map(({ codemods }) => codemods).flat(); 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 // The targeted version is the latest one that matches the given range
const targetedNPMVersion = npmVersionsMatches.at(-1); 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) // 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) { if (isSemVer(target) && target.raw !== targetedNPMVersion.version) {
throw new Error( 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
}`
); );
} }

View File

@ -28,8 +28,8 @@ export const upgrade = async (options: UpgradeOptions) => {
if (options.target === Version.ReleaseType.Major) { if (options.target === Version.ReleaseType.Major) {
upgrader upgrader
.addRequirement(requirements.major.REQUIRE_AVAILABLE_NEXT_MAJOR.asOptional()) .addRequirement(requirements.major.REQUIRE_AVAILABLE_NEXT_MAJOR)
.addRequirement(requirements.major.REQUIRE_LATEST_FOR_CURRENT_MAJOR.asOptional()); .addRequirement(requirements.major.REQUIRE_LATEST_FOR_CURRENT_MAJOR);
} }
upgrader.addRequirement(requirements.common.REQUIRE_GIT.asOptional()); upgrader.addRequirement(requirements.common.REQUIRE_GIT.asOptional());

View File

@ -9790,10 +9790,12 @@ __metadata:
resolution: "@strapi/provider-upload-local@workspace:packages/providers/upload-local" resolution: "@strapi/provider-upload-local@workspace:packages/providers/upload-local"
dependencies: dependencies:
"@strapi/pack-up": "npm:4.15.5" "@strapi/pack-up": "npm:4.15.5"
"@strapi/plugin-upload": "npm:4.15.5"
"@strapi/utils": "npm:4.15.5" "@strapi/utils": "npm:4.15.5"
"@types/jest": "npm:29.5.2" "@types/jest": "npm:29.5.2"
eslint-config-custom: "npm:4.15.5" eslint-config-custom: "npm:4.15.5"
fs-extra: "npm:10.1.0" fs-extra: "npm:10.1.0"
memfs: "npm:4.6.0"
tsconfig: "npm:4.15.5" tsconfig: "npm:4.15.5"
languageName: unknown languageName: unknown
linkType: soft linkType: soft