/
githubmirror
/
strapi
Обзор
Документация
Войти
/
githubmirror
/
strapi
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
develop
packages/utils/upgrade/src/modules/upgrader/upgrader.ts
450 строк
13 KB
Ben Irvin
fix(strapi): stabilize admin redux deps during upgrade (#26249)
15 июн 2026, 19:32
Не верифицирован
15 июн 2026, 19:32
964633a
Код
Авторство
О чём код?
import path from 'node:path'; import { rm } from 'node:fs/promises'; import chalk from 'chalk'; import semver from 'semver'; import { packageManager } from '@strapi/utils'; import { createJSONTransformAPI, saveJSON } from '../json'; import { constants as projectConstants } from '../project'; import { isSemverInstance, isSemVerReleaseType, isValidSemVer, rangeFromVersions, semVerFactory, } from '../version'; import { NPMCandidateNotFoundError, unknownToError } from '../error'; import * as f from '../format'; import { codemodRunnerFactory } from '../codemod-runner'; import type { Upgrader as UpgraderInterface, UpgradeReport } from './types'; import type { Version } from '../version'; import type { Logger } from '../logger'; import type { Requirement } from '../requirement'; import type { NPM } from '../npm'; import type { AppProject } from '../project'; import type { ConfirmationCallback } from '../common/types'; type DependenciesEntries = Array<[name: string, version: Version.SemVer]>; export class Upgrader implements UpgraderInterface { private readonly project: AppProject; private readonly npmPackage: NPM.Package; private target: Version.SemVer; private codemodsTarget!: Version.SemVer; private isDry: boolean; private logger: Logger | null; private requirements: Requirement.Requirement[]; private confirmationCallback: ConfirmationCallback | null; constructor(project: AppProject, target: Version.SemVer, npmPackage: NPM.Package) { this.project = project; this.npmPackage = npmPackage; this.target = target; this.syncCodemodsTarget(); this.isDry = false; this.requirements = []; this.logger = null; this.confirmationCallback = null; } getNPMPackage(): NPM.Package { return this.npmPackage; } getProject(): AppProject { return this.project; } getTarget(): Version.SemVer { return semVerFactory(this.target.raw); } setRequirements(requirements: Requirement.Requirement[]) { this.requirements = requirements; return this; } setTarget(target: Version.SemVer) { this.target = target; return this; } syncCodemodsTarget() { // Extract the <major>.<minor>.<patch> version from the target and assign it to the codemods target // // This is useful when dealing with alphas, betas or release candidates: // e.g. "5.0.0-beta.951" becomes "5.0.0" // // For experimental versions (e.g. "0.0.0-experimental.hex"), it is necessary to // override the codemods target manually in order to run the appropriate ones. this.codemodsTarget = semVerFactory( `${this.target.major}.${this.target.minor}.${this.target.patch}` ); this.logger?.debug?.( `The codemods target has been synced with the upgrade target. The codemod runner will now look for ${f.version( this.codemodsTarget )}` ); return this; } overrideCodemodsTarget(target: Version.SemVer) { this.codemodsTarget = target; this.logger?.debug?.( `Overriding the codemods target. The codemod runner will now look for ${f.version(target)}` ); return this; } setLogger(logger: Logger) { this.logger = logger; return this; } onConfirm(callback: ConfirmationCallback | null) { this.confirmationCallback = callback; return this; } dry(enabled: boolean = true) { this.isDry = enabled; return this; } addRequirement(requirement: Requirement.Requirement) { this.requirements.push(requirement); const fRequired = requirement.isRequired ? '(required)' : '(optional)'; this.logger?.debug?.( `Added a new requirement to the upgrade: ${f.highlight(requirement.name)} ${fRequired}` ); return this; } async upgrade(): Promise<UpgradeReport> { this.logger?.info?.( `Upgrading from ${f.version(this.project.strapiVersion)} to ${f.version(this.target)}` ); if (this.isDry) { this.logger?.warn?.( 'Running the upgrade in dry mode. No files will be modified during the process.' ); } const range = rangeFromVersions(this.project.strapiVersion, this.target); const codemodsRange = rangeFromVersions(this.project.strapiVersion, this.codemodsTarget); 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('Applying the latest code modifications', [2, 4])); await this.runCodemods(codemodsRange); // We need to refresh the project files to make sure we have // the latest version of each file (including package.json) for the next steps this.logger?.debug?.('Refreshing project information...'); this.project.refresh(); this.logger?.info?.(f.upgradeStep('Upgrading Strapi dependencies', [3, 4])); await this.updateDependencies(); this.logger?.info?.(f.upgradeStep('Installing dependencies', [4, 4])); await this.installDependencies(); } catch (e) { return erroredReport(unknownToError(e)); } return successReport(); } async confirm(message: string): Promise<boolean> { if (typeof this.confirmationCallback !== 'function') { return true; } return this.confirmationCallback(message); } private async checkRequirements( requirements: Requirement.Requirement[], context: Requirement.TestContext ) { for (const requirement of requirements) { const { pass, error } = await requirement.test(context); if (pass) { await this.onSuccessfulRequirement(requirement, context); } else { await this.onFailedRequirement(requirement, error); } } } private async onSuccessfulRequirement( requirement: Requirement.Requirement, context: Requirement.TestContext ): Promise<void> { const hasChildren = requirement.children.length > 0; if (hasChildren) { await this.checkRequirements(requirement.children, context); } } private async onFailedRequirement( requirement: Requirement.Requirement, originalError: Error ): Promise<void> { 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); if (requirement.isRequired) { throw error; } this.logger?.warn?.(warningMessage); const response = await this.confirmationCallback?.(confirmationMessage); if (!response) { throw error; } } private async updateDependencies(): Promise<void> { const { packageJSON, packageJSONPath } = this.project; const json = createJSONTransformAPI(packageJSON); const dependencies = json.get<Record<string, string>>('dependencies', {}); const devDependencies = json.get<Record<string, string>>('devDependencies', {}); const strapiProductionDependencies = this.getScopedStrapiDependencies(dependencies); const strapiDevelopmentDependencies = this.getScopedStrapiDependencies(devDependencies); const strapiPackagesToUpgradeCount = strapiProductionDependencies.length + strapiDevelopmentDependencies.length; this.logger?.debug?.( `Found ${f.highlight(strapiPackagesToUpgradeCount)} dependency(ies) to update` ); strapiProductionDependencies.forEach((dependency) => this.logger?.debug?.(`- ${dependency[0]} (${dependency[1]} -> ${this.target})`) ); strapiDevelopmentDependencies.forEach((dependency) => this.logger?.debug?.( `- ${dependency[0]} (devDependencies) (${dependency[1]} -> ${this.target})` ) ); if (strapiPackagesToUpgradeCount === 0) { return; } strapiProductionDependencies.forEach(([name]) => json.set(`dependencies.${name}`, this.target.raw) ); strapiDevelopmentDependencies.forEach(([name]) => json.set(`devDependencies.${name}`, this.target.raw) ); const updatedPackageJSON = json.root(); if (this.isDry) { this.logger?.debug?.(`Skipping dependencies update (${chalk.italic('dry mode')})`); return; } await saveJSON(packageJSONPath, updatedPackageJSON); } private getScopedStrapiDependencies(dependencies: Record<string, string>): DependenciesEntries { const { strapiVersion } = this.project; const strapiDependencies: DependenciesEntries = []; // Find all @strapi/* packages matching the current Strapi version for (const [name, version] of Object.entries(dependencies)) { const isScopedStrapiPackage = name.startsWith(projectConstants.SCOPED_STRAPI_PACKAGE_PREFIX); const isOnCurrentStrapiVersion = isValidSemVer(version) && version === strapiVersion.raw; if (isScopedStrapiPackage && isOnCurrentStrapiVersion) { strapiDependencies.push([name, semVerFactory(version)]); } } return strapiDependencies; } private async installDependencies(): Promise<void> { const projectPath = this.project.cwd; 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, }); await this.clearStrapiAdminViteCacheAfterUpgrade(); } /** * Removes only `node_modules/.strapi/vite` (Vite `cacheDir` for the admin panel). Generated * cache — not user source. Optionally gated by `confirm` when the CLI passes a callback so * users know the next `strapi develop` may spend longer once re-optimizing dependencies. */ private async clearStrapiAdminViteCacheAfterUpgrade(): Promise<void> { if (this.isDry) { return; } const shouldClear = typeof this.confirmationCallback === 'function' && (await this.confirm( [ 'Remove the Strapi admin dev cache', chalk.dim('(node_modules/.strapi/vite)'), '?', 'Recommended after dependency changes to avoid stale bundles;', 'the next', chalk.bold('strapi develop'), 'may take longer once while Vite re-optimizes.', ].join(' ') )); if (!shouldClear) { this.logger?.info?.( 'Skipped clearing admin dev cache. If the admin panel misbehaves after upgrading, delete node_modules/.strapi/vite and run develop again.' ); return; } const cachePath = path.join(this.project.cwd, 'node_modules', '.strapi', 'vite'); try { await rm(cachePath, { recursive: true, force: true }); this.logger?.debug?.(`Removed Strapi admin Vite cache at ${cachePath}`); } catch (error) { const message = error instanceof Error ? error.message : String(error); this.logger?.warn?.(`Could not remove Strapi admin Vite cache at ${cachePath}: ${message}`); } } private async runCodemods(range: Version.Range): Promise<void> { const codemodRunner = codemodRunnerFactory(this.project, range); codemodRunner.dry(this.isDry); if (this.logger) { codemodRunner.setLogger(this.logger); } await codemodRunner.run(); } } /** * Resolves the NPM target version based on the given project, target, and NPM package. * If target is a SemVer, it directly finds it. If it's a release type (major, minor, patch), * it calculates the range of versions for this release type and returns the latest version within this range. */ const resolveNPMTarget = ( project: AppProject, target: Version.ReleaseType | Version.SemVer, npmPackage: NPM.Package ): NPM.NPMPackageVersion => { // Semver if (isSemverInstance(target)) { const version = npmPackage.findVersion(target); if (!version) { throw new NPMCandidateNotFoundError(target); } return version; } // Release Types if (isSemVerReleaseType(target)) { const range = rangeFromVersions(project.strapiVersion, target); const npmVersionsMatches = npmPackage.findVersionsInRange(range); // The targeted version is the latest one that matches the given range const version = npmVersionsMatches.at(-1); if (!version) { throw new NPMCandidateNotFoundError(range, `The project is already up-to-date (${target})`); } return version; } throw new NPMCandidateNotFoundError(target); }; export const upgraderFactory = ( project: AppProject, target: Version.ReleaseType | Version.SemVer, npmPackage: NPM.Package ) => { const npmTarget = resolveNPMTarget(project, target, npmPackage); const semverTarget = semVerFactory(npmTarget.version); if (semver.eq(semverTarget, project.strapiVersion)) { throw new Error(`The project is already using v${semverTarget}`); } if (semver.lt(semverTarget, project.strapiVersion)) { throw new Error( `The target version v${semverTarget} must be greater than the current version v${project.strapiVersion}` ); } return new Upgrader(project, semverTarget, npmPackage); }; const successReport = (): UpgradeReport => ({ success: true, error: null }); const erroredReport = (error: Error): UpgradeReport => ({ success: false, error });