diff --git a/packages/utils/upgrade/src/modules/format/formats.ts b/packages/utils/upgrade/src/modules/format/formats.ts index eb4ddbf155..7f00d2a1d5 100644 --- a/packages/utils/upgrade/src/modules/format/formats.ts +++ b/packages/utils/upgrade/src/modules/format/formats.ts @@ -3,7 +3,7 @@ import chalk from 'chalk'; import { constants as timerConstants } from '../timer'; -import type { ProjectType } from '../project'; +import type { AppProject, PluginProject, ProjectType } from '../project'; import type { Codemod } from '../codemod'; import type { Version } from '../version'; import type { Report } from '../report'; @@ -18,6 +18,10 @@ export const codemodUID = (uid: string) => { return chalk.bold.cyan(uid); }; +export const projectDetails = (project: AppProject | PluginProject) => { + return `Project: TYPE=${projectType(project.type)}; CWD=${path(project.cwd)}; PATHS=${project.paths.map(path)}`; +}; + export const projectType = (type: ProjectType) => chalk.cyan(type); export const versionRange = (range: Version.Range) => chalk.italic.yellow(range.raw); diff --git a/packages/utils/upgrade/src/modules/project/__tests__/project.test.ts b/packages/utils/upgrade/src/modules/project/__tests__/project.test.ts index d1de365cb5..9f540eeade 100644 --- a/packages/utils/upgrade/src/modules/project/__tests__/project.test.ts +++ b/packages/utils/upgrade/src/modules/project/__tests__/project.test.ts @@ -8,7 +8,12 @@ jest.mock('fs', () => fs); const srcFilename = (cwd: string, filename: string) => path.join(cwd, 'src', filename); const srcFilenames = (cwd: string) => { - return Object.keys(srcFiles).map((filename) => srcFilename(cwd, filename)); + return Object.keys(defaultFiles).map((filename) => srcFilename(cwd, filename)); +}; + +const pluginServerFilename = (cwd: string, filename: string) => path.join(cwd, 'server', filename); +const pluginServerFilenames = (cwd: string) => { + return Object.keys(defaultFiles).map((filename) => pluginServerFilename(cwd, filename)); }; const currentStrapiVersion = '1.2.3'; @@ -29,7 +34,7 @@ const pluginPackageJSONFile = `{ } }`; -const srcFiles = { +const defaultFiles = { 'a.ts': 'console.log("a.ts")', 'b.ts': 'console.log("b.ts")', 'c.js': 'console.log("c.js")', @@ -40,12 +45,12 @@ const srcFiles = { const appVolume = { 'package.json': appPackageJSONFile, - src: srcFiles, + src: defaultFiles, }; const pluginVolume = { 'package.json': pluginPackageJSONFile, - src: srcFiles, + server: defaultFiles, }; describe('Project', () => { @@ -70,7 +75,7 @@ describe('Project', () => { }); test('Fails on project without package.json file', async () => { - vol.fromNestedJSON({ src: srcFiles }, defaultCWD); + vol.fromNestedJSON({ src: defaultFiles }, defaultCWD); expect(() => projectFactory(defaultCWD)).toThrow( `Could not find a package.json file in ${defaultCWD}` @@ -79,7 +84,7 @@ describe('Project', () => { test('Fails when not a plugin and no @strapi/strapi dependency found', async () => { vol.fromNestedJSON( - { 'package.json': `{ "name": "test", "version": "1.2.3" }`, src: srcFiles }, + { 'package.json': `{ "name": "test", "version": "1.2.3" }`, src: defaultFiles }, defaultCWD ); @@ -92,7 +97,7 @@ describe('Project', () => { vol.fromNestedJSON( { 'package.json': `{ "name": "test", "version": "1.2.3", "dependencies": { "@strapi/strapi": "^4.0.0" } }`, - src: srcFiles, + src: defaultFiles, }, defaultCWD ); @@ -133,7 +138,10 @@ describe('Project', () => { expect(project.files.length).toBe(7); expect(project.files).toStrictEqual( - expect.arrayContaining([path.join(defaultCWD, 'package.json'), ...srcFilenames(defaultCWD)]) + expect.arrayContaining([ + path.join(defaultCWD, 'package.json'), + ...pluginServerFilenames(defaultCWD), + ]) ); expect(project.cwd).toBe(defaultCWD); @@ -172,7 +180,10 @@ describe('Project', () => { expect(project.files.length).toBe(7); expect(project.files).toStrictEqual( - expect.arrayContaining([path.join(defaultCWD, 'package.json'), ...srcFilenames(defaultCWD)]) + expect.arrayContaining([ + path.join(defaultCWD, 'package.json'), + ...pluginServerFilenames(defaultCWD), + ]) ); expect(project.cwd).toBe(defaultCWD); diff --git a/packages/utils/upgrade/src/modules/project/constants.ts b/packages/utils/upgrade/src/modules/project/constants.ts index 8f792804c8..1fd424f26c 100644 --- a/packages/utils/upgrade/src/modules/project/constants.ts +++ b/packages/utils/upgrade/src/modules/project/constants.ts @@ -1,8 +1,12 @@ export const PROJECT_PACKAGE_JSON = 'package.json'; -export const PROJECT_DEFAULT_ALLOWED_ROOT_PATHS = ['src', 'config', 'public', 'admin', 'server']; +export const PROJECT_APP_ALLOWED_ROOT_PATHS = ['src', 'config', 'public']; -export const PROJECT_DEFAULT_CODE_EXTENSIONS = [ +export const PROJECT_PLUGIN_ALLOWED_ROOT_PATHS = ['admin', 'server']; + +export const PROJECT_PLUGIN_ROOT_FILES = ['strapi-admin.js', 'strapi-server.js']; + +export const PROJECT_CODE_EXTENSIONS = [ // Source files 'js', 'mjs', @@ -12,14 +16,9 @@ export const PROJECT_DEFAULT_CODE_EXTENSIONS = [ 'tsx', ]; -export const PROJECT_DEFAULT_JSON_EXTENSIONS = ['json']; +export const PROJECT_JSON_EXTENSIONS = ['json']; -export const PROJECT_DEFAULT_ALLOWED_EXTENSIONS = [ - ...PROJECT_DEFAULT_CODE_EXTENSIONS, - ...PROJECT_DEFAULT_JSON_EXTENSIONS, -]; - -export const PROJECT_DEFAULT_PATTERNS = ['package.json']; +export const PROJECT_ALLOWED_EXTENSIONS = [...PROJECT_CODE_EXTENSIONS, ...PROJECT_JSON_EXTENSIONS]; export const SCOPED_STRAPI_PACKAGE_PREFIX = '@strapi/'; diff --git a/packages/utils/upgrade/src/modules/project/project.ts b/packages/utils/upgrade/src/modules/project/project.ts index 3c966ce80f..8d0f182de5 100644 --- a/packages/utils/upgrade/src/modules/project/project.ts +++ b/packages/utils/upgrade/src/modules/project/project.ts @@ -12,7 +12,13 @@ import * as constants from './constants'; import type { Version } from '../version'; import type { Codemod } from '../codemod'; import type { Report } from '../report'; -import type { FileExtension, MinimalPackageJSON, ProjectType, RunCodemodsOptions } from './types'; +import type { + FileExtension, + MinimalPackageJSON, + ProjectConfig, + ProjectType, + RunCodemodsOptions, +} from './types'; export class Project { public cwd: string; @@ -25,12 +31,15 @@ export class Project { public packageJSON!: MinimalPackageJSON; - constructor(cwd: string) { + public readonly paths: string[]; + + constructor(cwd: string, config: ProjectConfig) { if (!fse.pathExistsSync(cwd)) { throw new Error(`ENOENT: no such file or directory, access '${cwd}'`); } this.cwd = cwd; + this.paths = config.paths; this.refresh(); } @@ -67,12 +76,8 @@ export class Project { } private createProjectCodemodsRunners(dry: boolean = false) { - const jsonExtensions = constants.PROJECT_DEFAULT_JSON_EXTENSIONS.map( - (ext) => `.${ext}` - ); - const codeExtensions = constants.PROJECT_DEFAULT_CODE_EXTENSIONS.map( - (ext) => `.${ext}` - ); + const jsonExtensions = constants.PROJECT_JSON_EXTENSIONS.map((ext) => `.${ext}`); + const codeExtensions = constants.PROJECT_CODE_EXTENSIONS.map((ext) => `.${ext}`); const jsonFiles = this.getFilesByExtensions(jsonExtensions); const codeFiles = this.getFilesByExtensions(codeExtensions); @@ -82,7 +87,7 @@ export class Project { parser: 'ts', runInBand: true, babel: true, - extensions: constants.PROJECT_DEFAULT_CODE_EXTENSIONS.join(','), + extensions: constants.PROJECT_CODE_EXTENSIONS.join(','), // Don't output any log coming from the runner print: false, silent: true, @@ -109,20 +114,9 @@ export class Project { } private refreshProjectFiles(): void { - const allowedRootPaths = formatGlobCollectionPattern( - constants.PROJECT_DEFAULT_ALLOWED_ROOT_PATHS - ); - - const allowedExtensions = formatGlobCollectionPattern( - constants.PROJECT_DEFAULT_ALLOWED_EXTENSIONS - ); - - const projectFilesPattern = `./${allowedRootPaths}/**/*.${allowedExtensions}`; - - const patterns = [projectFilesPattern, ...constants.PROJECT_DEFAULT_PATTERNS]; const scanner = fileScannerFactory(this.cwd); - this.files = scanner.scan(patterns); + this.files = scanner.scan(this.paths); } } @@ -131,8 +125,25 @@ export class AppProject extends Project { readonly type = 'application' as const satisfies ProjectType; + /** + * Returns an array of allowed file paths for a Strapi application + * + * The resulting paths include app default files and the root package.json file. + */ + private static get paths() { + const allowedRootPaths = formatGlobCollectionPattern(constants.PROJECT_APP_ALLOWED_ROOT_PATHS); + const allowedExtensions = formatGlobCollectionPattern(constants.PROJECT_ALLOWED_EXTENSIONS); + + return [ + // App default files + `./${allowedRootPaths}/**/*.${allowedExtensions}`, + // Root package.json file + constants.PROJECT_PACKAGE_JSON, + ]; + } + constructor(cwd: string) { - super(cwd); + super(cwd, { paths: AppProject.paths }); this.refreshStrapiVersion(); } @@ -206,6 +217,31 @@ const formatGlobCollectionPattern = (collection: string[]): string => { export class PluginProject extends Project { readonly type = 'plugin' as const satisfies ProjectType; + + /** + * Returns an array of allowed file paths for a Strapi plugin + * + * The resulting paths include plugin default files, the root package.json file, and plugin-specific files. + */ + private static get paths() { + const allowedRootPaths = formatGlobCollectionPattern( + constants.PROJECT_PLUGIN_ALLOWED_ROOT_PATHS + ); + const allowedExtensions = formatGlobCollectionPattern(constants.PROJECT_ALLOWED_EXTENSIONS); + + return [ + // Plugin default files + `./${allowedRootPaths}/**/*.${allowedExtensions}`, + // Root package.json file + constants.PROJECT_PACKAGE_JSON, + // Plugin root files + ...constants.PROJECT_PLUGIN_ROOT_FILES, + ]; + } + + constructor(cwd: string) { + super(cwd, { paths: PluginProject.paths }); + } } const isPlugin = (cwd: string) => { @@ -228,9 +264,5 @@ const isPlugin = (cwd: string) => { export const projectFactory = (cwd: string) => { fse.accessSync(cwd); - if (isPlugin(cwd)) { - return new PluginProject(cwd); - } - - return new AppProject(cwd); + return isPlugin(cwd) ? new PluginProject(cwd) : new AppProject(cwd); }; diff --git a/packages/utils/upgrade/src/modules/project/types.ts b/packages/utils/upgrade/src/modules/project/types.ts index cedb817fa6..1ebe7809f0 100644 --- a/packages/utils/upgrade/src/modules/project/types.ts +++ b/packages/utils/upgrade/src/modules/project/types.ts @@ -22,3 +22,7 @@ export type MinimalPackageJSON = { version: string; dependencies?: Record; } & Utils.JSONObject; + +export interface ProjectConfig { + paths: string[]; +} diff --git a/packages/utils/upgrade/src/tasks/__tests__/codemods.test.ts b/packages/utils/upgrade/src/tasks/__tests__/codemods.test.ts index 0e26111eab..134e67fff5 100644 --- a/packages/utils/upgrade/src/tasks/__tests__/codemods.test.ts +++ b/packages/utils/upgrade/src/tasks/__tests__/codemods.test.ts @@ -43,6 +43,7 @@ describe('codemods task', () => { }); (projectFactory as jest.Mock).mockReturnValue({ + paths: [], dry: jest.fn().mockReturnThis(), onSelectCodemods: jest.fn().mockReturnThis(), setLogger: jest.fn().mockReturnThis(), diff --git a/packages/utils/upgrade/src/tasks/codemods/list-codemods.ts b/packages/utils/upgrade/src/tasks/codemods/list-codemods.ts index 0b33bdf24d..ab433e87df 100644 --- a/packages/utils/upgrade/src/tasks/codemods/list-codemods.ts +++ b/packages/utils/upgrade/src/tasks/codemods/list-codemods.ts @@ -13,7 +13,7 @@ export const listCodemods = async (options: ListCodemodsOptions) => { const project = projectFactory(cwd); const range = findRangeFromTarget(project, target); - logger.debug(`Project: ${f.projectType(project.type)} found in ${f.path(cwd)}`); + logger.debug(f.projectDetails(project)); logger.debug(`Range: set to ${f.versionRange(range)}`); // Create a codemod repository targeting the default location of the codemods diff --git a/packages/utils/upgrade/src/tasks/codemods/run-codemods.ts b/packages/utils/upgrade/src/tasks/codemods/run-codemods.ts index f2aad60eb8..4b88da5f1a 100644 --- a/packages/utils/upgrade/src/tasks/codemods/run-codemods.ts +++ b/packages/utils/upgrade/src/tasks/codemods/run-codemods.ts @@ -17,7 +17,7 @@ export const runCodemods = async (options: RunCodemodsOptions) => { const project = projectFactory(cwd); const range = findRangeFromTarget(project, options.target); - logger.debug(`Project: ${f.projectType(project.type)} found in ${f.path(cwd)}`); + logger.debug(f.projectDetails(project)); logger.debug(`Range: set to ${f.versionRange(range)}`); const codemodRunner = codemodRunnerFactory(project, range) diff --git a/packages/utils/upgrade/src/tasks/upgrade/upgrade.ts b/packages/utils/upgrade/src/tasks/upgrade/upgrade.ts index 3ba8113dd4..3ef7dbafeb 100644 --- a/packages/utils/upgrade/src/tasks/upgrade/upgrade.ts +++ b/packages/utils/upgrade/src/tasks/upgrade/upgrade.ts @@ -19,12 +19,16 @@ export const upgrade = async (options: UpgradeOptions) => { const project = projectFactory(cwd); + logger.debug(f.projectDetails(project)); + if (!isApplicationProject(project)) { throw new Error( `The "${options.target}" upgrade can only be run on a Strapi project; for plugins, please use "codemods".` ); } + const npmPackage = npmPackageFactory(upgraderConstants.STRAPI_PACKAGE_NAME); + // Load all versions from the registry await npmPackage.refresh(); @@ -46,11 +50,11 @@ export const upgrade = async (options: UpgradeOptions) => { .addRequirement(requirements.major.REQUIRE_LATEST_FOR_CURRENT_MAJOR); } - // Make sure the git repository is in an optimal state before running the upgrade + // Make sure the git repository is in an optional state before running the upgrade // Mainly used to ease rollbacks in case the upgrade is corrupted upgrader.addRequirement(requirements.common.REQUIRE_GIT.asOptional()); - // Actually run the upgrade process once configured + // Actually run the upgrade process once configured, // The response contains information about the final status (success/error) const upgradeReport = await upgrader.upgrade();