Fix Upgrade Tool Targeted Files For Plugins (#21033)

This commit is contained in:
Jean-Sébastien Herbaux 2024-08-23 14:20:46 +02:00 committed by GitHub
parent 45c8d25668
commit ca3cb5d50a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 105 additions and 50 deletions

View File

@ -3,7 +3,7 @@ import chalk from 'chalk';
import { constants as timerConstants } from '../timer'; 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 { Codemod } from '../codemod';
import type { Version } from '../version'; import type { Version } from '../version';
import type { Report } from '../report'; import type { Report } from '../report';
@ -18,6 +18,10 @@ export const codemodUID = (uid: string) => {
return chalk.bold.cyan(uid); 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 projectType = (type: ProjectType) => chalk.cyan(type);
export const versionRange = (range: Version.Range) => chalk.italic.yellow(range.raw); export const versionRange = (range: Version.Range) => chalk.italic.yellow(range.raw);

View File

@ -8,7 +8,12 @@ jest.mock('fs', () => fs);
const srcFilename = (cwd: string, filename: string) => path.join(cwd, 'src', filename); const srcFilename = (cwd: string, filename: string) => path.join(cwd, 'src', filename);
const srcFilenames = (cwd: string) => { 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'; const currentStrapiVersion = '1.2.3';
@ -29,7 +34,7 @@ const pluginPackageJSONFile = `{
} }
}`; }`;
const srcFiles = { const defaultFiles = {
'a.ts': 'console.log("a.ts")', 'a.ts': 'console.log("a.ts")',
'b.ts': 'console.log("b.ts")', 'b.ts': 'console.log("b.ts")',
'c.js': 'console.log("c.js")', 'c.js': 'console.log("c.js")',
@ -40,12 +45,12 @@ const srcFiles = {
const appVolume = { const appVolume = {
'package.json': appPackageJSONFile, 'package.json': appPackageJSONFile,
src: srcFiles, src: defaultFiles,
}; };
const pluginVolume = { const pluginVolume = {
'package.json': pluginPackageJSONFile, 'package.json': pluginPackageJSONFile,
src: srcFiles, server: defaultFiles,
}; };
describe('Project', () => { describe('Project', () => {
@ -70,7 +75,7 @@ describe('Project', () => {
}); });
test('Fails on project without package.json file', async () => { test('Fails on project without package.json file', async () => {
vol.fromNestedJSON({ src: srcFiles }, defaultCWD); vol.fromNestedJSON({ src: defaultFiles }, defaultCWD);
expect(() => projectFactory(defaultCWD)).toThrow( expect(() => projectFactory(defaultCWD)).toThrow(
`Could not find a package.json file in ${defaultCWD}` `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 () => { test('Fails when not a plugin and no @strapi/strapi dependency found', async () => {
vol.fromNestedJSON( vol.fromNestedJSON(
{ 'package.json': `{ "name": "test", "version": "1.2.3" }`, src: srcFiles }, { 'package.json': `{ "name": "test", "version": "1.2.3" }`, src: defaultFiles },
defaultCWD defaultCWD
); );
@ -92,7 +97,7 @@ describe('Project', () => {
vol.fromNestedJSON( vol.fromNestedJSON(
{ {
'package.json': `{ "name": "test", "version": "1.2.3", "dependencies": { "@strapi/strapi": "^4.0.0" } }`, 'package.json': `{ "name": "test", "version": "1.2.3", "dependencies": { "@strapi/strapi": "^4.0.0" } }`,
src: srcFiles, src: defaultFiles,
}, },
defaultCWD defaultCWD
); );
@ -133,7 +138,10 @@ describe('Project', () => {
expect(project.files.length).toBe(7); expect(project.files.length).toBe(7);
expect(project.files).toStrictEqual( 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); expect(project.cwd).toBe(defaultCWD);
@ -172,7 +180,10 @@ describe('Project', () => {
expect(project.files.length).toBe(7); expect(project.files.length).toBe(7);
expect(project.files).toStrictEqual( 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); expect(project.cwd).toBe(defaultCWD);

View File

@ -1,8 +1,12 @@
export const PROJECT_PACKAGE_JSON = 'package.json'; 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 // Source files
'js', 'js',
'mjs', 'mjs',
@ -12,14 +16,9 @@ export const PROJECT_DEFAULT_CODE_EXTENSIONS = [
'tsx', 'tsx',
]; ];
export const PROJECT_DEFAULT_JSON_EXTENSIONS = ['json']; export const PROJECT_JSON_EXTENSIONS = ['json'];
export const PROJECT_DEFAULT_ALLOWED_EXTENSIONS = [ export const PROJECT_ALLOWED_EXTENSIONS = [...PROJECT_CODE_EXTENSIONS, ...PROJECT_JSON_EXTENSIONS];
...PROJECT_DEFAULT_CODE_EXTENSIONS,
...PROJECT_DEFAULT_JSON_EXTENSIONS,
];
export const PROJECT_DEFAULT_PATTERNS = ['package.json'];
export const SCOPED_STRAPI_PACKAGE_PREFIX = '@strapi/'; export const SCOPED_STRAPI_PACKAGE_PREFIX = '@strapi/';

View File

@ -12,7 +12,13 @@ import * as constants from './constants';
import type { Version } from '../version'; import type { Version } from '../version';
import type { Codemod } from '../codemod'; import type { Codemod } from '../codemod';
import type { Report } from '../report'; 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 { export class Project {
public cwd: string; public cwd: string;
@ -25,12 +31,15 @@ export class Project {
public packageJSON!: MinimalPackageJSON; public packageJSON!: MinimalPackageJSON;
constructor(cwd: string) { public readonly paths: string[];
constructor(cwd: string, config: ProjectConfig) {
if (!fse.pathExistsSync(cwd)) { if (!fse.pathExistsSync(cwd)) {
throw new Error(`ENOENT: no such file or directory, access '${cwd}'`); throw new Error(`ENOENT: no such file or directory, access '${cwd}'`);
} }
this.cwd = cwd; this.cwd = cwd;
this.paths = config.paths;
this.refresh(); this.refresh();
} }
@ -67,12 +76,8 @@ export class Project {
} }
private createProjectCodemodsRunners(dry: boolean = false) { private createProjectCodemodsRunners(dry: boolean = false) {
const jsonExtensions = constants.PROJECT_DEFAULT_JSON_EXTENSIONS.map<FileExtension>( const jsonExtensions = constants.PROJECT_JSON_EXTENSIONS.map<FileExtension>((ext) => `.${ext}`);
(ext) => `.${ext}` const codeExtensions = constants.PROJECT_CODE_EXTENSIONS.map<FileExtension>((ext) => `.${ext}`);
);
const codeExtensions = constants.PROJECT_DEFAULT_CODE_EXTENSIONS.map<FileExtension>(
(ext) => `.${ext}`
);
const jsonFiles = this.getFilesByExtensions(jsonExtensions); const jsonFiles = this.getFilesByExtensions(jsonExtensions);
const codeFiles = this.getFilesByExtensions(codeExtensions); const codeFiles = this.getFilesByExtensions(codeExtensions);
@ -82,7 +87,7 @@ export class Project {
parser: 'ts', parser: 'ts',
runInBand: true, runInBand: true,
babel: 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 // Don't output any log coming from the runner
print: false, print: false,
silent: true, silent: true,
@ -109,20 +114,9 @@ export class Project {
} }
private refreshProjectFiles(): void { 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); 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; 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) { constructor(cwd: string) {
super(cwd); super(cwd, { paths: AppProject.paths });
this.refreshStrapiVersion(); this.refreshStrapiVersion();
} }
@ -206,6 +217,31 @@ const formatGlobCollectionPattern = (collection: string[]): string => {
export class PluginProject extends Project { export class PluginProject extends Project {
readonly type = 'plugin' as const satisfies ProjectType; 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) => { const isPlugin = (cwd: string) => {
@ -228,9 +264,5 @@ const isPlugin = (cwd: string) => {
export const projectFactory = (cwd: string) => { export const projectFactory = (cwd: string) => {
fse.accessSync(cwd); fse.accessSync(cwd);
if (isPlugin(cwd)) { return isPlugin(cwd) ? new PluginProject(cwd) : new AppProject(cwd);
return new PluginProject(cwd);
}
return new AppProject(cwd);
}; };

View File

@ -22,3 +22,7 @@ export type MinimalPackageJSON = {
version: string; version: string;
dependencies?: Record<string, string>; dependencies?: Record<string, string>;
} & Utils.JSONObject; } & Utils.JSONObject;
export interface ProjectConfig {
paths: string[];
}

View File

@ -43,6 +43,7 @@ describe('codemods task', () => {
}); });
(projectFactory as jest.Mock).mockReturnValue({ (projectFactory as jest.Mock).mockReturnValue({
paths: [],
dry: jest.fn().mockReturnThis(), dry: jest.fn().mockReturnThis(),
onSelectCodemods: jest.fn().mockReturnThis(), onSelectCodemods: jest.fn().mockReturnThis(),
setLogger: jest.fn().mockReturnThis(), setLogger: jest.fn().mockReturnThis(),

View File

@ -13,7 +13,7 @@ export const listCodemods = async (options: ListCodemodsOptions) => {
const project = projectFactory(cwd); const project = projectFactory(cwd);
const range = findRangeFromTarget(project, target); 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)}`); logger.debug(`Range: set to ${f.versionRange(range)}`);
// Create a codemod repository targeting the default location of the codemods // Create a codemod repository targeting the default location of the codemods

View File

@ -17,7 +17,7 @@ export const runCodemods = async (options: RunCodemodsOptions) => {
const project = projectFactory(cwd); const project = projectFactory(cwd);
const range = findRangeFromTarget(project, options.target); 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)}`); logger.debug(`Range: set to ${f.versionRange(range)}`);
const codemodRunner = codemodRunnerFactory(project, range) const codemodRunner = codemodRunnerFactory(project, range)

View File

@ -19,12 +19,16 @@ export const upgrade = async (options: UpgradeOptions) => {
const project = projectFactory(cwd); const project = projectFactory(cwd);
logger.debug(f.projectDetails(project));
if (!isApplicationProject(project)) { if (!isApplicationProject(project)) {
throw new Error( throw new Error(
`The "${options.target}" upgrade can only be run on a Strapi project; for plugins, please use "codemods".` `The "${options.target}" upgrade can only be run on a Strapi project; for plugins, please use "codemods".`
); );
} }
const npmPackage = npmPackageFactory(upgraderConstants.STRAPI_PACKAGE_NAME); const npmPackage = npmPackageFactory(upgraderConstants.STRAPI_PACKAGE_NAME);
// Load all versions from the registry // Load all versions from the registry
await npmPackage.refresh(); await npmPackage.refresh();
@ -46,11 +50,11 @@ export const upgrade = async (options: UpgradeOptions) => {
.addRequirement(requirements.major.REQUIRE_LATEST_FOR_CURRENT_MAJOR); .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 // Mainly used to ease rollbacks in case the upgrade is corrupted
upgrader.addRequirement(requirements.common.REQUIRE_GIT.asOptional()); 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) // The response contains information about the final status (success/error)
const upgradeReport = await upgrader.upgrade(); const upgradeReport = await upgrader.upgrade();