diff --git a/common/changes/rush-migrate-subspace-plugin/pedrogomes-support-clean-subspace_2024-12-20-04-18.json b/common/changes/rush-migrate-subspace-plugin/pedrogomes-support-clean-subspace_2024-12-20-04-18.json new file mode 100644 index 0000000..6628831 --- /dev/null +++ b/common/changes/rush-migrate-subspace-plugin/pedrogomes-support-clean-subspace_2024-12-20-04-18.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "rush-migrate-subspace-plugin", + "comment": "Provides a parameter to automatically remove similar dependency versions, including duplicates and unused", + "type": "minor" + } + ], + "packageName": "rush-migrate-subspace-plugin" +} \ No newline at end of file diff --git a/rush-plugins/rush-migrate-subspace-plugin/command-line.json b/rush-plugins/rush-migrate-subspace-plugin/command-line.json index c6681b2..d785f49 100644 --- a/rush-plugins/rush-migrate-subspace-plugin/command-line.json +++ b/rush-plugins/rush-migrate-subspace-plugin/command-line.json @@ -28,6 +28,12 @@ "longName": "--debug", "description": "Provide debug logs", "associatedCommands": ["migrate-subspace"] + }, + { + "parameterKind": "flag", + "longName": "--clean", + "description": "Merge and clean multiple common versions together", + "associatedCommands": ["migrate-subspace"] } ] } diff --git a/rush-plugins/rush-migrate-subspace-plugin/src/cleanSubspace.ts b/rush-plugins/rush-migrate-subspace-plugin/src/cleanSubspace.ts index a847bf5..e329eb4 100644 --- a/rush-plugins/rush-migrate-subspace-plugin/src/cleanSubspace.ts +++ b/rush-plugins/rush-migrate-subspace-plugin/src/cleanSubspace.ts @@ -1,40 +1,285 @@ -import { chooseSubspacePrompt } from './prompts/subspace'; +import { + chooseSubspacePrompt, + scanForUnusedDependencyVersionsPrompt, + scanForDuplicatedDependenciesPrompt, + scanForSupersetDependencyVersionsPrompt, + scanForAllDependenciesPrompt +} from './prompts/subspace'; import Console from './providers/console'; -import { getRootPath } from './utilities/path'; import { Colorize } from '@rushstack/terminal'; import { - cleanSubspaceCommonVersions, getRushSubspaceCommonVersionsFilePath, - isSubspaceSupported + getSubspaceDependencies, + isSubspaceSupported, + loadRushSubspaceCommonVersions, + queryProjectsFromSubspace } from './utilities/subspace'; import { getRushSubspacesConfigurationJsonPath, querySubspaces } from './utilities/repository'; import { RushConstants } from '@rushstack/rush-sdk'; +import { chooseDependencyPrompt, confirmNextDependencyPrompt } from './prompts/dependency'; +import { IPackageJson, JsonFile } from '@rushstack/node-core-library'; +import { reverseSortVersions, subsetVersion } from './utilities/dependency'; +import { + getProjectPackageFilePath, + loadProjectPackageJson, + updateProjectDependency +} from './utilities/project'; +import { IRushConfigurationProjectJson } from '@rushstack/rush-sdk/lib/api/RushConfigurationProject'; +import { RESERVED_VERSIONS } from './constants/versions'; + +const removeSupersetDependency = ( + subspaceName: string, + dependencyName: string, + versionsMap: Map, + rootPath: string +): number => { + const versions: string[] = Array.from(versionsMap.keys()); + const subspaceCommonVersionsPath: string = getRushSubspaceCommonVersionsFilePath(subspaceName, rootPath); + const subspaceCommonVersionsJson: RushSubspaceCommonVersionsJson = loadRushSubspaceCommonVersions( + subspaceName, + rootPath + ); + + const newValidVersions: string[] = reverseSortVersions(versions).reduce( + (prevVersions, currVersion) => { + const newVersions: string[] = [...prevVersions]; + if (newVersions.includes(currVersion)) { + return newVersions; + } + + const newSubsetVersion: string | undefined = newVersions.find((newVersion) => + subsetVersion(newVersion, currVersion) + ); + + if (RESERVED_VERSIONS.includes(currVersion) || !newSubsetVersion) { + newVersions.push(currVersion); + } else { + // Update projects with new subset version + for (const projectName of versionsMap.get(currVersion) || []) { + if (updateProjectDependency(projectName, dependencyName, newSubsetVersion, rootPath)) { + Console.debug( + `Updated project ${Colorize.bold(projectName)} for dependency ${Colorize.bold( + dependencyName + )} ${Colorize.bold(currVersion)} => ${Colorize.bold(newSubsetVersion)}!` + ); + } + } + } + + return newVersions; + }, + [] + ); + + const removedAlternativeVersionsCount: number = versions.length - newValidVersions.length; + if (removedAlternativeVersionsCount > 0) { + // Update subspace common versions + if (newValidVersions.length > 0) { + subspaceCommonVersionsJson.allowedAlternativeVersions![dependencyName] = newValidVersions; + } else { + delete subspaceCommonVersionsJson.allowedAlternativeVersions![dependencyName]; + } + + JsonFile.save(subspaceCommonVersionsJson, subspaceCommonVersionsPath); + } + + return removedAlternativeVersionsCount; +}; + +const removeDuplicatedDependencies = (subspaceName: string, rootPath: string): void => { + Console.log(`Removing duplicated dependencies for subspace ${Colorize.bold(subspaceName)}...`); + + const projects: IRushConfigurationProjectJson[] = queryProjectsFromSubspace(subspaceName, rootPath); + let countRemoved: number = 0; + + for (const project of projects) { + const projectPackageFilePath: string = getProjectPackageFilePath(project.projectFolder, rootPath); + const projectPackageJson: IPackageJson = loadProjectPackageJson(project.projectFolder, rootPath); + + const dependencies: string[] = Object.keys(projectPackageJson.dependencies || {}); + const devDependencies: string[] = Object.keys(projectPackageJson.devDependencies || {}); + + for (const devDependency of devDependencies) { + if (dependencies.includes(devDependency)) { + countRemoved += 1; + Console.debug( + `Removed ${Colorize.bold(devDependency)} from project ${Colorize.bold(project.packageName)}` + ); + delete projectPackageJson.devDependencies![devDependency]; + } + } + + JsonFile.save(projectPackageJson, projectPackageFilePath); + } + + if (countRemoved > 0) { + Console.success( + `Removed ${Colorize.bold(`${countRemoved}`)} duplicated dependencies from subspace ${Colorize.bold( + subspaceName + )}!` + ); + } else { + Console.success(`No duplicated dependencies found for subspace ${Colorize.bold(subspaceName)}!`); + } +}; + +const removeUnusedAlternativeVersions = ( + subspaceName: string, + subspaceDependencies: Map>, + rootPath: string +): void => { + Console.log(`Removing unused alternative versions for subspace ${Colorize.bold(subspaceName)}...`); + + const subspaceCommonVersionsPath: string = getRushSubspaceCommonVersionsFilePath(subspaceName, rootPath); + const subspaceCommonVersionsJson: RushSubspaceCommonVersionsJson = loadRushSubspaceCommonVersions( + subspaceName, + rootPath + ); + + if (!subspaceCommonVersionsJson.allowedAlternativeVersions) { + return; + } + + let countRemoved: number = 0; + + for (const [dependency, alternativeVersions] of Object.entries( + subspaceCommonVersionsJson.allowedAlternativeVersions + )) { + const subspaceDependency: Map | undefined = subspaceDependencies.get(dependency); + const newAlternativeVersions: string[] = + subspaceDependency && subspaceDependency.size > 1 + ? alternativeVersions.filter((version) => subspaceDependency.has(version)) + : []; + + const removedAlternativeVersionsCount: number = + alternativeVersions.length - newAlternativeVersions.length; + if (removedAlternativeVersionsCount > 0) { + countRemoved += removedAlternativeVersionsCount; + Console.debug( + `Moving from [${Colorize.bold(alternativeVersions.join(','))}] to [${Colorize.bold( + newAlternativeVersions.join(',') + )}] for dependency ${Colorize.bold(dependency)}` + ); + } + + if (newAlternativeVersions.length === 0) { + delete subspaceCommonVersionsJson.allowedAlternativeVersions[dependency]; + continue; + } -export const cleanSubspace = async (): Promise => { + subspaceCommonVersionsJson.allowedAlternativeVersions = { + ...subspaceCommonVersionsJson.allowedAlternativeVersions, + [dependency]: newAlternativeVersions + }; + } + + if (countRemoved > 0) { + JsonFile.save(subspaceCommonVersionsJson, subspaceCommonVersionsPath); + Console.success( + `Removed ${Colorize.bold(`${countRemoved}`)} unused alternative versions from subspace ${Colorize.bold( + subspaceName + )}!` + ); + } else { + Console.success(`No unused alternative versions found for subspace ${Colorize.bold(subspaceName)}!`); + } +}; + +const removeSupersetDependencyVersions = async ( + subspaceName: string, + subspaceDependencies: Map>, + rootPath: string +): Promise => { + const multipleVersionDependencies: string[] = Array.from(subspaceDependencies.keys()).filter( + (dependency) => subspaceDependencies.get(dependency)!.size > 1 + ); + + if (multipleVersionDependencies.length === 0) { + Console.success( + `The subspace ${Colorize.bold(subspaceName)} doesn't contain alternative versions! Exiting...` + ); + return; + } + + if (await scanForAllDependenciesPrompt()) { + Console.log(`Removing superset versions for subspace ${Colorize.bold(subspaceName)}...`); + const countPerDependency: number[] = Array.from(subspaceDependencies.entries()).map( + ([dependency, versionsMap]) => removeSupersetDependency(subspaceName, dependency, versionsMap, rootPath) + ); + + const count: number = countPerDependency.reduce((a, b) => a + b, 0); + if (count > 0) { + Console.success(`Removed ${Colorize.bold(`${count}`)} superset alternative versions!`); + } else { + Console.success(`No alternative versions have been removed!`); + } + + return; + } + + do { + const selectedDependency: string = await chooseDependencyPrompt(multipleVersionDependencies); + + Console.log(`Removing superset versions for dependency ${Colorize.bold(selectedDependency)}...`); + const count: number = await removeSupersetDependency( + subspaceName, + selectedDependency, + subspaceDependencies.get(selectedDependency) as Map, + rootPath + ); + + if (count > 0) { + Console.success( + `Removed ${Colorize.bold(`${count}`)} superset alternative versions for dependency ${Colorize.bold( + selectedDependency + )}!` + ); + } else { + Console.success( + `No alternative versions have been removed for dependency ${Colorize.bold(selectedDependency)}!` + ); + } + + const index: number = multipleVersionDependencies.indexOf(selectedDependency); + multipleVersionDependencies.splice(index, 1); + } while (multipleVersionDependencies.length > 0 && (await confirmNextDependencyPrompt())); +}; + +export const cleanSubspace = async (rootPath: string): Promise => { Console.debug('Executing clean subspace command...'); - const targetSubspaces: string[] = querySubspaces(); - if (!isSubspaceSupported()) { + const targetSubspaces: string[] = querySubspaces(rootPath); + if (!isSubspaceSupported(rootPath)) { Console.error( - `The monorepo ${Colorize.bold( - getRootPath() - )} doesn't support subspaces! Make sure you have ${Colorize.bold( - getRushSubspacesConfigurationJsonPath() + `The monorepo ${Colorize.bold(rootPath)} doesn't support subspaces! Make sure you have ${Colorize.bold( + getRushSubspacesConfigurationJsonPath(rootPath) )} with the ${Colorize.bold(RushConstants.defaultSubspaceName)} subspace. Exiting...` ); return; } const targetSubspace: string = await chooseSubspacePrompt(targetSubspaces); - Console.title(`🛁 Cleaning subspace ${Colorize.underline(targetSubspace)} common versions...`); + Console.title(`🛁 Cleaning subspace ${Colorize.underline(targetSubspace)} alternative versions...`); - if (cleanSubspaceCommonVersions(targetSubspace)) { - Console.success( - `${Colorize.bold( - getRushSubspaceCommonVersionsFilePath(targetSubspace) - )} has been successfully refactored!` - ); - } else { - Console.success(`The subspace ${Colorize.bold(targetSubspace)} doesn't require cleaning! Exiting...`); + let subspaceDependencies: Map> = getSubspaceDependencies( + targetSubspace, + rootPath + ); + if (await scanForDuplicatedDependenciesPrompt()) { + removeDuplicatedDependencies(targetSubspace, rootPath); + subspaceDependencies = getSubspaceDependencies(targetSubspace, rootPath); + } + + if (await scanForSupersetDependencyVersionsPrompt()) { + await removeSupersetDependencyVersions(targetSubspace, subspaceDependencies, rootPath); + subspaceDependencies = getSubspaceDependencies(targetSubspace, rootPath); + } + + if (await scanForUnusedDependencyVersionsPrompt()) { + removeUnusedAlternativeVersions(targetSubspace, subspaceDependencies, rootPath); } + + Console.warn( + `Please run "rush update --subspace ${targetSubspace}" to update the subspace shrinkwrap file.` + ); }; diff --git a/rush-plugins/rush-migrate-subspace-plugin/src/cli.ts b/rush-plugins/rush-migrate-subspace-plugin/src/cli.ts index 7c3574f..31e1f8d 100644 --- a/rush-plugins/rush-migrate-subspace-plugin/src/cli.ts +++ b/rush-plugins/rush-migrate-subspace-plugin/src/cli.ts @@ -1,10 +1,14 @@ import { Command } from 'commander'; import inquirer from 'inquirer'; +import path from 'path'; +import { IPackageJson, JsonFile } from '@rushstack/node-core-library'; import inquirerSearchList from 'inquirer-search-list'; import { syncVersions } from './syncVersions'; import { migrateProject } from './migrateProject'; import Console from './providers/console'; import { interactMenu } from './interactMenu'; +import { cleanSubspace } from './cleanSubspace'; +import { getRootPath } from './utilities/path'; inquirer.registerPrompt('search-list', inquirerSearchList); @@ -13,19 +17,27 @@ const program: Command = new Command(); program .option('--sync', 'to sync the versions in a subspace') .option('--move', 'to move projects to a new subspace') + .option('--clean', 'to reduce subspace alternative versions') .option('--debug', 'to provide debug logs') - .description('Example: rush migrate-subspace [--move] [--sync] [--debug]') - .action(async ({ sync, debug, move }) => { + .description('Example: rush migrate-subspace [--move] [--sync] [--debug] [--clean]') + .action(async ({ sync, debug, move, clean }) => { + const packageJson: IPackageJson = JsonFile.load(`${path.resolve(__dirname, '../package.json')}`); + Console.enableDebug(debug); - Console.title('🚀 Welcome to the Rush Migrate Subspace Plugin!'); + Console.title(`🚀 Rush Migrate Subspace Plugin - version ${packageJson.version}`); Console.newLine(); + const sourceMonorepoPath: string = getRootPath(); + const targetMonorepoPath: string = getRootPath(); + if (sync) { - await syncVersions(); + await syncVersions(targetMonorepoPath); } else if (move) { - await migrateProject(); + await migrateProject(sourceMonorepoPath, targetMonorepoPath); + } else if (clean) { + await cleanSubspace(targetMonorepoPath); } else { - await interactMenu(); + await interactMenu(sourceMonorepoPath, targetMonorepoPath); } }); diff --git a/rush-plugins/rush-migrate-subspace-plugin/src/constants/versions.ts b/rush-plugins/rush-migrate-subspace-plugin/src/constants/versions.ts new file mode 100644 index 0000000..dee0bb6 --- /dev/null +++ b/rush-plugins/rush-migrate-subspace-plugin/src/constants/versions.ts @@ -0,0 +1,4 @@ +export const LATEST_VERSION: string = 'latest'; +export const LOCAL_VERSION: string = 'workspace:*'; + +export const RESERVED_VERSIONS: string[] = [LATEST_VERSION, LOCAL_VERSION]; diff --git a/rush-plugins/rush-migrate-subspace-plugin/src/functions/addProjectToSubspace.ts b/rush-plugins/rush-migrate-subspace-plugin/src/functions/addProjectToSubspace.ts index f074e6c..d2594aa 100644 --- a/rush-plugins/rush-migrate-subspace-plugin/src/functions/addProjectToSubspace.ts +++ b/rush-plugins/rush-migrate-subspace-plugin/src/functions/addProjectToSubspace.ts @@ -5,7 +5,6 @@ import Console from '../providers/console'; import { Colorize } from '@rushstack/terminal'; import { IRushConfigurationProjectJson } from '@rushstack/rush-sdk/lib/api/RushConfigurationProject'; import { enterNewProjectLocationPrompt, moveProjectPrompt } from '../prompts/project'; -import { getRootPath } from '../utilities/path'; import { RushConstants } from '@rushstack/rush-sdk'; import { addProjectToRushConfiguration } from './updateRushConfiguration'; import { @@ -20,7 +19,7 @@ import path from 'path'; const refreshSubspace = (subspaceName: string, rootPath: string): void => { const subspacesConfig: ISubspacesConfigurationJson = loadRushSubspacesConfiguration(rootPath); const newSubspaces: string[] = [...subspacesConfig.subspaceNames]; - if (queryProjectsFromSubspace(subspaceName).length === 0) { + if (queryProjectsFromSubspace(subspaceName, rootPath).length === 0) { newSubspaces.splice(newSubspaces.indexOf(subspaceName), 1); } @@ -31,11 +30,10 @@ const refreshSubspace = (subspaceName: string, rootPath: string): void => { const moveProjectToSubspaceFolder = async ( sourceProjectFolderPath: string, - targetSubspace: string + targetSubspace: string, + rootPath: string ): Promise => { - const targetSubspaceFolderPath: string = `${getRootPath()}/${ - RushNameConstants.SubspacesFolderName - }/${targetSubspace}`; + const targetSubspaceFolderPath: string = `${rootPath}/${RushNameConstants.SubspacesFolderName}/${targetSubspace}`; const targetProjectFolderPath: string = await enterNewProjectLocationPrompt( sourceProjectFolderPath, @@ -71,7 +69,8 @@ const moveProjectToSubspaceFolder = async ( export const addProjectToSubspace = async ( sourceProject: IRushConfigurationProjectJson, targetSubspace: string, - sourceMonorepoPath: string + sourceMonorepoPath: string, + targetMonorepoPath: string ): Promise => { Console.debug( `Adding project ${Colorize.bold(sourceProject.packageName)} to subspace ${Colorize.bold( @@ -79,16 +78,24 @@ export const addProjectToSubspace = async ( )}...` ); - let targetProjectFolderPath: string | undefined = `${getRootPath()}/${sourceProject.projectFolder}`; - if (isExternalMonorepo(sourceMonorepoPath) || (await moveProjectPrompt())) { + let targetProjectFolderPath: string | undefined = `${targetMonorepoPath}/${sourceProject.projectFolder}`; + if (isExternalMonorepo(sourceMonorepoPath, targetMonorepoPath) || (await moveProjectPrompt())) { const sourceProjectFolderPath: string = `${sourceMonorepoPath}/${sourceProject.projectFolder}`; - targetProjectFolderPath = await moveProjectToSubspaceFolder(sourceProjectFolderPath, targetSubspace); + targetProjectFolderPath = await moveProjectToSubspaceFolder( + sourceProjectFolderPath, + targetSubspace, + targetMonorepoPath + ); if (!targetProjectFolderPath) { return; } - if (FileSystem.exists(`${getRootPath()}/${RushNameConstants.EdenMonorepoFileName}`)) { - await updateEdenProject(sourceProject, path.relative(sourceMonorepoPath, targetProjectFolderPath)); + if (FileSystem.exists(`${targetMonorepoPath}/${RushNameConstants.EdenMonorepoFileName}`)) { + await updateEdenProject( + sourceProject, + path.relative(sourceMonorepoPath, targetProjectFolderPath), + targetMonorepoPath + ); } } @@ -103,7 +110,7 @@ export const addProjectToSubspace = async ( FileSystem.deleteFolder(targetLegacySubspaceFolderPath); } - addProjectToRushConfiguration(sourceProject, targetSubspace, targetProjectFolderPath); + addProjectToRushConfiguration(sourceProject, targetSubspace, targetProjectFolderPath, targetMonorepoPath); if (sourceProject.subspaceName) { refreshSubspace(sourceProject.subspaceName, sourceMonorepoPath); } diff --git a/rush-plugins/rush-migrate-subspace-plugin/src/functions/createSubspace.ts b/rush-plugins/rush-migrate-subspace-plugin/src/functions/createSubspace.ts index 6913e61..2b90b97 100644 --- a/rush-plugins/rush-migrate-subspace-plugin/src/functions/createSubspace.ts +++ b/rush-plugins/rush-migrate-subspace-plugin/src/functions/createSubspace.ts @@ -8,9 +8,9 @@ import { import { ISubspacesConfigurationJson } from '@rushstack/rush-sdk/lib/api/SubspacesConfiguration'; import { getRushSubspaceConfigurationFolderPath } from '../utilities/subspace'; -export const createSubspace = async (subspaceName: string): Promise => { +export const createSubspace = async (subspaceName: string, rootPath: string): Promise => { Console.debug(`Creating subspace ${Colorize.bold(subspaceName)}...`); - const subspaceConfigFolder: string = getRushSubspaceConfigurationFolderPath(subspaceName); + const subspaceConfigFolder: string = getRushSubspaceConfigurationFolderPath(subspaceName, rootPath); FileSystem.ensureFolder(subspaceConfigFolder); const files: string[] = ['.npmrc', 'common-versions.json', 'repo-state.json']; @@ -23,17 +23,19 @@ export const createSubspace = async (subspaceName: string): Promise => { } } - const subspacesConfigJson: ISubspacesConfigurationJson = loadRushSubspacesConfiguration(); + const subspacesConfigJson: ISubspacesConfigurationJson = loadRushSubspacesConfiguration(rootPath); if (!subspacesConfigJson.subspaceNames.includes(subspaceName)) { Console.debug( - `Updating ${getRushSubspacesConfigurationJsonPath()} by adding ${Colorize.bold(subspaceName)}...` + `Updating ${getRushSubspacesConfigurationJsonPath(rootPath)} by adding ${Colorize.bold( + subspaceName + )}...` ); const newSubspacesConfigJson: ISubspacesConfigurationJson = { ...subspacesConfigJson, subspaceNames: [...subspacesConfigJson.subspaceNames, subspaceName] }; - JsonFile.save(newSubspacesConfigJson, getRushSubspacesConfigurationJsonPath(), { + JsonFile.save(newSubspacesConfigJson, getRushSubspacesConfigurationJsonPath(rootPath), { updateExistingFile: true }); } diff --git a/rush-plugins/rush-migrate-subspace-plugin/src/functions/generateReport.ts b/rush-plugins/rush-migrate-subspace-plugin/src/functions/generateReport.ts index 483b4d2..a5f194f 100644 --- a/rush-plugins/rush-migrate-subspace-plugin/src/functions/generateReport.ts +++ b/rush-plugins/rush-migrate-subspace-plugin/src/functions/generateReport.ts @@ -5,16 +5,15 @@ import { Colorize } from '@rushstack/terminal'; import { RushNameConstants } from '../constants/paths'; import { getProjectMismatches } from '../utilities/project'; import { VersionMismatchFinderEntity } from '@rushstack/rush-sdk/lib/logic/versionMismatch/VersionMismatchFinderEntity'; -import { getRootPath } from '../utilities/path'; import { sortVersions } from '../utilities/dependency'; -export const generateReport = async (projectName: string): Promise => { +export const generateReport = async (projectName: string, rootPath: string): Promise => { Console.debug(`Generating mismatches report for the project ${Colorize.bold(projectName)}...`); const projectMismatches: ReadonlyMap< string, ReadonlyMap - > = getProjectMismatches(projectName); + > = getProjectMismatches(projectName, rootPath); if (projectMismatches.size === 0) { Console.success(`No mismatches found for the project ${Colorize.bold(projectName)}.`); return; @@ -39,7 +38,7 @@ export const generateReport = async (projectName: string): Promise => { } } - const reportFilePath: string = `${getRootPath()}/${projectName}_${RushNameConstants.AnalysisFileName}`; + const reportFilePath: string = `${rootPath}/${projectName}_${RushNameConstants.AnalysisFileName}`; const jsonFilePath: string = await enterReportFileLocationPrompt(reportFilePath); JsonFile.save(output, jsonFilePath, { prettyFormatting: true }); Console.success(`Saved report file to ${Colorize.bold(jsonFilePath)}.`); diff --git a/rush-plugins/rush-migrate-subspace-plugin/src/functions/initSubspaces.ts b/rush-plugins/rush-migrate-subspace-plugin/src/functions/initSubspaces.ts index 1bd67e6..d46c674 100644 --- a/rush-plugins/rush-migrate-subspace-plugin/src/functions/initSubspaces.ts +++ b/rush-plugins/rush-migrate-subspace-plugin/src/functions/initSubspaces.ts @@ -5,12 +5,11 @@ import { RushConstants } from '@rushstack/rush-sdk'; import { getRushSubspacesConfigurationJsonPath } from '../utilities/repository'; import { getRushSubspaceConfigurationFolderPath } from '../utilities/subspace'; import { Colorize } from '@rushstack/terminal'; -import { getRootPath } from '../utilities/path'; -export const initSubspaces = async (): Promise => { - Console.debug(`Starting subspaces feature on ${Colorize.bold(getRootPath())}...`); +export const initSubspaces = async (rootPath: string): Promise => { + Console.debug(`Starting subspaces feature on ${Colorize.bold(rootPath)}...`); - const subspacesConfigurationJsonPath: string = getRushSubspacesConfigurationJsonPath(); + const subspacesConfigurationJsonPath: string = getRushSubspacesConfigurationJsonPath(rootPath); if (!FileSystem.exists(subspacesConfigurationJsonPath)) { FileSystem.copyFile({ sourcePath: FileSystem.getRealPath(`${__dirname}/../templates/subspaces.json`), @@ -20,7 +19,9 @@ export const initSubspaces = async (): Promise => { Console.success(`${Colorize.bold(subspacesConfigurationJsonPath)} file created successfully!`); } - if (!FileSystem.exists(getRushSubspaceConfigurationFolderPath(RushConstants.defaultSubspaceName))) { - await createSubspace(RushConstants.defaultSubspaceName); + if ( + !FileSystem.exists(getRushSubspaceConfigurationFolderPath(RushConstants.defaultSubspaceName, rootPath)) + ) { + await createSubspace(RushConstants.defaultSubspaceName, rootPath); } }; diff --git a/rush-plugins/rush-migrate-subspace-plugin/src/functions/syncProjectDependencies.ts b/rush-plugins/rush-migrate-subspace-plugin/src/functions/syncProjectDependencies.ts index 2d92909..dd715eb 100644 --- a/rush-plugins/rush-migrate-subspace-plugin/src/functions/syncProjectDependencies.ts +++ b/rush-plugins/rush-migrate-subspace-plugin/src/functions/syncProjectDependencies.ts @@ -23,11 +23,14 @@ import { generateReport } from './generateReport'; const addVersionToCommonVersionConfiguration = ( subspaceName: string, dependencyName: string, - selectedVersion: string + selectedVersion: string, + rootPath: string ): void => { - const subspaceCommonVersionsPath: string = getRushSubspaceCommonVersionsFilePath(subspaceName); - const subspaceCommonVersionsJson: RushSubspaceCommonVersionsJson = - loadRushSubspaceCommonVersions(subspaceName); + const subspaceCommonVersionsPath: string = getRushSubspaceCommonVersionsFilePath(subspaceName, rootPath); + const subspaceCommonVersionsJson: RushSubspaceCommonVersionsJson = loadRushSubspaceCommonVersions( + subspaceName, + rootPath + ); Console.debug( `Adding ${Colorize.bold(selectedVersion)} to allowedAlternativeVersions of ${Colorize.bold( @@ -49,16 +52,19 @@ const addVersionToCommonVersionConfiguration = ( const syncDependencyVersion = async ( dependencyToUpdate: string, - projectToUpdate: string + projectToUpdate: string, + rootPath: string ): Promise => { const dependencyName: string = dependencyToUpdate.replace(' (cyclic)', ''); - const project: IRushConfigurationProjectJson | undefined = queryProject(projectToUpdate); + const project: IRushConfigurationProjectJson | undefined = queryProject(projectToUpdate, rootPath); if (!project || !project.subspaceName) { return false; } - const projectDependencies: IPackageJsonDependencyTable | undefined = - getProjectDependencies(projectToUpdate); + const projectDependencies: IPackageJsonDependencyTable | undefined = getProjectDependencies( + projectToUpdate, + rootPath + ); const currentVersion: string | undefined = projectDependencies?.[dependencyName]; if (!currentVersion) { Console.error( @@ -69,12 +75,16 @@ const syncDependencyVersion = async ( return false; } - const subspaceCommonVersionsJson: JsonObject = loadRushSubspaceCommonVersions(project.subspaceName); + const subspaceCommonVersionsJson: JsonObject = loadRushSubspaceCommonVersions( + project.subspaceName, + rootPath + ); const subspaceAlternativeVersions: string[] = subspaceCommonVersionsJson.allowedAlternativeVersions[dependencyName] || []; const subspaceDependencies: Map> = getSubspaceDependencies( - project.subspaceName + project.subspaceName, + rootPath ); const subspaceVersionsMap: Map = subspaceDependencies.get(dependencyName) as Map< @@ -118,22 +128,22 @@ const syncDependencyVersion = async ( return false; } else if (versionToSync === 'manual') { const newVersion: string = (await enterVersionPrompt(dependencyName)).trim(); - addVersionToCommonVersionConfiguration(project.subspaceName, dependencyName, newVersion); - await updateProjectDependency(projectToUpdate, dependencyName, newVersion); + addVersionToCommonVersionConfiguration(project.subspaceName, dependencyName, newVersion, rootPath); + await updateProjectDependency(projectToUpdate, dependencyName, newVersion, rootPath); } else if (versionToSync === 'alternative') { - addVersionToCommonVersionConfiguration(project.subspaceName, dependencyName, currentVersion); + addVersionToCommonVersionConfiguration(project.subspaceName, dependencyName, currentVersion, rootPath); } else { - await updateProjectDependency(projectToUpdate, dependencyName, versionToSync); + await updateProjectDependency(projectToUpdate, dependencyName, versionToSync, rootPath); } return true; }; -const fetchProjectMismatches = (projectName: string): string[] => { +const fetchProjectMismatches = (projectName: string, rootPath: string): string[] => { const projectMismatches: ReadonlyMap< string, ReadonlyMap - > = getProjectMismatches(projectName); + > = getProjectMismatches(projectName, rootPath); const mismatchedDependencies: string[] = Array.from(projectMismatches.keys()); if (mismatchedDependencies.length === 0) { @@ -157,16 +167,19 @@ const fetchProjectMismatches = (projectName: string): string[] => { return mismatchedDependencies; }; -export const syncProjectMismatchedDependencies = async (projectName: string): Promise => { +export const syncProjectMismatchedDependencies = async ( + projectName: string, + rootPath: string +): Promise => { Console.title(`🔄 Syncing version mismatches for project ${Colorize.bold(projectName)}...`); - let mismatchedDependencies: string[] = fetchProjectMismatches(projectName); + let mismatchedDependencies: string[] = fetchProjectMismatches(projectName, rootPath); if (mismatchedDependencies.length === 0) { Console.success(`No mismatches found in the project ${Colorize.bold(projectName)}!`); return true; } - const project: IRushConfigurationProjectJson | undefined = queryProject(projectName); + const project: IRushConfigurationProjectJson | undefined = queryProject(projectName, rootPath); if (!project || !project.subspaceName) { Console.error(`Project ${Colorize.bold(projectName)} is not part of a subspace!`); return true; @@ -175,15 +188,21 @@ export const syncProjectMismatchedDependencies = async (projectName: string): Pr const nextCommand: string = await chooseSyncCommandPrompt(project.packageName); switch (nextCommand) { case 'report': - await generateReport(project.packageName); + await generateReport(project.packageName, rootPath); break; case 'fix': do { - const selectedDependency: string = await chooseDependencyPrompt(mismatchedDependencies); - if (await syncDependencyVersion(selectedDependency, projectName)) { - mismatchedDependencies = fetchProjectMismatches(projectName); + const selectedDependency: string = await chooseDependencyPrompt( + mismatchedDependencies, + ` (${Colorize.bold(`${mismatchedDependencies.length}`)} mismatched dependencies)` + ); + if (await syncDependencyVersion(selectedDependency, projectName, rootPath)) { + mismatchedDependencies = fetchProjectMismatches(projectName, rootPath); } - } while (mismatchedDependencies.length > 0 && (await confirmNextDependencyPrompt(projectName))); + } while ( + mismatchedDependencies.length > 0 && + (await confirmNextDependencyPrompt(` (Current project: ${Colorize.bold(projectName)})`)) + ); break; case 'skip': diff --git a/rush-plugins/rush-migrate-subspace-plugin/src/functions/updateEdenProject.ts b/rush-plugins/rush-migrate-subspace-plugin/src/functions/updateEdenProject.ts index a768bb7..a1df07c 100644 --- a/rush-plugins/rush-migrate-subspace-plugin/src/functions/updateEdenProject.ts +++ b/rush-plugins/rush-migrate-subspace-plugin/src/functions/updateEdenProject.ts @@ -3,15 +3,15 @@ import { RushNameConstants } from '../constants/paths'; import Console from '../providers/console'; import { Colorize } from '@rushstack/terminal'; import { IRushConfigurationProjectJson } from '@rushstack/rush-sdk/lib/api/RushConfigurationProject'; -import { getRootPath } from '../utilities/path'; export async function updateEdenProject( sourceProject: IRushConfigurationProjectJson, - targetProjectFolderPath: string + targetProjectFolderPath: string, + rootPath: string ): Promise { - Console.debug(`Update monorepo eden configuration on ${Colorize.bold(getRootPath())}...`); + Console.debug(`Update monorepo eden configuration on ${Colorize.bold(rootPath)}...`); - const edenPipelineFilePath: string = `${getRootPath()}/${RushNameConstants.EdenPipelineFileName}`; + const edenPipelineFilePath: string = `${rootPath}/${RushNameConstants.EdenPipelineFileName}`; const edenPipelineJson: JsonObject = JsonFile.load(edenPipelineFilePath); for (const entry of Object.values(edenPipelineJson.scene.scm)) { if (entry.pipelinePath?.includes(sourceProject.projectFolder)) { @@ -30,7 +30,7 @@ export async function updateEdenProject( } } - const edenMonorepoFilePath: string = `${getRootPath()}/${RushNameConstants.EdenMonorepoFileName}`; + const edenMonorepoFilePath: string = `${rootPath}/${RushNameConstants.EdenMonorepoFileName}`; const edenMonorepoJson: JsonObject = JsonFile.load(edenMonorepoFilePath); const edenProject: JsonObject = edenMonorepoJson.packages.find( ({ name }: JsonObject) => name === sourceProject.packageName diff --git a/rush-plugins/rush-migrate-subspace-plugin/src/functions/updateProjectDependency.ts b/rush-plugins/rush-migrate-subspace-plugin/src/functions/updateProjectDependency.ts index 8b705fc..443f584 100644 --- a/rush-plugins/rush-migrate-subspace-plugin/src/functions/updateProjectDependency.ts +++ b/rush-plugins/rush-migrate-subspace-plugin/src/functions/updateProjectDependency.ts @@ -7,15 +7,16 @@ import Console from '../providers/console'; export const updateProjectDependency = async ( projectName: string, dependencyName: string, - newVersion: string + newVersion: string, + rootPath: string ): Promise => { - const project: IRushConfigurationProjectJson | undefined = queryProject(projectName); + const project: IRushConfigurationProjectJson | undefined = queryProject(projectName, rootPath); if (!project) { Console.error(`Could not load find the project ${Colorize.bold(projectName)}.`); return false; } - const pkgJsonPath: string = getProjectPackageFilePath(project.projectFolder); + const pkgJsonPath: string = getProjectPackageFilePath(project.projectFolder, rootPath); if (!FileSystem.exists(pkgJsonPath)) { Console.error(`Could not load ${Colorize.bold(pkgJsonPath)}.`); return false; diff --git a/rush-plugins/rush-migrate-subspace-plugin/src/functions/updateRushConfiguration.ts b/rush-plugins/rush-migrate-subspace-plugin/src/functions/updateRushConfiguration.ts index 8edee35..98ba840 100644 --- a/rush-plugins/rush-migrate-subspace-plugin/src/functions/updateRushConfiguration.ts +++ b/rush-plugins/rush-migrate-subspace-plugin/src/functions/updateRushConfiguration.ts @@ -1,5 +1,4 @@ import { IRushConfigurationProjectJson } from '@rushstack/rush-sdk/lib/api/RushConfigurationProject'; -import { getRootPath } from '../utilities/path'; import { IRushConfigurationJson } from '@rushstack/rush-sdk/lib/api/RushConfiguration'; import { loadRushConfiguration } from '../utilities/repository'; import Console from '../providers/console'; @@ -10,10 +9,10 @@ import path from 'path'; export const removeProjectFromRushConfiguration = ( project: IRushConfigurationProjectJson, - monoRepoRootPath: string = getRootPath() + rootPath: string ): void => { - const rushConfigFile: string = `${monoRepoRootPath}/${RushConstants.rushJsonFilename}`; - const rushConfig: IRushConfigurationJson = loadRushConfiguration(monoRepoRootPath); + const rushConfigFile: string = `${rootPath}/${RushConstants.rushJsonFilename}`; + const rushConfig: IRushConfigurationJson = loadRushConfiguration(rootPath); const projectIndex: number = rushConfig.projects.findIndex( ({ packageName }) => packageName === project.packageName ); @@ -47,17 +46,17 @@ export const addProjectToRushConfiguration = ( sourceProject: IRushConfigurationProjectJson, targetSubspace: string, targetProjectFolderPath: string, - monoRepoRootPath: string = getRootPath() + rootPath: string ): void => { - const rushConfigFile: string = `${monoRepoRootPath}/${RushConstants.rushJsonFilename}`; - const rushConfig: IRushConfigurationJson = loadRushConfiguration(monoRepoRootPath); + const rushConfigFile: string = `${rootPath}/${RushConstants.rushJsonFilename}`; + const rushConfig: IRushConfigurationJson = loadRushConfiguration(rootPath); const projectIndex: number = rushConfig.projects.findIndex( ({ packageName }) => packageName === sourceProject.packageName ); let newTargetProject: IRushConfigurationProjectJson = { subspaceName: targetSubspace, packageName: sourceProject.packageName, - projectFolder: path.relative(monoRepoRootPath, targetProjectFolderPath), + projectFolder: path.relative(rootPath, targetProjectFolderPath), decoupledLocalDependencies: [] }; diff --git a/rush-plugins/rush-migrate-subspace-plugin/src/functions/updateSubspace.ts b/rush-plugins/rush-migrate-subspace-plugin/src/functions/updateSubspace.ts index d879c43..338ab10 100644 --- a/rush-plugins/rush-migrate-subspace-plugin/src/functions/updateSubspace.ts +++ b/rush-plugins/rush-migrate-subspace-plugin/src/functions/updateSubspace.ts @@ -12,10 +12,14 @@ import { const mergeSubspaceCommonVersionsFiles = ( targetSubspaceName: string, - sourceSubspaceFolderPath: string + sourceSubspaceFolderPath: string, + rootPath: string ): void => { const sourceCommonVersionsFilePath: string = `${sourceSubspaceFolderPath}/${RushConstants.commonVersionsFilename}`; - const targetCommonVersionsFilePath: string = getRushSubspaceCommonVersionsFilePath(targetSubspaceName); + const targetCommonVersionsFilePath: string = getRushSubspaceCommonVersionsFilePath( + targetSubspaceName, + rootPath + ); const sourceCommonVersions: RushSubspaceCommonVersionsJson = JsonFile.load(sourceCommonVersionsFilePath); if (FileSystem.exists(targetCommonVersionsFilePath)) { @@ -97,15 +101,20 @@ const copySubspaceRepoStateFile = ( }; */ -const mergeSubspacePnpmFiles = (targetSubspaceName: string, sourceSubspaceFolderPath: string): void => { +const mergeSubspacePnpmFiles = ( + targetSubspaceName: string, + sourceSubspaceFolderPath: string, + rootPath: string +): void => { const sourcePnpmFilePath: string = `${sourceSubspaceFolderPath}/${RushNameConstants.PnpmSubspaceFileName}`; - const targetPnpmFilePath: string = getRushSubspacePnpmFilePath(targetSubspaceName); + const targetPnpmFilePath: string = getRushSubspacePnpmFilePath(targetSubspaceName, rootPath); if (FileSystem.exists(sourcePnpmFilePath)) { if (FileSystem.exists(targetPnpmFilePath)) { const tempPnpmSubspaceFileName: string = `.pnpmfile-subspace-temp_${new Date().getTime()}.cjs`; const targetPnpmTempFilePath: string = `${getRushSubspaceConfigurationFolderPath( - targetSubspaceName + targetSubspaceName, + rootPath )}/${tempPnpmSubspaceFileName}`; Console.debug( `Duplicating temporary ${Colorize.bold(sourcePnpmFilePath)} into ${Colorize.bold( @@ -139,9 +148,13 @@ const mergeSubspacePnpmFiles = (targetSubspaceName: string, sourceSubspaceFolder } }; -const copySubspaceNpmRcFile = (targetSubspaceName: string, sourceSubspaceFolderPath: string): void => { +const copySubspaceNpmRcFile = ( + targetSubspaceName: string, + sourceSubspaceFolderPath: string, + rootPath: string +): void => { const sourceNpmRcFilePath: string = `${sourceSubspaceFolderPath}/${RushNameConstants.NpmRcFileName}`; - const targetNpmRcFilePath: string = getRushSubspaceNpmRcFilePath(targetSubspaceName); + const targetNpmRcFilePath: string = getRushSubspaceNpmRcFilePath(targetSubspaceName, rootPath); if (FileSystem.exists(sourceNpmRcFilePath)) { Console.debug( @@ -156,10 +169,11 @@ const copySubspaceNpmRcFile = (targetSubspaceName: string, sourceSubspaceFolderP export const updateSubspace = async ( targetSubspaceName: string, - sourceSubspaceConfigurationFolderPath: string + sourceSubspaceConfigurationFolderPath: string, + rootPath: string ): Promise => { - copySubspaceNpmRcFile(targetSubspaceName, sourceSubspaceConfigurationFolderPath); - mergeSubspaceCommonVersionsFiles(targetSubspaceName, sourceSubspaceConfigurationFolderPath); + copySubspaceNpmRcFile(targetSubspaceName, sourceSubspaceConfigurationFolderPath, rootPath); + mergeSubspaceCommonVersionsFiles(targetSubspaceName, sourceSubspaceConfigurationFolderPath, rootPath); // copySubspaceRepoStateFile(sourceSubspaceConfigurationFolderPath, targetSubspaceConfigurationFolderPath); - mergeSubspacePnpmFiles(targetSubspaceName, sourceSubspaceConfigurationFolderPath); + mergeSubspacePnpmFiles(targetSubspaceName, sourceSubspaceConfigurationFolderPath, rootPath); }; diff --git a/rush-plugins/rush-migrate-subspace-plugin/src/interactMenu.ts b/rush-plugins/rush-migrate-subspace-plugin/src/interactMenu.ts index c2fad17..89d301a 100644 --- a/rush-plugins/rush-migrate-subspace-plugin/src/interactMenu.ts +++ b/rush-plugins/rush-migrate-subspace-plugin/src/interactMenu.ts @@ -1,9 +1,10 @@ +import { cleanSubspace } from './cleanSubspace'; import { migrateProject } from './migrateProject'; import { chooseCommandPrompt } from './prompts/command'; import Console from './providers/console'; import { syncVersions } from './syncVersions'; -export const interactMenu = async (): Promise => { +export const interactMenu = async (sourceMonorepoPath: string, targetMonorepoPath: string): Promise => { let exitApplication: boolean = false; do { const nextCommand: string = await chooseCommandPrompt(); @@ -13,12 +14,17 @@ export const interactMenu = async (): Promise => { break; case 'move': - await migrateProject(); + await migrateProject(sourceMonorepoPath, targetMonorepoPath); Console.newLine(); break; case 'sync': - await syncVersions(); + await syncVersions(targetMonorepoPath); + Console.newLine(); + break; + + case 'clean': + await cleanSubspace(targetMonorepoPath); Console.newLine(); break; } diff --git a/rush-plugins/rush-migrate-subspace-plugin/src/migrateProject.ts b/rush-plugins/rush-migrate-subspace-plugin/src/migrateProject.ts index 4c39fa6..d0bf9b1 100644 --- a/rush-plugins/rush-migrate-subspace-plugin/src/migrateProject.ts +++ b/rush-plugins/rush-migrate-subspace-plugin/src/migrateProject.ts @@ -2,7 +2,6 @@ import { chooseSubspacePrompt } from './prompts/subspace'; import { addProjectToSubspace } from './functions/addProjectToSubspace'; import { chooseProjectPrompt, confirmNextProjectToAddPrompt } from './prompts/project'; import Console from './providers/console'; -import { getRootPath } from './utilities/path'; import { Colorize } from '@rushstack/terminal'; import { updateSubspace } from './functions/updateSubspace'; import { getRushSubspaceConfigurationFolderPath, isSubspaceSupported } from './utilities/subspace'; @@ -16,36 +15,39 @@ import { import { RushConstants } from '@rushstack/rush-sdk'; import { syncProjectMismatchedDependencies } from './functions/syncProjectDependencies'; -export const migrateProject = async (): Promise => { +export const migrateProject = async ( + sourceMonorepoPath: string, + targetMonorepoPath: string +): Promise => { Console.debug('Executing project migration command...'); - Console.title(`🔍 Analyzing if monorepo ${Colorize.underline(getRootPath())} supports subspaces...`); + Console.title(`🔍 Analyzing if monorepo ${Colorize.underline(targetMonorepoPath)} supports subspaces...`); /** * WARN: Disabling auto subspace initialization for now. * if (!isSubspaceSupported()) { * Console.warn( - * `The monorepo ${Colorize.bold(getRootPath())} doesn't contain subspaces. Initializing subspaces...` + * `The monorepo ${Colorize.bold(rootPath)} doesn't contain subspaces. Initializing subspaces...` *); * await initSubspaces(); * } */ - const targetSubspaces: string[] = querySubspaces(); - if (!isSubspaceSupported() || targetSubspaces.length === 0) { + const targetSubspaces: string[] = querySubspaces(targetMonorepoPath); + if (!isSubspaceSupported(targetMonorepoPath) || targetSubspaces.length === 0) { Console.error( `The monorepo ${Colorize.bold( - getRootPath() + targetMonorepoPath )} doesn't support subspaces! Make sure you have ${Colorize.bold( - getRushSubspacesConfigurationJsonPath() + getRushSubspacesConfigurationJsonPath(targetMonorepoPath) )} with the ${Colorize.bold(RushConstants.defaultSubspaceName)} subspace. Exiting...` ); return; } - Console.success(`Monorepo ${Colorize.bold(getRootPath())} fully supports subspaces!`); + Console.success(`Monorepo ${Colorize.bold(targetMonorepoPath)} fully supports subspaces!`); - Console.title(`🔍 Finding projects to migrate to ${Colorize.bold(getRootPath())}...`); + Console.title(`🔍 Finding projects to migrate to ${Colorize.bold(targetMonorepoPath)}...`); /** * WARN: Disabling different repository selection for now. @@ -55,8 +57,6 @@ export const migrateProject = async (): Promise => { * ); */ - const sourceMonorepoPath: string = getRootPath(); - /** * WARN: Disabling creating new subspaces for now. * const subspaceSelectionType: string = await chooseCreateOrSelectSubspacePrompt(targetSubspaces); @@ -76,7 +76,10 @@ export const migrateProject = async (): Promise => { // Loop until user asks to quit do { const sourceProjects: IRushConfigurationProjectJson[] = queryProjects(sourceMonorepoPath); - const sourceAvailableProjects: IRushConfigurationProjectJson[] = isExternalMonorepo(sourceMonorepoPath) + const sourceAvailableProjects: IRushConfigurationProjectJson[] = isExternalMonorepo( + sourceMonorepoPath, + targetMonorepoPath + ) ? sourceProjects : sourceProjects.filter(({ subspaceName }) => subspaceName !== targetSubspace); @@ -108,11 +111,16 @@ export const migrateProject = async (): Promise => { sourceProjectToMigrate.projectFolder ); - await updateSubspace(targetSubspace, sourceSubspaceConfigurationFolderPath); + await updateSubspace(targetSubspace, sourceSubspaceConfigurationFolderPath, targetMonorepoPath); } - await addProjectToSubspace(sourceProjectToMigrate, targetSubspace, sourceMonorepoPath); - await syncProjectMismatchedDependencies(sourceProjectToMigrate.packageName); + await addProjectToSubspace( + sourceProjectToMigrate, + targetSubspace, + sourceMonorepoPath, + targetMonorepoPath + ); + await syncProjectMismatchedDependencies(sourceProjectToMigrate.packageName, targetMonorepoPath); } while (await confirmNextProjectToAddPrompt(targetSubspace)); Console.warn( diff --git a/rush-plugins/rush-migrate-subspace-plugin/src/prompts/command.ts b/rush-plugins/rush-migrate-subspace-plugin/src/prompts/command.ts index 4c1f6c5..ee43e74 100644 --- a/rush-plugins/rush-migrate-subspace-plugin/src/prompts/command.ts +++ b/rush-plugins/rush-migrate-subspace-plugin/src/prompts/command.ts @@ -9,6 +9,7 @@ export const chooseCommandPrompt = async (): Promise => { choices: [ { name: 'Move a project to a new subspace', value: 'move' }, { name: 'Scan & fix version mismatches', value: 'sync' }, + { name: 'Clean common subspace versions', value: 'clean' }, { name: 'Exit application', value: 'exit' } ] } diff --git a/rush-plugins/rush-migrate-subspace-plugin/src/prompts/dependency.ts b/rush-plugins/rush-migrate-subspace-plugin/src/prompts/dependency.ts index 98703c0..457283e 100644 --- a/rush-plugins/rush-migrate-subspace-plugin/src/prompts/dependency.ts +++ b/rush-plugins/rush-migrate-subspace-plugin/src/prompts/dependency.ts @@ -2,11 +2,11 @@ import { Colorize } from '@rushstack/terminal'; import inquirer from 'inquirer'; import { sortVersions } from '../utilities/dependency'; -export const confirmNextDependencyPrompt = async (projectName: string): Promise => { +export const confirmNextDependencyPrompt = async (suffix?: string): Promise => { const { confirmNext } = await inquirer.prompt([ { - message: `Do you want to fix another dependency?`, - suffix: ` (Current project: ${Colorize.bold(projectName)})`, + message: `Do you want to choose another dependency?`, + suffix, type: 'confirm', name: 'confirmNext', default: true @@ -79,13 +79,13 @@ export const enterVersionPrompt = async (dependencyName: string): Promise => { +export const chooseDependencyPrompt = async (dependencies: string[], suffix?: string): Promise => { const { dependencyInput } = await inquirer.prompt([ { type: 'list', name: 'dependencyInput', - message: `Please enter the dependency you wish to fix the mismatch.`, - suffix: ` (${Colorize.bold(`${dependencies.length}`)} mismatched dependencies)`, + message: `Please select the dependency name (Type to filter).`, + suffix, choices: dependencies.sort().map((name) => ({ name, value: name })) } diff --git a/rush-plugins/rush-migrate-subspace-plugin/src/prompts/subspace.ts b/rush-plugins/rush-migrate-subspace-plugin/src/prompts/subspace.ts index f590844..bfd34cf 100644 --- a/rush-plugins/rush-migrate-subspace-plugin/src/prompts/subspace.ts +++ b/rush-plugins/rush-migrate-subspace-plugin/src/prompts/subspace.ts @@ -56,3 +56,50 @@ export const chooseCreateOrSelectSubspacePrompt = async (subspaces: string[]): P return subspaceSelection; }; + +export const scanForUnusedDependencyVersionsPrompt = async (): Promise => { + const { removeUnused } = await inquirer.prompt([ + { + message: 'Do you want to remove unused alternative versions from the subspace?', + name: 'removeUnused', + type: 'confirm' + } + ]); + + return removeUnused; +}; + +export const scanForDuplicatedDependenciesPrompt = async (): Promise => { + const { scanForDuplicated } = await inquirer.prompt([ + { + message: 'Do you want to remove duplicated dependencies in the subspace?', + name: 'scanForDuplicated', + type: 'confirm' + } + ]); + + return scanForDuplicated; +}; + +export const scanForSupersetDependencyVersionsPrompt = async (): Promise => { + const { scanForSuperset } = await inquirer.prompt([ + { + message: '(EXPERIMENTAL) Do you want to remove superset dependency versions from the subspace?', + name: 'scanForSuperset', + type: 'confirm' + } + ]); + return scanForSuperset; +}; + +export const scanForAllDependenciesPrompt = async (): Promise => { + const { executeForAll } = await inquirer.prompt([ + { + message: `Do you want to scan for all dependencies?`, + type: 'confirm', + name: 'executeForAll' + } + ]); + + return executeForAll; +}; diff --git a/rush-plugins/rush-migrate-subspace-plugin/src/syncVersions.ts b/rush-plugins/rush-migrate-subspace-plugin/src/syncVersions.ts index ab6c46e..f831a3c 100644 --- a/rush-plugins/rush-migrate-subspace-plugin/src/syncVersions.ts +++ b/rush-plugins/rush-migrate-subspace-plugin/src/syncVersions.ts @@ -3,17 +3,16 @@ import { VersionMismatchFinderEntity } from '@rushstack/rush-sdk/lib/logic/versi import Console from './providers/console'; import { Colorize } from '@rushstack/terminal'; import { querySubspaces } from './utilities/repository'; -import { getRootPath } from './utilities/path'; import { getSubspaceMismatches } from './utilities/subspace'; import { chooseProjectPrompt, confirmNextProjectToSyncPrompt } from './prompts/project'; import { syncProjectMismatchedDependencies } from './functions/syncProjectDependencies'; -const fetchSubspaceMismatchedProjects = (subspaceName: string): string[] => { +const fetchSubspaceMismatchedProjects = (subspaceName: string, rootPath: string): string[] => { const mismatchedProjects: string[] = []; const subspaceMismatches: ReadonlyMap< string, ReadonlyMap - > = getSubspaceMismatches(subspaceName); + > = getSubspaceMismatches(subspaceName, rootPath); for (const [, versions] of subspaceMismatches) { for (const [, entities] of versions) { @@ -32,19 +31,19 @@ const fetchSubspaceMismatchedProjects = (subspaceName: string): string[] => { return mismatchedProjects; }; -export const syncVersions = async (): Promise => { +export const syncVersions = async (rootPath: string): Promise => { Console.debug('Synching project version...'); - const sourceSubspaces: string[] = querySubspaces(); + const sourceSubspaces: string[] = querySubspaces(rootPath); if (sourceSubspaces.length === 0) { - Console.error(`No subspaces found in the monorepo ${Colorize.bold(getRootPath())}! Exiting...`); + Console.error(`No subspaces found in the monorepo ${Colorize.bold(rootPath)}! Exiting...`); return; } const selectedSubspaceName: string = await chooseSubspacePrompt(sourceSubspaces); Console.title(`🔄 Syncing version mismatches for subspace ${Colorize.bold(selectedSubspaceName)}...`); - let mismatchedProjects: string[] = fetchSubspaceMismatchedProjects(selectedSubspaceName); + let mismatchedProjects: string[] = fetchSubspaceMismatchedProjects(selectedSubspaceName, rootPath); if (mismatchedProjects.length === 0) { Console.success(`No mismatches found in the subspace ${Colorize.bold(selectedSubspaceName)}! Exiting...`); return; @@ -52,11 +51,11 @@ export const syncVersions = async (): Promise => { do { const selectedProjectName: string = await chooseProjectPrompt(mismatchedProjects); - if (!(await syncProjectMismatchedDependencies(selectedProjectName))) { + if (!(await syncProjectMismatchedDependencies(selectedProjectName, rootPath))) { return; } - mismatchedProjects = fetchSubspaceMismatchedProjects(selectedSubspaceName); + mismatchedProjects = fetchSubspaceMismatchedProjects(selectedSubspaceName, rootPath); } while (mismatchedProjects.length > 0 && (await confirmNextProjectToSyncPrompt(selectedSubspaceName))); if (mismatchedProjects.length === 0) { diff --git a/rush-plugins/rush-migrate-subspace-plugin/src/utilities/dependency.ts b/rush-plugins/rush-migrate-subspace-plugin/src/utilities/dependency.ts index b56793c..577341c 100644 --- a/rush-plugins/rush-migrate-subspace-plugin/src/utilities/dependency.ts +++ b/rush-plugins/rush-migrate-subspace-plugin/src/utilities/dependency.ts @@ -1,9 +1,8 @@ import { compare, eq, intersects, minVersion, satisfies, subset, valid, validRange } from 'semver'; +import { RESERVED_VERSIONS } from '../constants/versions'; export const subsetVersion = (version1: string, version2: string): boolean => { - if (version1 === version2) { - return true; - } else if (['latest', 'workspace:*'].includes(version2)) { + if (RESERVED_VERSIONS.includes(version2)) { return true; } else if ((!valid(version1) && !validRange(version1)) || (!valid(version2) && !validRange(version2))) { return false; @@ -26,10 +25,10 @@ export const sortVersions = (versions: string[]): string[] => { if (v1 === v2) { // e.g. 1.0.0 , 1.0.0 return 0; - } else if (['latest', 'workspace:*'].includes(v1)) { + } else if (RESERVED_VERSIONS.includes(v1)) { // e.g. workspace:*, 1.0.0 return 1; - } else if (['latest', 'workspace:*'].includes(v2)) { + } else if (RESERVED_VERSIONS.includes(v2)) { // e.g. 1.0.0, workspace:* return -1; } else if (!valid(v1) && !validRange(v1)) { @@ -72,13 +71,13 @@ export const sortVersions = (versions: string[]): string[] => { return newVersions; }; -export const rSortVersions = (versions: string[]): string[] => { +export const reverseSortVersions = (versions: string[]): string[] => { return sortVersions(versions).reverse(); }; export const getClosestVersion = (targetVersion: string, versions: string[]): string | undefined => { - const rSortedVersions: string[] = rSortVersions(versions); - return rSortedVersions.find((version) => intersectsVersion(targetVersion, version)); + const reverseSortedVersions: string[] = reverseSortVersions(versions); + return reverseSortedVersions.find((version) => intersectsVersion(targetVersion, version)); }; export const getRecommendedVersion = (targetVersion: string, versions: string[]): string | undefined => { diff --git a/rush-plugins/rush-migrate-subspace-plugin/src/utilities/project.ts b/rush-plugins/rush-migrate-subspace-plugin/src/utilities/project.ts index 42d7bc2..da10fe7 100644 --- a/rush-plugins/rush-migrate-subspace-plugin/src/utilities/project.ts +++ b/rush-plugins/rush-migrate-subspace-plugin/src/utilities/project.ts @@ -1,41 +1,32 @@ import { IRushConfigurationJson } from '@rushstack/rush-sdk/lib/api/RushConfiguration'; import { IRushConfigurationProjectJson } from '@rushstack/rush-sdk/lib/api/RushConfigurationProject'; -import { getRootPath } from './path'; import { loadRushConfiguration } from './repository'; import { getSubspaceMismatches } from './subspace'; import { VersionMismatchFinderEntity } from '@rushstack/rush-sdk/lib/logic/versionMismatch/VersionMismatchFinderEntity'; import { RushNameConstants } from '../constants/paths'; import { IPackageJson, IPackageJsonDependencyTable, JsonFile } from '@rushstack/node-core-library'; -export const getProjectPackageFilePath = ( - projectFolder: string, - rootPath: string = getRootPath() -): string => { +export const getProjectPackageFilePath = (projectFolder: string, rootPath: string): string => { return `${rootPath}/${projectFolder}/${RushNameConstants.PackageName}`; }; -export const loadProjectPackageJson = ( - projectFolder: string, - rootPath: string = getRootPath() -): IPackageJson => { +export const loadProjectPackageJson = (projectFolder: string, rootPath: string): IPackageJson => { return JsonFile.load(getProjectPackageFilePath(projectFolder, rootPath)); }; -export const queryProjects = (rootPath: string = getRootPath()): IRushConfigurationProjectJson[] => { +export const queryProjects = (rootPath: string): IRushConfigurationProjectJson[] => { const rushJson: IRushConfigurationJson = loadRushConfiguration(rootPath); return rushJson.projects; }; -export const queryProjectsWithoutSubspace = ( - rootPath: string = getRootPath() -): IRushConfigurationProjectJson[] => { +export const queryProjectsWithoutSubspace = (rootPath: string): IRushConfigurationProjectJson[] => { const projects: IRushConfigurationProjectJson[] = queryProjects(rootPath); return projects.filter(({ subspaceName }) => !subspaceName); }; export const queryProject = ( projectName: string, - rootPath: string = getRootPath() + rootPath: string ): IRushConfigurationProjectJson | undefined => { const projects: IRushConfigurationProjectJson[] = queryProjects(rootPath); return projects.find(({ packageName }) => packageName === projectName); @@ -43,7 +34,7 @@ export const queryProject = ( export const getProjectMismatches = ( projectName: string, - rootPath: string = getRootPath() + rootPath: string ): ReadonlyMap> => { const project: IRushConfigurationProjectJson | undefined = queryProject(projectName, rootPath); if (!project || !project.subspaceName) { @@ -76,7 +67,7 @@ export const getProjectMismatches = ( export const getProjectDependencies = ( projectName: string, - rootPath: string = getRootPath() + rootPath: string ): IPackageJsonDependencyTable | undefined => { const project: IRushConfigurationProjectJson | undefined = queryProject(projectName, rootPath); if (!project || !project.subspaceName) { @@ -90,11 +81,37 @@ export const getProjectDependencies = ( }; }; -export const getProjectMismatchedDependencies = (projectName: string): string[] => { +export const getProjectMismatchedDependencies = (projectName: string, rootPath: string): string[] => { const projectMismatches: ReadonlyMap< string, ReadonlyMap - > = getProjectMismatches(projectName); + > = getProjectMismatches(projectName, rootPath); return Array.from(projectMismatches.keys()); }; + +export const updateProjectDependency = ( + projectName: string, + dependencyName: string, + newVersion: string, + rootPath: string +): boolean => { + const project: IRushConfigurationProjectJson | undefined = queryProject(projectName, rootPath); + if (!project) { + return false; + } + + const projectPackageFilePath: string = getProjectPackageFilePath(project.projectFolder, rootPath); + const projectPackageJson: IPackageJson = loadProjectPackageJson(project.projectFolder, rootPath); + + if (projectPackageJson.dependencies?.[dependencyName]) { + projectPackageJson.dependencies[dependencyName] = newVersion; + } + + if (projectPackageJson.devDependencies?.[dependencyName]) { + projectPackageJson.devDependencies[dependencyName] = newVersion; + } + + JsonFile.save(projectPackageJson, projectPackageFilePath); + return true; +}; diff --git a/rush-plugins/rush-migrate-subspace-plugin/src/utilities/repository.ts b/rush-plugins/rush-migrate-subspace-plugin/src/utilities/repository.ts index 428222d..7dee644 100644 --- a/rush-plugins/rush-migrate-subspace-plugin/src/utilities/repository.ts +++ b/rush-plugins/rush-migrate-subspace-plugin/src/utilities/repository.ts @@ -1,33 +1,27 @@ import { JsonFile } from '@rushstack/node-core-library'; -import { getRootPath } from './path'; import { IRushConfigurationJson } from '@rushstack/rush-sdk/lib/api/RushConfiguration'; import { RushPathConstants } from '../constants/paths'; import { ISubspacesConfigurationJson } from '@rushstack/rush-sdk/lib/api/SubspacesConfiguration'; import { RushConstants } from '@rushstack/rush-sdk'; -export const getRushConfigurationJsonPath = (rootPath: string = getRootPath()): string => +export const getRushConfigurationJsonPath = (rootPath: string): string => `${rootPath}/${RushConstants.rushJsonFilename}`; -export const getRushSubspacesConfigurationJsonPath = (rootPath: string = getRootPath()): string => +export const getRushSubspacesConfigurationJsonPath = (rootPath: string): string => `${rootPath}/${RushPathConstants.SubspacesConfigurationFilePath}`; -export const loadRushConfiguration = (rootPath: string = getRootPath()): IRushConfigurationJson => { +export const loadRushConfiguration = (rootPath: string): IRushConfigurationJson => { return JsonFile.load(getRushConfigurationJsonPath(rootPath)); }; -export const loadRushSubspacesConfiguration = ( - rootPath: string = getRootPath() -): ISubspacesConfigurationJson => { +export const loadRushSubspacesConfiguration = (rootPath: string): ISubspacesConfigurationJson => { return JsonFile.load(getRushSubspacesConfigurationJsonPath(rootPath)); }; -export const querySubspaces = (rootPath: string = getRootPath()): string[] => { +export const querySubspaces = (rootPath: string): string[] => { const subspaceJson: ISubspacesConfigurationJson = loadRushSubspacesConfiguration(rootPath); return subspaceJson.subspaceNames; }; -export const isExternalMonorepo = ( - sourceRootPath: string, - targetRootPath: string = getRootPath() -): boolean => { +export const isExternalMonorepo = (sourceRootPath: string, targetRootPath: string): boolean => { return sourceRootPath !== targetRootPath; }; diff --git a/rush-plugins/rush-migrate-subspace-plugin/src/utilities/subspace.ts b/rush-plugins/rush-migrate-subspace-plugin/src/utilities/subspace.ts index 860f965..4d615ad 100644 --- a/rush-plugins/rush-migrate-subspace-plugin/src/utilities/subspace.ts +++ b/rush-plugins/rush-migrate-subspace-plugin/src/utilities/subspace.ts @@ -1,7 +1,6 @@ import { FileSystem, IPackageJsonDependencyTable, JsonFile } from '@rushstack/node-core-library'; import { RushNameConstants, RushPathConstants } from '../constants/paths'; import { ISubspacesConfigurationJson } from '@rushstack/rush-sdk/lib/api/SubspacesConfiguration'; -import { getRootPath } from './path'; import { getRushConfigurationJsonPath, getRushSubspacesConfigurationJsonPath, @@ -15,11 +14,10 @@ import { VersionMismatchFinder } from '@rushstack/rush-sdk/lib/logic/versionMism import { VersionMismatchFinderEntity } from '@rushstack/rush-sdk/lib/logic/versionMismatch/VersionMismatchFinderEntity'; import { IRushConfigurationProjectJson } from '@rushstack/rush-sdk/lib/api/RushConfigurationProject'; import { getProjectDependencies } from './project'; -import { sortVersions, subsetVersion } from './dependency'; export const queryProjectsFromSubspace = ( targetSubspaceName: string, - rootPath: string = getRootPath() + rootPath: string ): IRushConfigurationProjectJson[] => { const rushConfig: IRushConfigurationJson = loadRushConfiguration(rootPath); return rushConfig.projects.filter(({ subspaceName }) => subspaceName === targetSubspaceName); @@ -28,14 +26,14 @@ export const queryProjectsFromSubspace = ( const getRushLegacySubspaceConfigurationFolderPath = ( subspaceName: string, projectFolderPath: string, - rootPath: string = getRootPath() + rootPath: string ): string => { return `${rootPath}/${projectFolderPath}/subspace/${subspaceName}`; }; export const getRushSubspaceConfigurationFolderPath = ( subspaceName: string, - rootPath: string = getRootPath(), + rootPath: string, projectFolderPath?: string ): string => { if (projectFolderPath) { @@ -53,10 +51,7 @@ export const getRushSubspaceConfigurationFolderPath = ( return `${rootPath}/${RushPathConstants.SubspacesConfigurationFolderPath}/${subspaceName}`; }; -export const getRushSubspaceCommonVersionsFilePath = ( - subspaceName: string, - rootPath: string = getRootPath() -): string => { +export const getRushSubspaceCommonVersionsFilePath = (subspaceName: string, rootPath: string): string => { return `${getRushSubspaceConfigurationFolderPath(subspaceName, rootPath)}/${ RushConstants.commonVersionsFilename }`; @@ -64,15 +59,12 @@ export const getRushSubspaceCommonVersionsFilePath = ( export const loadRushSubspaceCommonVersions = ( subspaceName: string, - rootPath: string = getRootPath() + rootPath: string ): RushSubspaceCommonVersionsJson => { return JsonFile.load(getRushSubspaceCommonVersionsFilePath(subspaceName, rootPath)); }; -export const getRushSubspacePnpmFilePath = ( - subspaceName: string, - rootPath: string = getRootPath() -): string => { +export const getRushSubspacePnpmFilePath = (subspaceName: string, rootPath: string): string => { return `${getRushSubspaceConfigurationFolderPath(subspaceName, rootPath)}/${ RushNameConstants.PnpmSubspaceFileName }`; @@ -80,15 +72,12 @@ export const getRushSubspacePnpmFilePath = ( export const loadRushSubspacePnpm = ( subspaceName: string, - rootPath: string = getRootPath() + rootPath: string ): RushSubspaceCommonVersionsJson => { return JsonFile.load(getRushSubspacePnpmFilePath(subspaceName, rootPath)); }; -export const getRushSubspaceNpmRcFilePath = ( - subspaceName: string, - rootPath: string = getRootPath() -): string => { +export const getRushSubspaceNpmRcFilePath = (subspaceName: string, rootPath: string): string => { return `${getRushSubspaceConfigurationFolderPath(subspaceName, rootPath)}/${ RushNameConstants.NpmRcFileName }`; @@ -96,31 +85,31 @@ export const getRushSubspaceNpmRcFilePath = ( export const loadRushSubspaceNpmRc = ( subspaceName: string, - rootPath: string = getRootPath() + rootPath: string ): RushSubspaceCommonVersionsJson => { return JsonFile.load(getRushSubspaceNpmRcFilePath(subspaceName, rootPath)); }; -export const isSubspaceSupported = (rootPath: string = getRootPath()): boolean => { +export const isSubspaceSupported = (rootPath: string): boolean => { if ( FileSystem.exists(getRushSubspacesConfigurationJsonPath(rootPath)) && FileSystem.exists(getRushSubspaceConfigurationFolderPath(RushConstants.defaultSubspaceName, rootPath)) ) { - const subspacesConfig: ISubspacesConfigurationJson = loadRushSubspacesConfiguration(); + const subspacesConfig: ISubspacesConfigurationJson = loadRushSubspacesConfiguration(rootPath); return subspacesConfig.subspacesEnabled; } return false; }; -export const subspaceExists = (subspaceName: string, rootPath: string = getRootPath()): boolean => { +export const subspaceExists = (subspaceName: string, rootPath: string): boolean => { const subspaces: string[] = querySubspaces(rootPath); return !!subspaces.find((name) => name === subspaceName); }; export const getSubspaceMismatches = ( subspaceName: string, - rootPath: string = getRootPath() + rootPath: string ): ReadonlyMap> => { const rushConfig: RushConfiguration = RushConfiguration.loadFromConfigurationFile( getRushConfigurationJsonPath(rootPath) @@ -136,7 +125,7 @@ export const getSubspaceMismatches = ( export const getSubspaceDependencies = ( subspaceName: string, - rootPath: string = getRootPath() + rootPath: string ): Map> => { const subspaceProjects: IRushConfigurationProjectJson[] = queryProjectsFromSubspace(subspaceName, rootPath); const subspaceDependencies: Map> = new Map>(); @@ -160,60 +149,3 @@ export const getSubspaceDependencies = ( return subspaceDependencies; }; - -const reduceSubspaceDependencyVersions = (versions: string[]): string[] => { - const validVersions: string[] = sortVersions(versions); - - let targetIndex: number = 0; - while (targetIndex < validVersions.length) { - const targetVersion: string = validVersions[targetIndex]; - const toCompareVersions: string[] = validVersions.slice(targetIndex + 1); - - const toDeleteIndex: number = toCompareVersions.findIndex((toCompareVersion) => - subsetVersion(toCompareVersion, targetVersion) - ); - - if (toDeleteIndex > -1) { - validVersions.splice(targetIndex + 1 + toDeleteIndex, 1); - } else { - targetIndex += 1; - } - } - - return validVersions; -}; - -export const cleanSubspaceCommonVersions = ( - subspaceName: string, - rootPath: string = getRootPath() -): boolean => { - let hasChanged: boolean = false; - const subspaceCommonVersionsPath: string = getRushSubspaceCommonVersionsFilePath(subspaceName, rootPath); - const subspaceCommonVersionsJson: RushSubspaceCommonVersionsJson = loadRushSubspaceCommonVersions( - subspaceName, - rootPath - ); - - subspaceCommonVersionsJson.allowedAlternativeVersions = - subspaceCommonVersionsJson.allowedAlternativeVersions || {}; - for (const [dependency, versions] of Object.entries( - subspaceCommonVersionsJson.allowedAlternativeVersions - )) { - // Remove duplicates & unnecessary versions - const validVersions: string[] = reduceSubspaceDependencyVersions(versions); - if (validVersions.length === 0) { - delete subspaceCommonVersionsJson.allowedAlternativeVersions[dependency]; - } else { - subspaceCommonVersionsJson.allowedAlternativeVersions[dependency] = validVersions; - } - - hasChanged = hasChanged || validVersions.length !== versions.length; - } - - if (hasChanged) { - JsonFile.save(subspaceCommonVersionsJson, subspaceCommonVersionsPath); - return true; - } - - return false; -};