From 0f4ae68e95344812c9633f81ee8404b082e93c69 Mon Sep 17 00:00:00 2001 From: Nick Pape Date: Fri, 21 Sep 2018 16:41:27 -0700 Subject: [PATCH 01/27] introduce the new command - incomplete --- apps/rush-lib/src/api/RushConfiguration.ts | 10 ++ apps/rush-lib/src/cli/actions/AddAction.ts | 115 +++++++++++++++++++++ 2 files changed, 125 insertions(+) create mode 100644 apps/rush-lib/src/cli/actions/AddAction.ts diff --git a/apps/rush-lib/src/api/RushConfiguration.ts b/apps/rush-lib/src/api/RushConfiguration.ts index 71cc76c9172..25d9e7c0742 100644 --- a/apps/rush-lib/src/api/RushConfiguration.ts +++ b/apps/rush-lib/src/api/RushConfiguration.ts @@ -743,6 +743,16 @@ export class RushConfiguration { return this._versionPolicyConfiguration; } + public getCurrentProjectFromPath(currentFolderPath: string): RushConfigurationProject | undefined { + const resolvedPath: string = path.resolve(currentFolderPath); + for (const project of this.projects) { + if (path.relative(project.projectFolder, resolvedPath).indexOf('..') !== 0) { + return project; + } + } + return undefined; + } + /** * Use RushConfiguration.loadFromConfigurationFile() or Use RushConfiguration.loadFromDefaultLocation() * instead. diff --git a/apps/rush-lib/src/cli/actions/AddAction.ts b/apps/rush-lib/src/cli/actions/AddAction.ts new file mode 100644 index 00000000000..0819d12da78 --- /dev/null +++ b/apps/rush-lib/src/cli/actions/AddAction.ts @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import * as os from 'os'; +import * as path from 'path'; +import * as child_process from 'child_process'; +import * as colors from 'colors'; + +import inquirer = require('inquirer'); + +import { + CommandLineFlagParameter, + CommandLineStringParameter +} from '@microsoft/ts-command-line'; +import { FileSystem } from '@microsoft/node-core-library'; + +import { RushConfigurationProject } from '../../api/RushConfigurationProject'; +import { + IChangeFile, + IChangeInfo +} from '../../api/ChangeManagement'; +import { VersionControl } from '../../utilities/VersionControl'; +import { ChangeFile } from '../../api/ChangeFile'; +import { BaseRushAction } from './BaseRushAction'; +import { RushCommandLineParser } from '../RushCommandLineParser'; +import { ChangeFiles } from '../../logic/ChangeFiles'; +import { + VersionPolicy, + IndividualVersionPolicy, + LockStepVersionPolicy, + VersionPolicyDefinitionName +} from '../../api/VersionPolicy'; + +export class ChangeAction extends BaseRushAction { + private _exactFlag: CommandLineFlagParameter; + private _caretFlag: CommandLineFlagParameter; + private _devDependencyFlag: CommandLineFlagParameter; + private _makeConsistentFlag: CommandLineFlagParameter; + private _noInstallFlag: CommandLineFlagParameter; + private _packageName: CommandLineStringParameter; + private _versionSpecifier: CommandLineStringParameter; + + constructor(parser: RushCommandLineParser) { + const documentation: string[] = [ + 'Blah.' + ]; + super({ + actionName: 'add', + summary: 'Adds a dependency to the package.json and runs rush upgrade.', + documentation: documentation.join(os.EOL), + safeForSimultaneousRushProcesses: false, + parser + }); + } + + public onDefineParameters(): void { + this._packageName = this.defineStringParameter({ + parameterLongName: '--package', + parameterShortName: '-p', + required: true, + argumentName: 'PACKAGE_NAME', + description: '(Required) The name of the package which should be added as a dependency' + }); + this._versionSpecifier = this.defineStringParameter({ + parameterLongName: '--version', + parameterShortName: '-v', + argumentName: 'VERSION_RANGE', + description: '' + }); + this._exactFlag = this.defineFlagParameter({ + parameterLongName: '--exact', + description: 'If specified, the version specifier inserted into the' + + ' package.json will be a locked, exact version.' + }); + this._caretFlag = this.defineFlagParameter({ + parameterLongName: '--caret', + description: 'If specified, the version specifier inserted into the' + + ' package.json will be a prepended with a "caret" specifier ("^").' + }); + this._devDependencyFlag = this.defineFlagParameter({ + parameterLongName: '--dev', + description: 'If specified, the package will be added as a "devDependency"' + + ' to the package.json' + }); + this._makeConsistentFlag = this.defineFlagParameter({ + parameterLongName: '--make-consistent', + parameterShortName: '-c', + description: 'If specified, other packages with this dependency will have their package.json' + + ' files updated to point at the specified depdendency' + }); + this._noInstallFlag = this.defineFlagParameter({ + parameterLongName: '--no-install', + parameterShortName: '-n', + description: 'If specified, the "rush update" command will not be run after updating the' + + ' package.json files.' + }); + } + + public run(): Promise { + const project: RushConfigurationProject | undefined + = this.rushConfiguration.getCurrentProjectFromPath(process.cwd()); + + if (!project) { + return Promise.reject(new Error('Not currently in a project folder')); + } + + + + + this.rushConfiguration.projects + + + return Promise.resolve(); + } +} From 72608617479314a7302024ab12b9f904beb15562 Mon Sep 17 00:00:00 2001 From: Nick Pape Date: Mon, 24 Sep 2018 13:38:45 -0700 Subject: [PATCH 02/27] API Change --- common/reviews/api/rush-lib.api.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/common/reviews/api/rush-lib.api.ts b/common/reviews/api/rush-lib.api.ts index 8df45ed5d77..f633219bf8a 100644 --- a/common/reviews/api/rush-lib.api.ts +++ b/common/reviews/api/rush-lib.api.ts @@ -156,6 +156,8 @@ class RushConfiguration { readonly eventHooks: EventHooks; findProjectByShorthandName(shorthandProjectName: string): RushConfigurationProject | undefined; findProjectByTempName(tempProjectName: string): RushConfigurationProject | undefined; + // (undocumented) + getCurrentProjectFromPath(currentFolderPath: string): RushConfigurationProject | undefined; getProjectByName(projectName: string): RushConfigurationProject | undefined; readonly gitAllowedEmailRegExps: string[]; readonly gitSampleEmail: string; From 60f2fa992b5fe9c80f429de4a287ebe92152eb0e Mon Sep 17 00:00:00 2001 From: Nick Pape Date: Mon, 24 Sep 2018 16:30:13 -0700 Subject: [PATCH 03/27] Fill out most of the rest of the logic for the add command --- .../rush-lib/src/cli/RushCommandLineParser.ts | 2 + apps/rush-lib/src/cli/actions/AddAction.ts | 115 +++++++++++++++++- .../CommandLineHelp.test.ts.snap | 30 +++++ 3 files changed, 143 insertions(+), 4 deletions(-) diff --git a/apps/rush-lib/src/cli/RushCommandLineParser.ts b/apps/rush-lib/src/cli/RushCommandLineParser.ts index 436196af817..0ad6f0cd7da 100644 --- a/apps/rush-lib/src/cli/RushCommandLineParser.ts +++ b/apps/rush-lib/src/cli/RushCommandLineParser.ts @@ -14,6 +14,7 @@ import { CommandJson } from '../api/CommandLineJson'; import { Utilities } from '../utilities/Utilities'; import { BaseScriptAction } from '../cli/scriptActions/BaseScriptAction'; +import { AddAction } from './actions/AddAction'; import { ChangeAction } from './actions/ChangeAction'; import { CheckAction } from './actions/CheckAction'; import { UpdateAction } from './actions/UpdateAction'; @@ -109,6 +110,7 @@ export class RushCommandLineParser extends CommandLineParser { this.rushConfiguration = RushConfiguration.loadFromConfigurationFile(rushJsonFilename); } + this.addAction(new AddAction(this)); this.addAction(new ChangeAction(this)); this.addAction(new CheckAction(this)); this.addAction(new InstallAction(this)); diff --git a/apps/rush-lib/src/cli/actions/AddAction.ts b/apps/rush-lib/src/cli/actions/AddAction.ts index 0819d12da78..40a795c2305 100644 --- a/apps/rush-lib/src/cli/actions/AddAction.ts +++ b/apps/rush-lib/src/cli/actions/AddAction.ts @@ -5,6 +5,7 @@ import * as os from 'os'; import * as path from 'path'; import * as child_process from 'child_process'; import * as colors from 'colors'; +import * as semver from 'semver'; import inquirer = require('inquirer'); @@ -12,7 +13,11 @@ import { CommandLineFlagParameter, CommandLineStringParameter } from '@microsoft/ts-command-line'; -import { FileSystem } from '@microsoft/node-core-library'; +import { + FileSystem, + JsonFile, + FileConstants +} from '@microsoft/node-core-library'; import { RushConfigurationProject } from '../../api/RushConfigurationProject'; import { @@ -30,8 +35,11 @@ import { LockStepVersionPolicy, VersionPolicyDefinitionName } from '../../api/VersionPolicy'; +import { InstallManager, IInstallManagerOptions } from '../../logic/InstallManager'; +import { PurgeManager } from '../../logic/PurgeManager'; +import { Utilities } from '../../utilities/Utilities'; -export class ChangeAction extends BaseRushAction { +export class AddAction extends BaseRushAction { private _exactFlag: CommandLineFlagParameter; private _caretFlag: CommandLineFlagParameter; private _devDependencyFlag: CommandLineFlagParameter; @@ -104,12 +112,111 @@ export class ChangeAction extends BaseRushAction { return Promise.reject(new Error('Not currently in a project folder')); } + if (this._caretFlag.value && this._exactFlag.value) { + return Promise.reject(new Error('Only one of --caret and --exact should be specified')); + } + + const packageName: string = this._packageName.value!; + const initialVersion: string | undefined = this._versionSpecifier.value; + + const implicitlyPinned: Map + = InstallManager.collectImplicitlyPreferredVersions(this.rushConfiguration); + + const version = this._getNormalizedVersionSpec( + packageName, initialVersion, implicitlyPinned.get(packageName)); + + if (this._devDependencyFlag.value) { + project.packageJson.devDependencies + = this._updateDependency(project.packageJson.devDependencies, packageName, version) + } else { + project.packageJson.dependencies + = this._updateDependency(project.packageJson.dependencies, packageName, version); + } + + // overwrite existing file + JsonFile.save(project.packageJson, path.join(project.projectFolder, FileConstants.PackageJson)); + + if (this._noInstallFlag.value) { + return Promise.resolve(); + } + + const purgeManager: PurgeManager = new PurgeManager(this.rushConfiguration); + const installManager: InstallManager = new InstallManager(this.rushConfiguration, purgeManager); + const installManagerOptions: IInstallManagerOptions = { + debug: this.parser.isDebug, + allowShrinkwrapUpdates: true, + bypassPolicy: false, + noLink: false, + fullUpgrade: false, + recheckShrinkwrap: false, + networkConcurrency: undefined, + collectLogFile: true + }; + return installManager.doInstall(installManagerOptions) + .then(() => { + purgeManager.deleteAll(); + }) + .catch((error) => { + purgeManager.deleteAll(); + throw error; + }); + } + + private _getNormalizedVersionSpec( + packageName: string, + initialSpec: string | undefined, + implicitlyPinnedVersion: string | undefined): string { + // if ensureConsistentVersions => reuse the pinned version + // else, query the registry and use the latest that satisfies semver spec + if (initialSpec && implicitlyPinnedVersion && initialSpec === implicitlyPinnedVersion) { + return initialSpec; + } + if (this.rushConfiguration.enforceConsistentVersions) { + if (implicitlyPinnedVersion) { + return implicitlyPinnedVersion; + } + } + + const allVersions: string = + Utilities.executeCommandAndCaptureOutput(this.rushConfiguration.packageManagerToolFilename, + ['view', initialSpec ? `${packageName}@${initialSpec}` : packageName, 'versions'], + this.rushConfiguration.commonTempFolder); - this.rushConfiguration.projects + const versionList: Array = JSON.parse(allVersions).sort(semver.compare); + let selectedVersion: string | undefined; - return Promise.resolve(); + if (initialSpec) { + for (const version of versionList) { + if (semver.satisfies(version, initialSpec)) { + selectedVersion = version; + break; + } + } + if (!selectedVersion) { + throw new Error(`Cannot find version for ${packageName} that satisfies "${initialSpec}"`); + } + } else { + selectedVersion = versionList[0]; + } + + if (this._caretFlag.value) { + return '^' + selectedVersion; + } else if (this._exactFlag.value) { + return selectedVersion; + } else { + return '~' + selectedVersion!; + } + } + + private _updateDependency(dependencies: { [key: string]: string } | undefined, + packageName: string, version: string): { [key: string]: string } { + if (!dependencies) { + dependencies = {}; + } + dependencies[packageName] = version!; + return dependencies; } } diff --git a/apps/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap b/apps/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap index 309c7185bda..ab769156139 100644 --- a/apps/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap +++ b/apps/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap @@ -15,6 +15,7 @@ proven turnkey solution for monorepo management, Rush is for you. Positional arguments: + add Adds a dependency to the package.json and runs rush upgrade. change Records changes made to projects, indicating how the package version number should be bumped for the next publish. @@ -51,6 +52,35 @@ For detailed help about a specific command, use: rush -h " `; +exports[`CommandLineHelp prints the help for "rush add" 1`] = ` +"usage: rush add [-h] -p PACKAGE_NAME [-v VERSION_RANGE] [--exact] [--caret] + [--dev] [-c] [-n] + + +Blah. + +Optional arguments: + -h, --help Show this help message and exit. + -p PACKAGE_NAME, --package PACKAGE_NAME + (Required) The name of the package which should be + added as a dependency + -v VERSION_RANGE, --version VERSION_RANGE + --exact If specified, the version specifier inserted into the + package.json will be a locked, exact version. + --caret If specified, the version specifier inserted into the + package.json will be a prepended with a \\"caret\\" + specifier (\\"^\\"). + --dev If specified, the package will be added as a + \\"devDependency\\" to the package.json + -c, --make-consistent + If specified, other packages with this dependency + will have their package.json files updated to point + at the specified depdendency + -n, --no-install If specified, the \\"rush update\\" command will not be + run after updating the package.json files. +" +`; + exports[`CommandLineHelp prints the help for "rush build" 1`] = ` "usage: rush build [-h] [-p COUNT] [-t PROJECT1] [--to-version-policy VERSION_POLICY_NAME] [-f PROJECT2] [-v] From 0ae6ccfc042400eeb78f18c8bee4411dc7184232 Mon Sep 17 00:00:00 2001 From: Nick Pape Date: Mon, 24 Sep 2018 16:34:05 -0700 Subject: [PATCH 04/27] Fix lint issues --- apps/rush-lib/src/cli/actions/AddAction.ts | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/apps/rush-lib/src/cli/actions/AddAction.ts b/apps/rush-lib/src/cli/actions/AddAction.ts index 40a795c2305..f27bb644a7b 100644 --- a/apps/rush-lib/src/cli/actions/AddAction.ts +++ b/apps/rush-lib/src/cli/actions/AddAction.ts @@ -3,38 +3,20 @@ import * as os from 'os'; import * as path from 'path'; -import * as child_process from 'child_process'; -import * as colors from 'colors'; import * as semver from 'semver'; -import inquirer = require('inquirer'); - import { CommandLineFlagParameter, CommandLineStringParameter } from '@microsoft/ts-command-line'; import { - FileSystem, JsonFile, FileConstants } from '@microsoft/node-core-library'; import { RushConfigurationProject } from '../../api/RushConfigurationProject'; -import { - IChangeFile, - IChangeInfo -} from '../../api/ChangeManagement'; -import { VersionControl } from '../../utilities/VersionControl'; -import { ChangeFile } from '../../api/ChangeFile'; import { BaseRushAction } from './BaseRushAction'; import { RushCommandLineParser } from '../RushCommandLineParser'; -import { ChangeFiles } from '../../logic/ChangeFiles'; -import { - VersionPolicy, - IndividualVersionPolicy, - LockStepVersionPolicy, - VersionPolicyDefinitionName -} from '../../api/VersionPolicy'; import { InstallManager, IInstallManagerOptions } from '../../logic/InstallManager'; import { PurgeManager } from '../../logic/PurgeManager'; import { Utilities } from '../../utilities/Utilities'; @@ -122,12 +104,12 @@ export class AddAction extends BaseRushAction { const implicitlyPinned: Map = InstallManager.collectImplicitlyPreferredVersions(this.rushConfiguration); - const version = this._getNormalizedVersionSpec( + const version: string = this._getNormalizedVersionSpec( packageName, initialVersion, implicitlyPinned.get(packageName)); if (this._devDependencyFlag.value) { project.packageJson.devDependencies - = this._updateDependency(project.packageJson.devDependencies, packageName, version) + = this._updateDependency(project.packageJson.devDependencies, packageName, version); } else { project.packageJson.dependencies = this._updateDependency(project.packageJson.dependencies, packageName, version); From a66230df3d66fe649d6330845f7c5f449433c91d Mon Sep 17 00:00:00 2001 From: Nick Pape Date: Mon, 24 Sep 2018 17:56:45 -0700 Subject: [PATCH 05/27] Some small change4s --- apps/rush-lib/src/cli/actions/AddAction.ts | 26 +++++++++++++++------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/apps/rush-lib/src/cli/actions/AddAction.ts b/apps/rush-lib/src/cli/actions/AddAction.ts index f27bb644a7b..8227385668c 100644 --- a/apps/rush-lib/src/cli/actions/AddAction.ts +++ b/apps/rush-lib/src/cli/actions/AddAction.ts @@ -104,6 +104,8 @@ export class AddAction extends BaseRushAction { const implicitlyPinned: Map = InstallManager.collectImplicitlyPreferredVersions(this.rushConfiguration); + console.log(`implicitlyPinned size: ${implicitlyPinned.size}`); + const version: string = this._getNormalizedVersionSpec( packageName, initialVersion, implicitlyPinned.get(packageName)); @@ -149,6 +151,11 @@ export class AddAction extends BaseRushAction { packageName: string, initialSpec: string | undefined, implicitlyPinnedVersion: string | undefined): string { + console.log(`_getNormalizedVersionSpec()`); + console.log(`packageName: ${packageName}`); + console.log(`initialSpec: ${initialSpec}`); + console.log(`implicitlyPinnedVersion: ${implicitlyPinnedVersion}`); + // if ensureConsistentVersions => reuse the pinned version // else, query the registry and use the latest that satisfies semver spec if (initialSpec && implicitlyPinnedVersion && initialSpec === implicitlyPinnedVersion) { @@ -161,16 +168,17 @@ export class AddAction extends BaseRushAction { } } - const allVersions: string = - Utilities.executeCommandAndCaptureOutput(this.rushConfiguration.packageManagerToolFilename, - ['view', initialSpec ? `${packageName}@${initialSpec}` : packageName, 'versions'], - this.rushConfiguration.commonTempFolder); - - const versionList: Array = JSON.parse(allVersions).sort(semver.compare); - let selectedVersion: string | undefined; if (initialSpec) { + const allVersions: string = + Utilities.executeCommandAndCaptureOutput(this.rushConfiguration.packageManagerToolFilename, + ['view', packageName, 'versions', '--json'], + this.rushConfiguration.commonTempFolder); + + let versionList: Array = JSON.parse(allVersions); + versionList = versionList.sort((a: string, b: string) => { return semver.gt(a, b) ? -1 : 1; }); + for (const version of versionList) { if (semver.satisfies(version, initialSpec)) { selectedVersion = version; @@ -181,7 +189,9 @@ export class AddAction extends BaseRushAction { throw new Error(`Cannot find version for ${packageName} that satisfies "${initialSpec}"`); } } else { - selectedVersion = versionList[0]; + selectedVersion = Utilities.executeCommandAndCaptureOutput(this.rushConfiguration.packageManagerToolFilename, + ['view', `${packageName}@latest`, 'version'], + this.rushConfiguration.commonTempFolder).trim(); } if (this._caretFlag.value) { From 9c6f76f896fa0476e8394a5dfdd6c1418cdc6634 Mon Sep 17 00:00:00 2001 From: Nick Pape Date: Mon, 24 Sep 2018 18:59:35 -0700 Subject: [PATCH 06/27] Small changes --- .../rush-lib/src/api/VersionMismatchFinder.ts | 45 ++++++++++-------- apps/rush-lib/src/cli/actions/AddAction.ts | 46 +++++++++++++++++-- 2 files changed, 66 insertions(+), 25 deletions(-) diff --git a/apps/rush-lib/src/api/VersionMismatchFinder.ts b/apps/rush-lib/src/api/VersionMismatchFinder.ts index aec95eeaab7..3c787d3e67d 100644 --- a/apps/rush-lib/src/api/VersionMismatchFinder.ts +++ b/apps/rush-lib/src/api/VersionMismatchFinder.ts @@ -30,31 +30,36 @@ export class VersionMismatchFinder { VersionMismatchFinder._checkForInconsistentVersions(rushConfiguration, false); } - private static _checkForInconsistentVersions( - rushConfiguration: RushConfiguration, - isRushCheckCommand: boolean): void { + public static getMismatches(rushConfiguration: RushConfiguration): VersionMismatchFinder { + // Collect all the preferred versions into a single table + const allPreferredVersions: { [dependency: string]: string } = {}; - if (rushConfiguration.enforceConsistentVersions || isRushCheckCommand) { - // Collect all the preferred versions into a single table - const allPreferredVersions: { [dependency: string]: string } = {}; + rushConfiguration.commonVersions.getAllPreferredVersions().forEach((version: string, dependency: string) => { + allPreferredVersions[dependency] = version; + }); - rushConfiguration.commonVersions.getAllPreferredVersions().forEach((version: string, dependency: string) => { - allPreferredVersions[dependency] = version; - }); + // Create a fake project for the purposes of reporting conflicts with preferredVersions + // or xstitchPreferredVersions from common-versions.json + const projects: RushConfigurationProject[] = [...rushConfiguration.projects]; - // Create a fake project for the purposes of reporting conflicts with preferredVersions - // or xstitchPreferredVersions from common-versions.json - const projects: RushConfigurationProject[] = [...rushConfiguration.projects]; + projects.push({ + packageName: 'preferred versions from ' + RushConstants.commonVersionsFilename, + packageJson: { dependencies: allPreferredVersions } + } as RushConfigurationProject); - projects.push({ - packageName: 'preferred versions from ' + RushConstants.commonVersionsFilename, - packageJson: { dependencies: allPreferredVersions } - } as RushConfigurationProject); + return new VersionMismatchFinder( + projects, + rushConfiguration.commonVersions.allowedAlternativeVersions + ); + } + + private static _checkForInconsistentVersions( + rushConfiguration: RushConfiguration, + isRushCheckCommand: boolean): void { - const mismatchFinder: VersionMismatchFinder = new VersionMismatchFinder( - projects, - rushConfiguration.commonVersions.allowedAlternativeVersions - ); + if (rushConfiguration.enforceConsistentVersions || isRushCheckCommand) { + const mismatchFinder: VersionMismatchFinder + = VersionMismatchFinder.getMismatches(rushConfiguration); // Iterate over the list. For any dependency with mismatching versions, print the projects mismatchFinder.getMismatches().forEach((dependency: string) => { diff --git a/apps/rush-lib/src/cli/actions/AddAction.ts b/apps/rush-lib/src/cli/actions/AddAction.ts index 8227385668c..ff882c20ad2 100644 --- a/apps/rush-lib/src/cli/actions/AddAction.ts +++ b/apps/rush-lib/src/cli/actions/AddAction.ts @@ -20,6 +20,7 @@ import { RushCommandLineParser } from '../RushCommandLineParser'; import { InstallManager, IInstallManagerOptions } from '../../logic/InstallManager'; import { PurgeManager } from '../../logic/PurgeManager'; import { Utilities } from '../../utilities/Utilities'; +import { VersionMismatchFinder } from '../../api/VersionMismatchFinder'; export class AddAction extends BaseRushAction { private _exactFlag: CommandLineFlagParameter; @@ -117,6 +118,43 @@ export class AddAction extends BaseRushAction { = this._updateDependency(project.packageJson.dependencies, packageName, version); } + if (this.rushConfiguration.enforceConsistentVersions) { + // we need to do a mismatch check + const mismatchFinder: VersionMismatchFinder = VersionMismatchFinder.getMismatches(this.rushConfiguration); + + const mismatches: Array = mismatchFinder.getMismatches(); + if (mismatches.length) { + if (!this._makeConsistentFlag.value) { + return Promise.reject(new Error(`Adding "${packageName}@${version}" to ${project.packageName}` + + ` causes mismatched dependencies. Use the --make-consistent flag to update other packages to use this` + + ` version, or do not specify the --version flag.`)); + } + + // otherwise we need to go update a bunch of other projects + for (const mismatchedVersion of mismatchFinder.getVersionsOfMismatch(packageName)!) { + for (const consumer of mismatchFinder.getConsumersOfMismatch(packageName, mismatchedVersion)!) { + if (packageName !== project.packageName) { + const consumerProject: RushConfigurationProject = this.rushConfiguration.getProjectByName(consumer)!; + + if (consumerProject.packageJson.devDependencies + && consumerProject.packageJson.devDependencies[packageName]) { + consumerProject.packageJson.devDependencies + = this._updateDependency(consumerProject.packageJson.devDependencies, packageName, version); + } else { + consumerProject.packageJson.dependencies + = this._updateDependency(consumerProject.packageJson.dependencies, packageName, version); + } + + // overwrite existing file + const consumerPackageJsonPath: string + = path.join(consumerProject.projectFolder, FileConstants.PackageJson); + JsonFile.save(consumerProject.packageJson, consumerPackageJsonPath); + } + } + } + } + } + // overwrite existing file JsonFile.save(project.packageJson, path.join(project.projectFolder, FileConstants.PackageJson)); @@ -162,15 +200,13 @@ export class AddAction extends BaseRushAction { return initialSpec; } - if (this.rushConfiguration.enforceConsistentVersions) { - if (implicitlyPinnedVersion) { - return implicitlyPinnedVersion; - } + if (this.rushConfiguration.enforceConsistentVersions && !initialSpec && implicitlyPinnedVersion) { + return implicitlyPinnedVersion; } let selectedVersion: string | undefined; - if (initialSpec) { + if (initialSpec && initialSpec !== 'latest') { const allVersions: string = Utilities.executeCommandAndCaptureOutput(this.rushConfiguration.packageManagerToolFilename, ['view', packageName, 'versions', '--json'], From 30df58264db7c878f3789a336b43224ae7f32de5 Mon Sep 17 00:00:00 2001 From: Nick Pape Date: Tue, 25 Sep 2018 17:10:07 -0700 Subject: [PATCH 07/27] Move add code into a seperate class --- apps/rush-lib/src/cli/actions/AddAction.ts | 169 ++------------- .../src/logic/DependencyIntegrator.ts | 198 ++++++++++++++++++ 2 files changed, 210 insertions(+), 157 deletions(-) create mode 100644 apps/rush-lib/src/logic/DependencyIntegrator.ts diff --git a/apps/rush-lib/src/cli/actions/AddAction.ts b/apps/rush-lib/src/cli/actions/AddAction.ts index ff882c20ad2..e1d2ecae159 100644 --- a/apps/rush-lib/src/cli/actions/AddAction.ts +++ b/apps/rush-lib/src/cli/actions/AddAction.ts @@ -2,25 +2,16 @@ // See LICENSE in the project root for license information. import * as os from 'os'; -import * as path from 'path'; -import * as semver from 'semver'; import { CommandLineFlagParameter, CommandLineStringParameter } from '@microsoft/ts-command-line'; -import { - JsonFile, - FileConstants -} from '@microsoft/node-core-library'; import { RushConfigurationProject } from '../../api/RushConfigurationProject'; import { BaseRushAction } from './BaseRushAction'; import { RushCommandLineParser } from '../RushCommandLineParser'; -import { InstallManager, IInstallManagerOptions } from '../../logic/InstallManager'; -import { PurgeManager } from '../../logic/PurgeManager'; -import { Utilities } from '../../utilities/Utilities'; -import { VersionMismatchFinder } from '../../api/VersionMismatchFinder'; +import { DependencyIntegrator, SemVerStyle } from '../../logic/DependencyIntegrator'; export class AddAction extends BaseRushAction { private _exactFlag: CommandLineFlagParameter; @@ -99,152 +90,16 @@ export class AddAction extends BaseRushAction { return Promise.reject(new Error('Only one of --caret and --exact should be specified')); } - const packageName: string = this._packageName.value!; - const initialVersion: string | undefined = this._versionSpecifier.value; - - const implicitlyPinned: Map - = InstallManager.collectImplicitlyPreferredVersions(this.rushConfiguration); - - console.log(`implicitlyPinned size: ${implicitlyPinned.size}`); - - const version: string = this._getNormalizedVersionSpec( - packageName, initialVersion, implicitlyPinned.get(packageName)); - - if (this._devDependencyFlag.value) { - project.packageJson.devDependencies - = this._updateDependency(project.packageJson.devDependencies, packageName, version); - } else { - project.packageJson.dependencies - = this._updateDependency(project.packageJson.dependencies, packageName, version); - } - - if (this.rushConfiguration.enforceConsistentVersions) { - // we need to do a mismatch check - const mismatchFinder: VersionMismatchFinder = VersionMismatchFinder.getMismatches(this.rushConfiguration); - - const mismatches: Array = mismatchFinder.getMismatches(); - if (mismatches.length) { - if (!this._makeConsistentFlag.value) { - return Promise.reject(new Error(`Adding "${packageName}@${version}" to ${project.packageName}` - + ` causes mismatched dependencies. Use the --make-consistent flag to update other packages to use this` - + ` version, or do not specify the --version flag.`)); - } - - // otherwise we need to go update a bunch of other projects - for (const mismatchedVersion of mismatchFinder.getVersionsOfMismatch(packageName)!) { - for (const consumer of mismatchFinder.getConsumersOfMismatch(packageName, mismatchedVersion)!) { - if (packageName !== project.packageName) { - const consumerProject: RushConfigurationProject = this.rushConfiguration.getProjectByName(consumer)!; - - if (consumerProject.packageJson.devDependencies - && consumerProject.packageJson.devDependencies[packageName]) { - consumerProject.packageJson.devDependencies - = this._updateDependency(consumerProject.packageJson.devDependencies, packageName, version); - } else { - consumerProject.packageJson.dependencies - = this._updateDependency(consumerProject.packageJson.dependencies, packageName, version); - } - - // overwrite existing file - const consumerPackageJsonPath: string - = path.join(consumerProject.projectFolder, FileConstants.PackageJson); - JsonFile.save(consumerProject.packageJson, consumerPackageJsonPath); - } - } - } - } - } - - // overwrite existing file - JsonFile.save(project.packageJson, path.join(project.projectFolder, FileConstants.PackageJson)); - - if (this._noInstallFlag.value) { - return Promise.resolve(); - } - - const purgeManager: PurgeManager = new PurgeManager(this.rushConfiguration); - const installManager: InstallManager = new InstallManager(this.rushConfiguration, purgeManager); - const installManagerOptions: IInstallManagerOptions = { - debug: this.parser.isDebug, - allowShrinkwrapUpdates: true, - bypassPolicy: false, - noLink: false, - fullUpgrade: false, - recheckShrinkwrap: false, - networkConcurrency: undefined, - collectLogFile: true - }; - - return installManager.doInstall(installManagerOptions) - .then(() => { - purgeManager.deleteAll(); - }) - .catch((error) => { - purgeManager.deleteAll(); - throw error; - }); - } - - private _getNormalizedVersionSpec( - packageName: string, - initialSpec: string | undefined, - implicitlyPinnedVersion: string | undefined): string { - console.log(`_getNormalizedVersionSpec()`); - console.log(`packageName: ${packageName}`); - console.log(`initialSpec: ${initialSpec}`); - console.log(`implicitlyPinnedVersion: ${implicitlyPinnedVersion}`); - - // if ensureConsistentVersions => reuse the pinned version - // else, query the registry and use the latest that satisfies semver spec - if (initialSpec && implicitlyPinnedVersion && initialSpec === implicitlyPinnedVersion) { - return initialSpec; - } - - if (this.rushConfiguration.enforceConsistentVersions && !initialSpec && implicitlyPinnedVersion) { - return implicitlyPinnedVersion; - } - - let selectedVersion: string | undefined; - - if (initialSpec && initialSpec !== 'latest') { - const allVersions: string = - Utilities.executeCommandAndCaptureOutput(this.rushConfiguration.packageManagerToolFilename, - ['view', packageName, 'versions', '--json'], - this.rushConfiguration.commonTempFolder); - - let versionList: Array = JSON.parse(allVersions); - versionList = versionList.sort((a: string, b: string) => { return semver.gt(a, b) ? -1 : 1; }); - - for (const version of versionList) { - if (semver.satisfies(version, initialSpec)) { - selectedVersion = version; - break; - } - } - if (!selectedVersion) { - throw new Error(`Cannot find version for ${packageName} that satisfies "${initialSpec}"`); - } - } else { - selectedVersion = Utilities.executeCommandAndCaptureOutput(this.rushConfiguration.packageManagerToolFilename, - ['view', `${packageName}@latest`, 'version'], - this.rushConfiguration.commonTempFolder).trim(); - } - - if (this._caretFlag.value) { - return '^' + selectedVersion; - } else if (this._exactFlag.value) { - return selectedVersion; - } else { - return '~' + selectedVersion!; - } - } - - private _updateDependency(dependencies: { [key: string]: string } | undefined, - packageName: string, version: string): { [key: string]: string } { - if (!dependencies) { - dependencies = {}; - } - dependencies[packageName] = version!; - return dependencies; + return new DependencyIntegrator(this.rushConfiguration).run({ + currentProject: project, + packageName: this._packageName.value!, + initialVersion: this._versionSpecifier.value, + devDependency: this._devDependencyFlag.value, + updateOtherPackages: this._makeConsistentFlag.value, + skipInstall: this._noInstallFlag.value, + debugInstall: this.parser.isDebug, + rangeStyle: this._caretFlag.value ? SemVerStyle.Caret + : (this._exactFlag.value ? SemVerStyle.Exact : SemVerStyle.Tilde) + }); } } diff --git a/apps/rush-lib/src/logic/DependencyIntegrator.ts b/apps/rush-lib/src/logic/DependencyIntegrator.ts new file mode 100644 index 00000000000..3fcc1ee6c02 --- /dev/null +++ b/apps/rush-lib/src/logic/DependencyIntegrator.ts @@ -0,0 +1,198 @@ +import * as path from 'path'; +import * as semver from 'semver'; + +import { + JsonFile, + FileConstants +} from '@microsoft/node-core-library'; + +import { RushConfiguration } from '../api/RushConfiguration'; +import { InstallManager, IInstallManagerOptions } from './InstallManager'; +import { RushConfigurationProject } from '../api/RushConfigurationProject'; +import { VersionMismatchFinder } from '../api/VersionMismatchFinder'; +import { PurgeManager } from './PurgeManager'; +import { Utilities } from '../utilities/Utilities'; + +export const enum SemVerStyle { + Exact = 'exact', + Caret = 'caret', + Tilde = 'tilde' +} + +export interface IDependencyIntegratorOptions { + currentProject: RushConfigurationProject; + packageName: string; + initialVersion: string | undefined; + devDependency: boolean; + updateOtherPackages: boolean; + skipInstall: boolean; + debugInstall: boolean; + rangeStyle: SemVerStyle; +} + +export class DependencyIntegrator { + private _rushConfiguration: RushConfiguration; + + public constructor(rushConfiguration: RushConfiguration) { + this._rushConfiguration = rushConfiguration; + } + + public run(options: IDependencyIntegratorOptions): Promise { + const { + currentProject, + packageName, + initialVersion, + devDependency, + updateOtherPackages, + skipInstall, + debugInstall, + rangeStyle + } = options; + + const implicitlyPinned: Map + = InstallManager.collectImplicitlyPreferredVersions(this._rushConfiguration); + + console.log(`implicitlyPinned size: ${implicitlyPinned.size}`); + + const version: string = this._getNormalizedVersionSpec( + packageName, initialVersion, implicitlyPinned.get(packageName), rangeStyle); + + if (devDependency) { + currentProject.packageJson.devDependencies + = this._updateDependency(currentProject.packageJson.devDependencies, packageName, version); + } else { + currentProject.packageJson.dependencies + = this._updateDependency(currentProject.packageJson.dependencies, packageName, version); + } + + if (this._rushConfiguration.enforceConsistentVersions) { + // we need to do a mismatch check + const mismatchFinder: VersionMismatchFinder = VersionMismatchFinder.getMismatches(this._rushConfiguration); + + const mismatches: Array = mismatchFinder.getMismatches(); + if (mismatches.length) { + if (!updateOtherPackages) { + return Promise.reject(new Error(`Adding '${packageName}@${version}' to ${currentProject.packageName}` + + ` causes mismatched dependencies. Use the --make-consistent flag to update other packages to use this` + + ` version, or do not specify the --version flag.`)); + } + + // otherwise we need to go update a bunch of other projects + for (const mismatchedVersion of mismatchFinder.getVersionsOfMismatch(packageName)!) { + for (const consumer of mismatchFinder.getConsumersOfMismatch(packageName, mismatchedVersion)!) { + if (packageName !== currentProject.packageName) { + const consumerProject: RushConfigurationProject = this._rushConfiguration.getProjectByName(consumer)!; + + if (consumerProject.packageJson.devDependencies + && consumerProject.packageJson.devDependencies[packageName]) { + consumerProject.packageJson.devDependencies + = this._updateDependency(consumerProject.packageJson.devDependencies, packageName, version); + } else { + consumerProject.packageJson.dependencies + = this._updateDependency(consumerProject.packageJson.dependencies, packageName, version); + } + + // overwrite existing file + const consumerPackageJsonPath: string + = path.join(consumerProject.projectFolder, FileConstants.PackageJson); + JsonFile.save(consumerProject.packageJson, consumerPackageJsonPath); + } + } + } + } + } + + // overwrite existing file + JsonFile.save(currentProject.packageJson, path.join(currentProject.projectFolder, FileConstants.PackageJson)); + + if (skipInstall) { + return Promise.resolve(); + } + + const purgeManager: PurgeManager = new PurgeManager(this._rushConfiguration); + const installManager: InstallManager = new InstallManager(this._rushConfiguration, purgeManager); + const installManagerOptions: IInstallManagerOptions = { + debug: debugInstall, + allowShrinkwrapUpdates: true, + bypassPolicy: false, + noLink: false, + fullUpgrade: false, + recheckShrinkwrap: false, + networkConcurrency: undefined, + collectLogFile: true + }; + + return installManager.doInstall(installManagerOptions) + .then(() => { + purgeManager.deleteAll(); + }) + .catch((error) => { + purgeManager.deleteAll(); + throw error; + }); + } + + private _getNormalizedVersionSpec( + packageName: string, + initialSpec: string | undefined, + implicitlyPinnedVersion: string | undefined, + rangeStyle: SemVerStyle ): string { + console.log(`_getNormalizedVersionSpec()`); + console.log(`packageName: ${packageName}`); + console.log(`initialSpec: ${initialSpec}`); + console.log(`implicitlyPinnedVersion: ${implicitlyPinnedVersion}`); + + // if ensureConsistentVersions => reuse the pinned version + // else, query the registry and use the latest that satisfies semver spec + if (initialSpec && implicitlyPinnedVersion && initialSpec === implicitlyPinnedVersion) { + return initialSpec; + } + + if (this._rushConfiguration.enforceConsistentVersions && !initialSpec && implicitlyPinnedVersion) { + return implicitlyPinnedVersion; + } + + let selectedVersion: string | undefined; + + if (initialSpec && initialSpec !== 'latest') { + const allVersions: string = + Utilities.executeCommandAndCaptureOutput(this._rushConfiguration.packageManagerToolFilename, + ['view', packageName, 'versions', '--json'], + this._rushConfiguration.commonTempFolder); + + let versionList: Array = JSON.parse(allVersions); + versionList = versionList.sort((a: string, b: string) => { return semver.gt(a, b) ? -1 : 1; }); + + for (const version of versionList) { + if (semver.satisfies(version, initialSpec)) { + selectedVersion = version; + break; + } + } + if (!selectedVersion) { + throw new Error(`Cannot find version for ${packageName} that satisfies '${initialSpec}'`); + } + } else { + selectedVersion = Utilities.executeCommandAndCaptureOutput(this._rushConfiguration.packageManagerToolFilename, + ['view', `${packageName}@latest`, 'version'], + this._rushConfiguration.commonTempFolder).trim(); + } + + if (rangeStyle === SemVerStyle.Caret) { + return '^' + selectedVersion; + } else if (rangeStyle === SemVerStyle.Exact) { + return selectedVersion; + } else { + return '~' + selectedVersion!; + } + } + + private _updateDependency(dependencies: { [key: string]: string } | undefined, + packageName: string, version: string): { [key: string]: string } { + if (!dependencies) { + dependencies = {}; + } + dependencies[packageName] = version!; + return dependencies; + } +} \ No newline at end of file From caf6983dfd8944dad8a790e86a158eae89b58b88 Mon Sep 17 00:00:00 2001 From: Nick Pape Date: Tue, 25 Sep 2018 17:51:10 -0700 Subject: [PATCH 08/27] Fix bugs --- .../src/logic/DependencyIntegrator.ts | 105 +++++++++++++----- 1 file changed, 79 insertions(+), 26 deletions(-) diff --git a/apps/rush-lib/src/logic/DependencyIntegrator.ts b/apps/rush-lib/src/logic/DependencyIntegrator.ts index 3fcc1ee6c02..1f37ac80da5 100644 --- a/apps/rush-lib/src/logic/DependencyIntegrator.ts +++ b/apps/rush-lib/src/logic/DependencyIntegrator.ts @@ -3,7 +3,8 @@ import * as semver from 'semver'; import { JsonFile, - FileConstants + FileConstants, + IPackageJson } from '@microsoft/node-core-library'; import { RushConfiguration } from '../api/RushConfiguration'; @@ -19,6 +20,19 @@ export const enum SemVerStyle { Tilde = 'tilde' } +export const enum DependencyKind { + DevDependency = 'devDependency', + Dependency = 'dependency' +} + +export interface IUpdateProjectOptions { + project: RushConfigurationProject; + packageName: string; + newVersion: string; + dependencyKind?: DependencyKind; + doNotSave?: boolean; +} + export interface IDependencyIntegratorOptions { currentProject: RushConfigurationProject; packageName: string; @@ -57,13 +71,17 @@ export class DependencyIntegrator { const version: string = this._getNormalizedVersionSpec( packageName, initialVersion, implicitlyPinned.get(packageName), rangeStyle); - if (devDependency) { - currentProject.packageJson.devDependencies - = this._updateDependency(currentProject.packageJson.devDependencies, packageName, version); - } else { - currentProject.packageJson.dependencies - = this._updateDependency(currentProject.packageJson.dependencies, packageName, version); - } + const currentProjectUpdate: IUpdateProjectOptions = { + project: currentProject, + packageName, + newVersion: version, + dependencyKind: devDependency ? DependencyKind.DevDependency : DependencyKind.Dependency, + doNotSave: true + }; + this.updateProject(currentProjectUpdate); + + currentProjectUpdate.doNotSave = false; + const packageUpdates: Array = [currentProjectUpdate]; if (this._rushConfiguration.enforceConsistentVersions) { // we need to do a mismatch check @@ -80,30 +98,19 @@ export class DependencyIntegrator { // otherwise we need to go update a bunch of other projects for (const mismatchedVersion of mismatchFinder.getVersionsOfMismatch(packageName)!) { for (const consumer of mismatchFinder.getConsumersOfMismatch(packageName, mismatchedVersion)!) { - if (packageName !== currentProject.packageName) { - const consumerProject: RushConfigurationProject = this._rushConfiguration.getProjectByName(consumer)!; - - if (consumerProject.packageJson.devDependencies - && consumerProject.packageJson.devDependencies[packageName]) { - consumerProject.packageJson.devDependencies - = this._updateDependency(consumerProject.packageJson.devDependencies, packageName, version); - } else { - consumerProject.packageJson.dependencies - = this._updateDependency(consumerProject.packageJson.dependencies, packageName, version); - } - - // overwrite existing file - const consumerPackageJsonPath: string - = path.join(consumerProject.projectFolder, FileConstants.PackageJson); - JsonFile.save(consumerProject.packageJson, consumerPackageJsonPath); + if (consumer !== currentProject.packageName) { + packageUpdates.push({ + project: this._rushConfiguration.getProjectByName(consumer)!, + packageName: packageName, + newVersion: version + }); } } } } } - // overwrite existing file - JsonFile.save(currentProject.packageJson, path.join(currentProject.projectFolder, FileConstants.PackageJson)); + this.updateProjects(packageUpdates); if (skipInstall) { return Promise.resolve(); @@ -132,6 +139,52 @@ export class DependencyIntegrator { }); } + public updateProjects(projectUpdates: Array): void { + for (const update of projectUpdates) { + this.updateProject(update); + } + } + + public updateProject(options: IUpdateProjectOptions): void { + let { dependencyKind } = options; + const { + project, + packageName, + newVersion, + doNotSave + } = options; + const packageJson: IPackageJson = project.packageJson; + + let oldDependencyKind: DependencyKind | undefined = undefined; + if (packageJson.dependencies && packageJson.dependencies[packageName]) { + oldDependencyKind = DependencyKind.Dependency; + } else if (packageJson.devDependencies && packageJson.devDependencies[packageName]) { + oldDependencyKind = DependencyKind.DevDependency; + } + + if (!dependencyKind && !oldDependencyKind) { + throw new Error(`Cannot auto-detect dependency type of "${packageName}" for project "${project.packageName}"`); + } + + if (!dependencyKind) { + dependencyKind = oldDependencyKind; + } + + // update the dependency + if (dependencyKind === DependencyKind.Dependency) { + packageJson.dependencies = this._updateDependency(packageJson.dependencies, packageName, newVersion); + } else if (dependencyKind === DependencyKind.DevDependency) { + packageJson.devDependencies = this._updateDependency(packageJson.devDependencies, packageName, newVersion); + } + + if (!doNotSave) { + // overwrite existing file + const packageJsonPath: string + = path.join(project.projectFolder, FileConstants.PackageJson); + JsonFile.save(project.packageJson, packageJsonPath); + } + } + private _getNormalizedVersionSpec( packageName: string, initialSpec: string | undefined, From 0a8a92200cd2204773f83855a7e6c978791787ce Mon Sep 17 00:00:00 2001 From: Nick Pape Date: Tue, 25 Sep 2018 18:46:48 -0700 Subject: [PATCH 09/27] Add logging --- .../src/logic/DependencyIntegrator.ts | 58 +++++++++++++++---- 1 file changed, 47 insertions(+), 11 deletions(-) diff --git a/apps/rush-lib/src/logic/DependencyIntegrator.ts b/apps/rush-lib/src/logic/DependencyIntegrator.ts index 1f37ac80da5..dc1bf7de21f 100644 --- a/apps/rush-lib/src/logic/DependencyIntegrator.ts +++ b/apps/rush-lib/src/logic/DependencyIntegrator.ts @@ -1,3 +1,4 @@ +import * as colors from 'colors'; import * as path from 'path'; import * as semver from 'semver'; @@ -66,11 +67,14 @@ export class DependencyIntegrator { const implicitlyPinned: Map = InstallManager.collectImplicitlyPreferredVersions(this._rushConfiguration); - console.log(`implicitlyPinned size: ${implicitlyPinned.size}`); - const version: string = this._getNormalizedVersionSpec( packageName, initialVersion, implicitlyPinned.get(packageName), rangeStyle); + console.log(); + console.log(colors.green(`Updating projects to use `) + + packageName + '@' + colors.magenta(version)); + console.log(); + const currentProjectUpdate: IUpdateProjectOptions = { project: currentProject, packageName, @@ -181,7 +185,9 @@ export class DependencyIntegrator { // overwrite existing file const packageJsonPath: string = path.join(project.projectFolder, FileConstants.PackageJson); - JsonFile.save(project.packageJson, packageJsonPath); + JsonFile.save(project.packageJson, packageJsonPath); + + console.log(colors.green('Wrote ') + packageJsonPath); } } @@ -189,25 +195,37 @@ export class DependencyIntegrator { packageName: string, initialSpec: string | undefined, implicitlyPinnedVersion: string | undefined, - rangeStyle: SemVerStyle ): string { - console.log(`_getNormalizedVersionSpec()`); - console.log(`packageName: ${packageName}`); - console.log(`initialSpec: ${initialSpec}`); - console.log(`implicitlyPinnedVersion: ${implicitlyPinnedVersion}`); + rangeStyle: SemVerStyle): string { + + console.log(colors.gray(`Determining new version for dependency: ${packageName}`)); + if (initialSpec) { + console.log(`Specified version selector: ${colors.magenta(initialSpec)}`); + } else { + console.log(`No version selector specified, will be automatically determined.`); + } + console.log(); // if ensureConsistentVersions => reuse the pinned version // else, query the registry and use the latest that satisfies semver spec if (initialSpec && implicitlyPinnedVersion && initialSpec === implicitlyPinnedVersion) { + console.log(colors.green('The specified version ') + + colors.magenta(initialSpec) + + colors.green(' has been selected as it matches the implicitly preferred version.')); return initialSpec; } if (this._rushConfiguration.enforceConsistentVersions && !initialSpec && implicitlyPinnedVersion) { + console.log(colors.grey('The enforceConsistentVersions policy is currently active.')); + console.log(`Using the implicitly preferred version ${colors.magenta(implicitlyPinnedVersion)}`); return implicitlyPinnedVersion; } let selectedVersion: string | undefined; if (initialSpec && initialSpec !== 'latest') { + console.log(colors.gray('Finding newest version that satisfies the selector: ') + initialSpec); + console.log(); + console.log(`Querying registry for all versions of ${packageName}...`); const allVersions: string = Utilities.executeCommandAndCaptureOutput(this._rushConfiguration.packageManagerToolFilename, ['view', packageName, 'versions', '--json'], @@ -216,9 +234,12 @@ export class DependencyIntegrator { let versionList: Array = JSON.parse(allVersions); versionList = versionList.sort((a: string, b: string) => { return semver.gt(a, b) ? -1 : 1; }); + console.log(colors.gray(`Found ${versionList.length} available versions.`)); + for (const version of versionList) { if (semver.satisfies(version, initialSpec)) { selectedVersion = version; + console.log(`Found latest version: ${colors.magenta(selectedVersion)}`); break; } } @@ -226,16 +247,31 @@ export class DependencyIntegrator { throw new Error(`Cannot find version for ${packageName} that satisfies '${initialSpec}'`); } } else { - selectedVersion = Utilities.executeCommandAndCaptureOutput(this._rushConfiguration.packageManagerToolFilename, - ['view', `${packageName}@latest`, 'version'], - this._rushConfiguration.commonTempFolder).trim(); + if (initialSpec !== 'latest') { + console.log(colors.gray(`The enforceConsistentVersions policy is NOT active,` + + ` therefore using the latest version.`)); + console.log(); + } + console.log(`Querying NPM registry for latest version of ${packageName}...`); + + selectedVersion = Utilities.executeCommandAndCaptureOutput(this._rushConfiguration.packageManagerToolFilename, + ['view', `${packageName}@latest`, 'version'], + this._rushConfiguration.commonTempFolder).trim(); + console.log(); + + console.log(`Found latest version: ${colors.magenta(selectedVersion)}`); } + console.log(); + if (rangeStyle === SemVerStyle.Caret) { + console.log(colors.gray('The --caret flag was specified, prepending ^ specifier to version.')); return '^' + selectedVersion; } else if (rangeStyle === SemVerStyle.Exact) { + console.log(colors.gray('The --exact flag was specified, not prepending a specifier to version.')); return selectedVersion; } else { + console.log(colors.gray('Prepending ~ specifier to version.')); return '~' + selectedVersion!; } } From 8add64f6a61eb0274778b4106995190e9957b6f3 Mon Sep 17 00:00:00 2001 From: Nick Pape Date: Wed, 26 Sep 2018 13:21:28 -0700 Subject: [PATCH 10/27] Fix wording --- apps/rush-lib/src/logic/DependencyIntegrator.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/rush-lib/src/logic/DependencyIntegrator.ts b/apps/rush-lib/src/logic/DependencyIntegrator.ts index dc1bf7de21f..c8bbc78611d 100644 --- a/apps/rush-lib/src/logic/DependencyIntegrator.ts +++ b/apps/rush-lib/src/logic/DependencyIntegrator.ts @@ -87,7 +87,7 @@ export class DependencyIntegrator { currentProjectUpdate.doNotSave = false; const packageUpdates: Array = [currentProjectUpdate]; - if (this._rushConfiguration.enforceConsistentVersions) { + if (this._rushConfiguration.ensureConsistentVersions) { // we need to do a mismatch check const mismatchFinder: VersionMismatchFinder = VersionMismatchFinder.getMismatches(this._rushConfiguration); @@ -214,7 +214,7 @@ export class DependencyIntegrator { return initialSpec; } - if (this._rushConfiguration.enforceConsistentVersions && !initialSpec && implicitlyPinnedVersion) { + if (this._rushConfiguration.ensureConsistentVersions && !initialSpec && implicitlyPinnedVersion) { console.log(colors.grey('The enforceConsistentVersions policy is currently active.')); console.log(`Using the implicitly preferred version ${colors.magenta(implicitlyPinnedVersion)}`); return implicitlyPinnedVersion; From bc4356076e1fe3424ab65b46f05a921bc9093127 Mon Sep 17 00:00:00 2001 From: Nick Pape Date: Wed, 26 Sep 2018 16:22:18 -0700 Subject: [PATCH 11/27] PR Feedback --- apps/rush-lib/src/api/RushConfiguration.ts | 17 ++++- apps/rush-lib/src/cli/actions/AddAction.ts | 34 ++++++--- .../CommandLineHelp.test.ts.snap | 30 +++++--- .../src/logic/DependencyIntegrator.ts | 74 ++++++++++++++++++- common/reviews/api/rush-lib.api.ts | 4 +- 5 files changed, 129 insertions(+), 30 deletions(-) diff --git a/apps/rush-lib/src/api/RushConfiguration.ts b/apps/rush-lib/src/api/RushConfiguration.ts index 84a8fed71f1..515e28038f9 100644 --- a/apps/rush-lib/src/api/RushConfiguration.ts +++ b/apps/rush-lib/src/api/RushConfiguration.ts @@ -4,7 +4,13 @@ import * as path from 'path'; import * as fs from 'fs'; import * as semver from 'semver'; -import { JsonFile, JsonSchema, PackageName, FileSystem } from '@microsoft/node-core-library'; +import { + JsonFile, + JsonSchema, + Path, + PackageName, + FileSystem +} from '@microsoft/node-core-library'; import { Rush } from '../api/Rush'; import { RushConfigurationProject, IRushConfigurationProjectJson } from './RushConfigurationProject'; @@ -743,10 +749,15 @@ export class RushConfiguration { return this._versionPolicyConfiguration; } - public getCurrentProjectFromPath(currentFolderPath: string): RushConfigurationProject | undefined { + /** + * Returns the project for which the specified path is underneath that project's folder. + * If the path is not under any project's folder, returns undefined. + * @beta + */ + public getProjectForPath(currentFolderPath: string): RushConfigurationProject | undefined { const resolvedPath: string = path.resolve(currentFolderPath); for (const project of this.projects) { - if (path.relative(project.projectFolder, resolvedPath).indexOf('..') !== 0) { + if (Path.isUnder(project.projectFolder, resolvedPath)) { return project; } } diff --git a/apps/rush-lib/src/cli/actions/AddAction.ts b/apps/rush-lib/src/cli/actions/AddAction.ts index e1d2ecae159..e8de2fc4dc9 100644 --- a/apps/rush-lib/src/cli/actions/AddAction.ts +++ b/apps/rush-lib/src/cli/actions/AddAction.ts @@ -18,13 +18,19 @@ export class AddAction extends BaseRushAction { private _caretFlag: CommandLineFlagParameter; private _devDependencyFlag: CommandLineFlagParameter; private _makeConsistentFlag: CommandLineFlagParameter; - private _noInstallFlag: CommandLineFlagParameter; + private _skipUpdateFlag: CommandLineFlagParameter; private _packageName: CommandLineStringParameter; private _versionSpecifier: CommandLineStringParameter; constructor(parser: RushCommandLineParser) { const documentation: string[] = [ - 'Blah.' + 'Adds a dependency on a certain package to the current project (detected using the current' + + ' working directory) and then runs rush update. If no version is specified, a version will' + + ' be automatically detected (typically either the latest version or a version that won\'t break' + + ' the ensureConsistentVersions policy). If a version range is specified, the latest version' + + ' in the range will be used. The version will be automatically prepended with a tilde, unless' + + ' the --exact or --caret flags are used. The --make-consistent flag can be used to update' + + ' all packages with the dependency.' ]; super({ actionName: 'add', @@ -47,32 +53,36 @@ export class AddAction extends BaseRushAction { parameterLongName: '--version', parameterShortName: '-v', argumentName: 'VERSION_RANGE', - description: '' + description: 'An optional version specifier. If specified, the largest version satisfying this range' + + ' will be added to the package.json.' }); this._exactFlag = this.defineFlagParameter({ parameterLongName: '--exact', - description: 'If specified, the version specifier inserted into the' + parameterShortName: '-e', + description: 'If specified, the SemVer specifier added to the' + ' package.json will be a locked, exact version.' }); this._caretFlag = this.defineFlagParameter({ parameterLongName: '--caret', - description: 'If specified, the version specifier inserted into the' + parameterShortName: '-c', + description: 'If specified, the SemVer specifier added to the' + ' package.json will be a prepended with a "caret" specifier ("^").' }); this._devDependencyFlag = this.defineFlagParameter({ parameterLongName: '--dev', + parameterShortName: '-d', description: 'If specified, the package will be added as a "devDependency"' + ' to the package.json' }); this._makeConsistentFlag = this.defineFlagParameter({ parameterLongName: '--make-consistent', - parameterShortName: '-c', + parameterShortName: '-m', description: 'If specified, other packages with this dependency will have their package.json' - + ' files updated to point at the specified depdendency' + + ' files updated to use the same version of the dependency.' }); - this._noInstallFlag = this.defineFlagParameter({ - parameterLongName: '--no-install', - parameterShortName: '-n', + this._skipUpdateFlag = this.defineFlagParameter({ + parameterLongName: '--skip-update', + parameterShortName: '-s', description: 'If specified, the "rush update" command will not be run after updating the' + ' package.json files.' }); @@ -80,7 +90,7 @@ export class AddAction extends BaseRushAction { public run(): Promise { const project: RushConfigurationProject | undefined - = this.rushConfiguration.getCurrentProjectFromPath(process.cwd()); + = this.rushConfiguration.getProjectForPath(process.cwd()); if (!project) { return Promise.reject(new Error('Not currently in a project folder')); @@ -96,7 +106,7 @@ export class AddAction extends BaseRushAction { initialVersion: this._versionSpecifier.value, devDependency: this._devDependencyFlag.value, updateOtherPackages: this._makeConsistentFlag.value, - skipInstall: this._noInstallFlag.value, + skipUpdate: this._skipUpdateFlag.value, debugInstall: this.parser.isDebug, rangeStyle: this._caretFlag.value ? SemVerStyle.Caret : (this._exactFlag.value ? SemVerStyle.Exact : SemVerStyle.Tilde) diff --git a/apps/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap b/apps/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap index ab769156139..b381021e5bf 100644 --- a/apps/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap +++ b/apps/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap @@ -53,11 +53,18 @@ For detailed help about a specific command, use: rush -h `; exports[`CommandLineHelp prints the help for "rush add" 1`] = ` -"usage: rush add [-h] -p PACKAGE_NAME [-v VERSION_RANGE] [--exact] [--caret] - [--dev] [-c] [-n] +"usage: rush add [-h] -p PACKAGE_NAME [-v VERSION_RANGE] [-e] [-c] [-d] [-m] + [-s] -Blah. +Adds a dependency on a certain package to the current project (detected using +the current working directory) and then runs rush update. If no version is +specified, a version will be automatically detected (typically either the +latest version or a version that won't break the ensureConsistentVersions +policy). If a version range is specified, the latest version in the range +will be used. The version will be automatically prepended with a tilde, +unless the --exact or --caret flags are used. The --make-consistent flag can +be used to update all packages with the dependency. Optional arguments: -h, --help Show this help message and exit. @@ -65,18 +72,21 @@ Optional arguments: (Required) The name of the package which should be added as a dependency -v VERSION_RANGE, --version VERSION_RANGE - --exact If specified, the version specifier inserted into the + An optional version specifier. If specified, the + largest version satisfying this range will be added + to the package.json. + -e, --exact If specified, the SemVer specifier added to the package.json will be a locked, exact version. - --caret If specified, the version specifier inserted into the + -c, --caret If specified, the SemVer specifier added to the package.json will be a prepended with a \\"caret\\" specifier (\\"^\\"). - --dev If specified, the package will be added as a + -d, --dev If specified, the package will be added as a \\"devDependency\\" to the package.json - -c, --make-consistent + -m, --make-consistent If specified, other packages with this dependency - will have their package.json files updated to point - at the specified depdendency - -n, --no-install If specified, the \\"rush update\\" command will not be + will have their package.json files updated to use the + same version of the dependency. + -s, --skip-update If specified, the \\"rush update\\" command will not be run after updating the package.json files. " `; diff --git a/apps/rush-lib/src/logic/DependencyIntegrator.ts b/apps/rush-lib/src/logic/DependencyIntegrator.ts index c8bbc78611d..218fa477089 100644 --- a/apps/rush-lib/src/logic/DependencyIntegrator.ts +++ b/apps/rush-lib/src/logic/DependencyIntegrator.ts @@ -15,36 +15,95 @@ import { VersionMismatchFinder } from '../api/VersionMismatchFinder'; import { PurgeManager } from './PurgeManager'; import { Utilities } from '../utilities/Utilities'; +/** + * The type of SemVer range specifier that is prepended to the version + */ export const enum SemVerStyle { Exact = 'exact', Caret = 'caret', Tilde = 'tilde' } +/** + * The type of dependency that this is. Note: we don't support PeerDependencies + */ export const enum DependencyKind { DevDependency = 'devDependency', Dependency = 'dependency' } +/** + * Configuration options for adding or updating a dependency in a single project + */ export interface IUpdateProjectOptions { + /** + * The project which will have its package.json updated + */ project: RushConfigurationProject; + /** + * The name of the dependency to be added or updated in the project + */ packageName: string; + /** + * The new SemVer specifier that should be added to the project's package.json + */ newVersion: string; + /** + * The type of dependency that should be updated. If left empty, this will be auto-detected. + * If it cannot be auto-detected an exception will be thrown. + */ dependencyKind?: DependencyKind; + /** + * If specified, the package.json will only be updated in memory, but the changes will not + * be written to disk. + */ doNotSave?: boolean; } +/** + * Options for adding a dependency to a particular project. + */ export interface IDependencyIntegratorOptions { + /** + * The project whose package.json should get updated + */ currentProject: RushConfigurationProject; + /** + * The name of the dependency to be added + */ packageName: string; + /** + * The initial version specifier. + * If undefined, the latest version will be used (that doesn't break ensureConsistentVersions). + * If specified, the latest version meeting the SemVer specifier will be used as the basis. + */ initialVersion: string | undefined; + /** + * Whether or not this dependency should be added as a devDependency or a regular dependency. + */ devDependency: boolean; + /** + * If specified, other packages that use this dependency will also have their package.json's updated. + */ updateOtherPackages: boolean; - skipInstall: boolean; + /** + * If specified, "rush update" will not be run after updating the package.json file(s). + */ + skipUpdate: boolean; + /** + * If specified, "rush update" will be run in debug mode. + */ debugInstall: boolean; + /** + * The style of range that should be used if the version is automatically detected. + */ rangeStyle: SemVerStyle; } +/** + * A helper class for managing the dependencies of various package.json files. + * @internal + */ export class DependencyIntegrator { private _rushConfiguration: RushConfiguration; @@ -52,6 +111,9 @@ export class DependencyIntegrator { this._rushConfiguration = rushConfiguration; } + /** + * Adds a dependency to a particular project. The core business logic for "rush add". + */ public run(options: IDependencyIntegratorOptions): Promise { const { currentProject, @@ -59,7 +121,7 @@ export class DependencyIntegrator { initialVersion, devDependency, updateOtherPackages, - skipInstall, + skipUpdate, debugInstall, rangeStyle } = options; @@ -116,7 +178,7 @@ export class DependencyIntegrator { this.updateProjects(packageUpdates); - if (skipInstall) { + if (skipUpdate) { return Promise.resolve(); } @@ -143,12 +205,18 @@ export class DependencyIntegrator { }); } + /** + * Updates several projects' package.json files + */ public updateProjects(projectUpdates: Array): void { for (const update of projectUpdates) { this.updateProject(update); } } + /** + * Updates a single project's package.json file + */ public updateProject(options: IUpdateProjectOptions): void { let { dependencyKind } = options; const { diff --git a/common/reviews/api/rush-lib.api.ts b/common/reviews/api/rush-lib.api.ts index 9e668145f56..1c49e1433db 100644 --- a/common/reviews/api/rush-lib.api.ts +++ b/common/reviews/api/rush-lib.api.ts @@ -156,9 +156,9 @@ class RushConfiguration { readonly eventHooks: EventHooks; findProjectByShorthandName(shorthandProjectName: string): RushConfigurationProject | undefined; findProjectByTempName(tempProjectName: string): RushConfigurationProject | undefined; - // (undocumented) - getCurrentProjectFromPath(currentFolderPath: string): RushConfigurationProject | undefined; getProjectByName(projectName: string): RushConfigurationProject | undefined; + // @beta + getProjectForPath(currentFolderPath: string): RushConfigurationProject | undefined; readonly gitAllowedEmailRegExps: string[]; readonly gitSampleEmail: string; readonly hotfixChangeEnabled: boolean; From 56a6ab2c6ecb9df0d8cb7e072ff34813c04dda12 Mon Sep 17 00:00:00 2001 From: Nick Pape Date: Thu, 27 Sep 2018 16:54:53 -0700 Subject: [PATCH 12/27] Refactor --- apps/rush-lib/src/api/PackageJsonEditor.ts | 219 ++++++++++++++++++ apps/rush-lib/src/api/RushConfiguration.ts | 1 - .../src/api/RushConfigurationProject.ts | 13 ++ .../rush-lib/src/api/VersionMismatchFinder.ts | 85 ++++--- .../api/test/VersionMismatchFinder.test.ts | 115 ++++----- apps/rush-lib/src/cli/actions/AddAction.ts | 25 +- .../CommandLineHelp.test.ts.snap | 8 +- ...ncyIntegrator.ts => PackageJsonUpdater.ts} | 138 ++++------- 8 files changed, 402 insertions(+), 202 deletions(-) create mode 100644 apps/rush-lib/src/api/PackageJsonEditor.ts rename apps/rush-lib/src/logic/{DependencyIntegrator.ts => PackageJsonUpdater.ts} (79%) diff --git a/apps/rush-lib/src/api/PackageJsonEditor.ts b/apps/rush-lib/src/api/PackageJsonEditor.ts new file mode 100644 index 00000000000..c0b8bc07298 --- /dev/null +++ b/apps/rush-lib/src/api/PackageJsonEditor.ts @@ -0,0 +1,219 @@ +import * as semver from 'semver'; + +import { + IPackageJson, + JsonFile +} from '@microsoft/node-core-library'; + +export const enum DependencyType { + Dependency = 'dependency', + DevDependency = 'devDependency', + OptionalDependency = 'optionalDependency', + PeerOnly = 'peerDependency' +} + +export class Dependency { + private _type: DependencyType; + private _name: string; + private _version: string | undefined; + private _peerVersion: string | undefined; + private _onChange: () => void; + + public constructor(name: string, + version: string | undefined, + type: DependencyType, + peerVersion: string | undefined, + onChange: () => void) { + this._name = name; + this._version = version; + this._type = type; + this._peerVersion = peerVersion; + this._onChange = onChange; + + if (this._version && this._type === DependencyType.PeerOnly) { + throw new Error(`Cannot specify a primary version if the dependency type is peer-only.`); + } + if (!this._peerVersion && this._type === DependencyType.PeerOnly) { + throw new Error(`Must specify a peer version if the dependency type if peer-only.`); + } + } + + public get name(): string { + return this._name; + } + + public get version(): string | undefined { + return this._version; + } + + public setVersion(newVersion: string): void { + if (!semver.valid(newVersion) && !semver.validRange(newVersion)) { + throw new Error(`Cannot set version to invalid value: "${newVersion}"`); + } + this._version = newVersion; + this._onChange(); + } + + public get dependencyType(): DependencyType { + return this._type; + } + + public setDependencyType(newType: DependencyType): void { + this._type = newType; + this._onChange(); + } + + public get peerVersion(): string | undefined { + return this._peerVersion; + } +} + +export class PackageJsonEditor { + private readonly _filepath: string; + private readonly _data: IPackageJson; + private readonly _dependencies: Map; + + private _onChange: () => void; + private _modified: boolean; + + public static load(filepath: string): PackageJsonEditor { + return new PackageJsonEditor(filepath, JsonFile.load(filepath)); + } + + public static fromObject(object: IPackageJson, filename: string): PackageJsonEditor { + return new PackageJsonEditor(filename, object); + } + + public get name(): string { + return this._data.name; + } + + public get version(): string { + return this._data.version; + } + + public get filePath(): string { + return this._filepath; + } + + public getDependency(packageName: string): Dependency | undefined { + return this._dependencies.get(packageName); + } + + public forEachDependency(cb: (dependency: Dependency) => void): void { + this._dependencies.forEach(cb); + } + + public addOrUpdateDependency(packageName: string, newVersion: string, dependencyType: DependencyType): void { + if (this._dependencies.has(packageName)) { + const dependency: Dependency = this._dependencies.get(packageName)!; + dependency.setVersion(newVersion); + dependency.setDependencyType(dependencyType); + } else { + const dependency: Dependency + = new Dependency(packageName, newVersion, dependencyType, undefined, this._onChange); + this._dependencies.set(packageName, dependency); + } + } + + public saveIfModified(): boolean { + if (this._modified) { + JsonFile.save(this._normalize(), this._filepath); + this._modified = false; + return true; + } + return false; + } + + private constructor(filepath: string, data: IPackageJson) { + this._filepath = filepath; + this._data = data; + + this._dependencies = new Map(); + + const dependencies: { [key: string]: string } = data.dependencies || {}; + const devDependencies: { [key: string]: string } = data.devDependencies || {}; + const optionalDependencies: { [key: string]: string } = data.optionalDependencies || {}; + const peerDependencies: { [key: string]: string } = data.peerDependencies || {}; + + this._onChange = () => { + this._modified = true; + }; + + Object.keys(dependencies || {}).forEach((dependency: string) => { + if (devDependencies[dependency]) { + throw new Error(`The package "${dependency}" is listed as both a dev and a regular dependency`); + } + if (optionalDependencies[dependency]) { + throw new Error(`The package "${dependency}" is listed as both a dev and a regular dependency`); + } + + this._dependencies.set(dependency, new Dependency(dependency, dependencies[dependency], + DependencyType.Dependency, peerDependencies[dependency], this._onChange)); + }); + + Object.keys(devDependencies || {}).forEach((dependency: string) => { + if (optionalDependencies[dependency]) { + throw new Error(`The package "${dependency}" is listed as both a dev and an optional dependency`); + } + + this._dependencies.set(dependency, new Dependency(dependency, devDependencies[dependency], + DependencyType.Dependency, peerDependencies[dependency], this._onChange)); + }); + + Object.keys(optionalDependencies || {}).forEach((dependency: string) => { + this._dependencies.set(dependency, new Dependency(dependency, optionalDependencies[dependency], + DependencyType.OptionalDependency, peerDependencies[dependency], this._onChange)); + }); + + Object.keys(peerDependencies || {}).forEach((dependency: string) => { + if (!this._dependencies.has(dependency)) { + this._dependencies.set(dependency, new Dependency(dependency, undefined, + DependencyType.PeerOnly, peerDependencies[dependency], this._onChange)); + } + }); + } + + private _normalize(): IPackageJson { + delete this._data.dependencies; + delete this._data.devDependencies; + delete this._data.peerDependencies; + delete this._data.optionalDependencies; + + const keys: Array = [...this._dependencies.keys()].sort(); + + for (const packageName of keys) { + const dependency: Dependency = this._dependencies.get(packageName)!; + + if (dependency.dependencyType === DependencyType.Dependency) { + if (!this._data.dependencies) { + this._data.dependencies = {}; + } + this._data.dependencies[dependency.name] = dependency.version!; + } + + if (dependency.dependencyType === DependencyType.DevDependency) { + if (!this._data.devDependencies) { + this._data.devDependencies = {}; + } + this._data.devDependencies[dependency.name] = dependency.version!; + } + + if (dependency.dependencyType === DependencyType.OptionalDependency) { + if (!this._data.optionalDependencies) { + this._data.optionalDependencies = {}; + } + this._data.optionalDependencies[dependency.name] = dependency.version!; + } + + if (dependency.peerVersion) { + if (!this._data.peerDependencies) { + this._data.peerDependencies = {}; + } + this._data.peerDependencies[dependency.name] = dependency.peerVersion; + } + } + + return this._data; + } +} \ No newline at end of file diff --git a/apps/rush-lib/src/api/RushConfiguration.ts b/apps/rush-lib/src/api/RushConfiguration.ts index 515e28038f9..c5aa9f106b0 100644 --- a/apps/rush-lib/src/api/RushConfiguration.ts +++ b/apps/rush-lib/src/api/RushConfiguration.ts @@ -752,7 +752,6 @@ export class RushConfiguration { /** * Returns the project for which the specified path is underneath that project's folder. * If the path is not under any project's folder, returns undefined. - * @beta */ public getProjectForPath(currentFolderPath: string): RushConfigurationProject | undefined { const resolvedPath: string = path.resolve(currentFolderPath); diff --git a/apps/rush-lib/src/api/RushConfigurationProject.ts b/apps/rush-lib/src/api/RushConfigurationProject.ts index e5e2eac9ce7..f64b12c4da9 100644 --- a/apps/rush-lib/src/api/RushConfigurationProject.ts +++ b/apps/rush-lib/src/api/RushConfigurationProject.ts @@ -12,6 +12,7 @@ import { import { RushConfiguration } from '../api/RushConfiguration'; import { VersionPolicy, LockStepVersionPolicy } from './VersionPolicy'; +import { PackageJsonEditor } from './PackageJsonEditor'; /** * This represents the JSON data object for a project entry in the rush.json configuration file. @@ -37,6 +38,7 @@ export class RushConfigurationProject { private _projectRelativeFolder: string; private _reviewCategory: string; private _packageJson: IPackageJson; + private _packageJsonEditor: PackageJsonEditor; private _tempProjectName: string; private _unscopedTempProjectName: string; private _cyclicDependencyProjects: Set; @@ -97,6 +99,8 @@ export class RushConfigurationProject { + ` match the name "${this._packageJson.name}" from package.json`); } + this._packageJsonEditor = PackageJsonEditor.load(packageJsonFilename); + this._tempProjectName = tempProjectName; // The "rushProject.tempProjectName" is guaranteed to be unique name (e.g. by adding the "-2" @@ -172,11 +176,20 @@ export class RushConfigurationProject { /** * The parsed NPM "package.json" file from projectFolder. + * Will be deprecated soon in favor of packageJsonEditor */ public get packageJson(): IPackageJson { return this._packageJson; } + /** + * A useful wrapper around the package.json file for making modifications + * @alpha + */ + public get packageJsonEditor(): PackageJsonEditor { + return this._packageJsonEditor; + } + /** * The unique name for the temporary project that will be generated in the Common folder. * For example, if the project name is "@scope/MyProject", the temporary project name diff --git a/apps/rush-lib/src/api/VersionMismatchFinder.ts b/apps/rush-lib/src/api/VersionMismatchFinder.ts index 30c8fd58eff..80c7f3146e7 100644 --- a/apps/rush-lib/src/api/VersionMismatchFinder.ts +++ b/apps/rush-lib/src/api/VersionMismatchFinder.ts @@ -6,6 +6,7 @@ import * as colors from 'colors'; import { RushConfiguration } from './RushConfiguration'; import { RushConfigurationProject } from './RushConfigurationProject'; import { RushConstants } from '../logic/RushConstants'; +import { Dependency, DependencyType, PackageJsonEditor } from './PackageJsonEditor'; /** * @public @@ -44,7 +45,8 @@ export class VersionMismatchFinder { projects.push({ packageName: 'preferred versions from ' + RushConstants.commonVersionsFilename, - packageJson: { dependencies: allPreferredVersions } + packageJsonEditor: PackageJsonEditor.fromObject( + { dependencies: allPreferredVersions } as any, 'preferred-versions.json') // tslint:disable-line:no-any } as RushConfigurationProject); return new VersionMismatchFinder( @@ -61,17 +63,7 @@ export class VersionMismatchFinder { const mismatchFinder: VersionMismatchFinder = VersionMismatchFinder.getMismatches(rushConfiguration); - // Iterate over the list. For any dependency with mismatching versions, print the projects - mismatchFinder.getMismatches().forEach((dependency: string) => { - console.log(colors.yellow(dependency)); - mismatchFinder.getVersionsOfMismatch(dependency)!.forEach((version: string) => { - console.log(` ${version}`); - mismatchFinder.getConsumersOfMismatch(dependency, version)!.forEach((project: string) => { - console.log(` - ${project}`); - }); - }); - console.log(); - }); + mismatchFinder.print(); if (mismatchFinder.numberOfMismatches) { console.log(colors.red(`Found ${mismatchFinder.numberOfMismatches} mis-matching dependencies!`)); @@ -115,6 +107,20 @@ export class VersionMismatchFinder { return mismatchedVersion; } + public print(): void { + // Iterate over the list. For any dependency with mismatching versions, print the projects + this.getMismatches().forEach((dependency: string) => { + console.log(colors.yellow(dependency)); + this.getVersionsOfMismatch(dependency)!.forEach((version: string) => { + console.log(` ${version}`); + this.getConsumersOfMismatch(dependency, version)!.forEach((project: string) => { + console.log(` - ${project}`); + }); + }); + console.log(); + }); + } + private _analyze(): void { this._projects.forEach((project: RushConfigurationProject) => { if (!project.skipRushCheck) { @@ -125,12 +131,28 @@ export class VersionMismatchFinder { // patterns consistent, but on the other hand different projects may have different // levels of compatibility -- we should wait for someone to actually request this feature // before we get into that.) - this._addDependenciesToList(project.packageName, - project.packageJson.dependencies, project.cyclicDependencyProjects); - this._addDependenciesToList(project.packageName, - project.packageJson.devDependencies, project.cyclicDependencyProjects); - this._addDependenciesToList(project.packageName, - project.packageJson.optionalDependencies, project.cyclicDependencyProjects); + project.packageJsonEditor.forEachDependency((dependency: Dependency) => { + if (dependency.dependencyType !== DependencyType.PeerOnly + && !project.cyclicDependencyProjects.has(dependency.name)) { + + const version: string = dependency.version!; + + if (this._isVersionAllowedAlternative(dependency.name, version)) { + return; + } + + if (!this._mismatches.has(dependency.name)) { + this._mismatches.set(dependency.name, new Map()); + } + + const dependencyVersions: Map = this._mismatches.get(dependency.name)!; + + if (!dependencyVersions.has(version)) { + dependencyVersions.set(version, []); + } + dependencyVersions.get(version)!.push(project.packageName); + } + }); } }); @@ -141,33 +163,6 @@ export class VersionMismatchFinder { }); } - private _addDependenciesToList( - project: string, - dependencyMap: { [dependency: string]: string } | undefined, - exclude: Set): void { - - if (dependencyMap) { - Object.keys(dependencyMap).forEach((dependency: string) => { - if (!exclude || !exclude.has(dependency)) { - const version: string = dependencyMap[dependency]; - if (this._isVersionAllowedAlternative(dependency, version)) { - return; - } - if (!this._mismatches.has(dependency)) { - this._mismatches.set(dependency, new Map()); - } - - const dependencyVersions: Map = this._mismatches.get(dependency)!; - - if (!dependencyVersions.has(version)) { - dependencyVersions.set(version, []); - } - dependencyVersions.get(version)!.push(project); - } - }); - } - } - private _isVersionAllowedAlternative( dependency: string, version: string): boolean { diff --git a/apps/rush-lib/src/api/test/VersionMismatchFinder.test.ts b/apps/rush-lib/src/api/test/VersionMismatchFinder.test.ts index d4d520ea31f..cd85f70f219 100644 --- a/apps/rush-lib/src/api/test/VersionMismatchFinder.test.ts +++ b/apps/rush-lib/src/api/test/VersionMismatchFinder.test.ts @@ -3,31 +3,33 @@ import { RushConfigurationProject } from '../RushConfigurationProject'; import { VersionMismatchFinder } from '../VersionMismatchFinder'; +import { PackageJsonEditor } from '../PackageJsonEditor'; +// tslint:disable:no-any describe('VersionMismatchFinder', () => { it('finds no mismatches if there are none', (done: jest.DoneCallback) => { const projects: RushConfigurationProject[] = [ { packageName: 'A', - packageJson: { + packageJsonEditor: PackageJsonEditor.fromObject({ dependencies: { '@types/foo': '1.2.3', 'karma': '0.0.1' } - }, + } as any, 'foo.json'), cyclicDependencyProjects: new Set() }, { packageName: 'B', - packageJson: { + packageJsonEditor: PackageJsonEditor.fromObject({ dependencies: { '@types/foo': '1.2.3', 'karma': '0.0.1' } - }, + } as any, 'foo.json'), cyclicDependencyProjects: new Set() } - ] as any as RushConfigurationProject[]; // tslint:disable-line:no-any + ] as any as RushConfigurationProject[]; const mismatchFinder: VersionMismatchFinder = new VersionMismatchFinder(projects); expect(mismatchFinder.numberOfMismatches).toEqual(0); expect(mismatchFinder.getMismatches().length).toEqual(0); @@ -38,25 +40,25 @@ describe('VersionMismatchFinder', () => { const projects: RushConfigurationProject[] = [ { packageName: 'A', - packageJson: { + packageJsonEditor: PackageJsonEditor.fromObject({ dependencies: { '@types/foo': '1.2.3', 'karma': '0.0.1' } - }, + } as any, 'foo.json'), cyclicDependencyProjects: new Set() }, { packageName: 'B', - packageJson: { + packageJsonEditor: PackageJsonEditor.fromObject({ dependencies: { '@types/foo': '2.0.0', 'karma': '0.0.1' } - }, + } as any, 'foo.json'), cyclicDependencyProjects: new Set() } - ] as any as RushConfigurationProject[]; // tslint:disable-line:no-any + ] as any as RushConfigurationProject[]; const mismatchFinder: VersionMismatchFinder = new VersionMismatchFinder(projects); expect(mismatchFinder.numberOfMismatches).toEqual(1); expect(mismatchFinder.getMismatches().length).toEqual(1); @@ -71,25 +73,25 @@ describe('VersionMismatchFinder', () => { const projects: RushConfigurationProject[] = [ { packageName: 'A', - packageJson: { + packageJsonEditor: PackageJsonEditor.fromObject({ dependencies: { '@types/foo': '1.2.3', 'karma': '0.0.1' } - }, + } as any, 'foo.json'), cyclicDependencyProjects: new Set(['@types/foo']) }, { packageName: 'B', - packageJson: { + packageJsonEditor: PackageJsonEditor.fromObject({ dependencies: { '@types/foo': '2.0.0', 'karma': '0.0.1' } - }, + } as any, 'foo.json'), cyclicDependencyProjects: new Set() } - ] as any as RushConfigurationProject[]; // tslint:disable-line:no-any + ] as any as RushConfigurationProject[]; const mismatchFinder: VersionMismatchFinder = new VersionMismatchFinder(projects); expect(mismatchFinder.numberOfMismatches).toEqual(0); expect(mismatchFinder.getMismatches().length).toEqual(0); @@ -100,25 +102,25 @@ describe('VersionMismatchFinder', () => { const projects: RushConfigurationProject[] = [ { packageName: 'A', - packageJson: { + packageJsonEditor: PackageJsonEditor.fromObject({ dependencies: { '@types/foo': '1.2.3', 'karma': '0.0.1' } - }, + } as any, 'foo.json'), cyclicDependencyProjects: new Set() }, { packageName: 'B', - packageJson: { + packageJsonEditor: PackageJsonEditor.fromObject({ dependencies: { '@types/foo': '2.0.0', 'karma': '0.0.1' } - }, + } as any, 'foo.json'), cyclicDependencyProjects: new Set() } - ] as any as RushConfigurationProject[]; // tslint:disable-line:no-any + ] as any as RushConfigurationProject[]; const mismatchFinder: VersionMismatchFinder = new VersionMismatchFinder(projects); expect(mismatchFinder.getVersionsOfMismatch('@types/foobar')).toEqual(undefined); expect(mismatchFinder.getConsumersOfMismatch('@types/fobar', '2.0.0')).toEqual(undefined); @@ -130,45 +132,45 @@ describe('VersionMismatchFinder', () => { const projects: RushConfigurationProject[] = [ { packageName: 'A', - packageJson: { + packageJsonEditor: PackageJsonEditor.fromObject({ dependencies: { '@types/foo': '1.2.3', 'karma': '0.0.1' } - }, + } as any, 'foo.json'), cyclicDependencyProjects: new Set() }, { packageName: 'B', - packageJson: { + packageJsonEditor: PackageJsonEditor.fromObject({ dependencies: { '@types/foo': '2.0.0', 'karma': '0.0.1' } - }, + } as any, 'foo.json'), cyclicDependencyProjects: new Set() }, { packageName: 'C', - packageJson: { + packageJsonEditor: PackageJsonEditor.fromObject({ dependencies: { 'mocha': '1.2.3', 'karma': '0.0.1' } - }, + } as any, 'foo.json'), cyclicDependencyProjects: new Set() }, { packageName: 'D', - packageJson: { + packageJsonEditor: PackageJsonEditor.fromObject({ dependencies: { 'mocha': '2.0.0', 'karma': '0.0.1' } - }, + } as any, 'foo.json'), cyclicDependencyProjects: new Set() } - ] as any as RushConfigurationProject[]; // tslint:disable-line:no-any + ] as any as RushConfigurationProject[]; const mismatchFinder: VersionMismatchFinder = new VersionMismatchFinder(projects); expect(mismatchFinder.numberOfMismatches).toEqual(2); expect(mismatchFinder.getMismatches().length).toEqual(2); @@ -186,35 +188,35 @@ describe('VersionMismatchFinder', () => { const projects: RushConfigurationProject[] = [ { packageName: 'A', - packageJson: { + packageJsonEditor: PackageJsonEditor.fromObject({ dependencies: { '@types/foo': '1.2.3', 'karma': '0.0.1' } - }, + } as any, 'foo.json'), cyclicDependencyProjects: new Set() }, { packageName: 'B', - packageJson: { + packageJsonEditor: PackageJsonEditor.fromObject({ dependencies: { '@types/foo': '2.0.0', 'karma': '0.0.1' } - }, + } as any, 'foo.json'), cyclicDependencyProjects: new Set() }, { packageName: 'C', - packageJson: { + packageJsonEditor: PackageJsonEditor.fromObject({ dependencies: { '@types/foo': '9.9.9', 'karma': '0.0.1' } - }, + } as any, 'foo.json'), cyclicDependencyProjects: new Set() } - ] as any as RushConfigurationProject[]; // tslint:disable-line:no-any + ] as any as RushConfigurationProject[]; const mismatchFinder: VersionMismatchFinder = new VersionMismatchFinder(projects); expect(mismatchFinder.numberOfMismatches).toEqual(1); expect(mismatchFinder.getMismatches().length).toEqual(1); @@ -230,26 +232,27 @@ describe('VersionMismatchFinder', () => { const projects: RushConfigurationProject[] = [ { packageName: 'A', - packageJson: { + packageJsonEditor: PackageJsonEditor.fromObject({ dependencies: { '@types/foo': '1.2.3', 'karma': '0.0.1' } - }, + } as any, 'foo.json'), cyclicDependencyProjects: new Set() }, { packageName: 'B', - packageJson: { + packageJsonEditor: PackageJsonEditor.fromObject({ devDependencies: { '@types/foo': '2.0.0', 'karma': '0.0.1' } - }, + } as any, 'foo.json'), cyclicDependencyProjects: new Set() } - ] as any as RushConfigurationProject[]; // tslint:disable-line:no-any + ] as any as RushConfigurationProject[]; const mismatchFinder: VersionMismatchFinder = new VersionMismatchFinder(projects); + expect(mismatchFinder.numberOfMismatches).toEqual(1); expect(mismatchFinder.getMismatches().length).toEqual(1); expect(mismatchFinder.getMismatches()[0]).toEqual('@types/foo'); @@ -263,25 +266,25 @@ describe('VersionMismatchFinder', () => { const projects: RushConfigurationProject[] = [ { packageName: 'A', - packageJson: { + packageJsonEditor: PackageJsonEditor.fromObject({ dependencies: { '@types/foo': '1.2.3', 'karma': '0.0.1' } - }, + } as any, 'foo.json'), cyclicDependencyProjects: new Set() }, { packageName: 'B', - packageJson: { + packageJsonEditor: PackageJsonEditor.fromObject({ peerDependencies: { '@types/foo': '2.0.0', 'karma': '0.0.1' } - }, + } as any, 'foo.json'), cyclicDependencyProjects: new Set() } - ] as any as RushConfigurationProject[]; // tslint:disable-line:no-any + ] as any as RushConfigurationProject[]; const mismatchFinder: VersionMismatchFinder = new VersionMismatchFinder(projects); expect(mismatchFinder.numberOfMismatches).toEqual(0); done(); @@ -291,25 +294,25 @@ describe('VersionMismatchFinder', () => { const projects: RushConfigurationProject[] = [ { packageName: 'A', - packageJson: { + packageJsonEditor: PackageJsonEditor.fromObject({ dependencies: { '@types/foo': '1.2.3', 'karma': '0.0.1' } - }, + } as any, 'foo.json'), cyclicDependencyProjects: new Set() }, { packageName: 'B', - packageJson: { + packageJsonEditor: PackageJsonEditor.fromObject({ optionalDependencies: { '@types/foo': '2.0.0', 'karma': '0.0.1' } - }, + } as any, 'foo.json'), cyclicDependencyProjects: new Set() } - ] as any as RushConfigurationProject[]; // tslint:disable-line:no-any + ] as any as RushConfigurationProject[]; const mismatchFinder: VersionMismatchFinder = new VersionMismatchFinder(projects); expect(mismatchFinder.numberOfMismatches).toEqual(1); expect(mismatchFinder.getMismatches().length).toEqual(1); @@ -324,25 +327,25 @@ describe('VersionMismatchFinder', () => { const projects: RushConfigurationProject[] = [ { packageName: 'A', - packageJson: { + packageJsonEditor: PackageJsonEditor.fromObject({ dependencies: { '@types/foo': '1.2.3', 'karma': '0.0.1' } - }, + } as any, 'foo.json'), cyclicDependencyProjects: new Set() }, { packageName: 'B', - packageJson: { + packageJsonEditor: PackageJsonEditor.fromObject({ dependencies: { '@types/foo': '2.0.0', 'karma': '0.0.1' } - }, + } as any, 'foo.json'), cyclicDependencyProjects: new Set() } - ] as any as RushConfigurationProject[]; // tslint:disable-line:no-any + ] as any as RushConfigurationProject[]; const alternatives: Map> = new Map>(); alternatives.set('@types/foo', ['2.0.0']); const mismatchFinder: VersionMismatchFinder = new VersionMismatchFinder(projects, alternatives); diff --git a/apps/rush-lib/src/cli/actions/AddAction.ts b/apps/rush-lib/src/cli/actions/AddAction.ts index e8de2fc4dc9..87516db522e 100644 --- a/apps/rush-lib/src/cli/actions/AddAction.ts +++ b/apps/rush-lib/src/cli/actions/AddAction.ts @@ -2,6 +2,7 @@ // See LICENSE in the project root for license information. import * as os from 'os'; +import * as semver from 'semver'; import { CommandLineFlagParameter, @@ -11,7 +12,8 @@ import { import { RushConfigurationProject } from '../../api/RushConfigurationProject'; import { BaseRushAction } from './BaseRushAction'; import { RushCommandLineParser } from '../RushCommandLineParser'; -import { DependencyIntegrator, SemVerStyle } from '../../logic/DependencyIntegrator'; +import { PackageJsonUpdater, SemVerStyle } from '../../logic/PackageJsonUpdater'; +import { PackageName } from '@microsoft/node-core-library'; export class AddAction extends BaseRushAction { private _exactFlag: CommandLineFlagParameter; @@ -58,13 +60,11 @@ export class AddAction extends BaseRushAction { }); this._exactFlag = this.defineFlagParameter({ parameterLongName: '--exact', - parameterShortName: '-e', description: 'If specified, the SemVer specifier added to the' + ' package.json will be a locked, exact version.' }); this._caretFlag = this.defineFlagParameter({ parameterLongName: '--caret', - parameterShortName: '-c', description: 'If specified, the SemVer specifier added to the' + ' package.json will be a prepended with a "caret" specifier ("^").' }); @@ -93,17 +93,28 @@ export class AddAction extends BaseRushAction { = this.rushConfiguration.getProjectForPath(process.cwd()); if (!project) { - return Promise.reject(new Error('Not currently in a project folder')); + return Promise.reject(new Error('The "rush add" command must be invoked under a project' + + ' folder that is registered in rush.json.')); } if (this._caretFlag.value && this._exactFlag.value) { return Promise.reject(new Error('Only one of --caret and --exact should be specified')); } - return new DependencyIntegrator(this.rushConfiguration).run({ + const packageName: string = this._packageName.value!; + if (!PackageName.isValidName(packageName)) { + return Promise.reject(new Error(`The package name "${packageName}" is not valid.`)); + } + + const version: string | undefined = this._versionSpecifier.value; + if (version && !semver.validRange(version) && !semver.valid(version)) { + return Promise.reject(new Error(`The SemVer specifier "${version}" is not valid.`)); + } + + return new PackageJsonUpdater(this.rushConfiguration).doRushAdd({ currentProject: project, - packageName: this._packageName.value!, - initialVersion: this._versionSpecifier.value, + packageName: packageName, + initialVersion: version, devDependency: this._devDependencyFlag.value, updateOtherPackages: this._makeConsistentFlag.value, skipUpdate: this._skipUpdateFlag.value, diff --git a/apps/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap b/apps/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap index b381021e5bf..509509fe1c2 100644 --- a/apps/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap +++ b/apps/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap @@ -53,8 +53,8 @@ For detailed help about a specific command, use: rush -h `; exports[`CommandLineHelp prints the help for "rush add" 1`] = ` -"usage: rush add [-h] -p PACKAGE_NAME [-v VERSION_RANGE] [-e] [-c] [-d] [-m] - [-s] +"usage: rush add [-h] -p PACKAGE_NAME [-v VERSION_RANGE] [--exact] [--caret] + [-d] [-m] [-s] Adds a dependency on a certain package to the current project (detected using @@ -75,9 +75,9 @@ Optional arguments: An optional version specifier. If specified, the largest version satisfying this range will be added to the package.json. - -e, --exact If specified, the SemVer specifier added to the + --exact If specified, the SemVer specifier added to the package.json will be a locked, exact version. - -c, --caret If specified, the SemVer specifier added to the + --caret If specified, the SemVer specifier added to the package.json will be a prepended with a \\"caret\\" specifier (\\"^\\"). -d, --dev If specified, the package will be added as a diff --git a/apps/rush-lib/src/logic/DependencyIntegrator.ts b/apps/rush-lib/src/logic/PackageJsonUpdater.ts similarity index 79% rename from apps/rush-lib/src/logic/DependencyIntegrator.ts rename to apps/rush-lib/src/logic/PackageJsonUpdater.ts index 218fa477089..844c0d1a19a 100644 --- a/apps/rush-lib/src/logic/DependencyIntegrator.ts +++ b/apps/rush-lib/src/logic/PackageJsonUpdater.ts @@ -1,19 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + import * as colors from 'colors'; -import * as path from 'path'; import * as semver from 'semver'; -import { - JsonFile, - FileConstants, - IPackageJson -} from '@microsoft/node-core-library'; - import { RushConfiguration } from '../api/RushConfiguration'; import { InstallManager, IInstallManagerOptions } from './InstallManager'; import { RushConfigurationProject } from '../api/RushConfigurationProject'; import { VersionMismatchFinder } from '../api/VersionMismatchFinder'; import { PurgeManager } from './PurgeManager'; import { Utilities } from '../utilities/Utilities'; +import { DependencyType, PackageJsonEditor, Dependency } from '../api/PackageJsonEditor'; /** * The type of SemVer range specifier that is prepended to the version @@ -24,46 +21,10 @@ export const enum SemVerStyle { Tilde = 'tilde' } -/** - * The type of dependency that this is. Note: we don't support PeerDependencies - */ -export const enum DependencyKind { - DevDependency = 'devDependency', - Dependency = 'dependency' -} - -/** - * Configuration options for adding or updating a dependency in a single project - */ -export interface IUpdateProjectOptions { - /** - * The project which will have its package.json updated - */ - project: RushConfigurationProject; - /** - * The name of the dependency to be added or updated in the project - */ - packageName: string; - /** - * The new SemVer specifier that should be added to the project's package.json - */ - newVersion: string; - /** - * The type of dependency that should be updated. If left empty, this will be auto-detected. - * If it cannot be auto-detected an exception will be thrown. - */ - dependencyKind?: DependencyKind; - /** - * If specified, the package.json will only be updated in memory, but the changes will not - * be written to disk. - */ - doNotSave?: boolean; -} - /** * Options for adding a dependency to a particular project. */ -export interface IDependencyIntegratorOptions { +export interface IPackageJsonUpdaterRushAddOptions { /** * The project whose package.json should get updated */ @@ -100,11 +61,34 @@ export interface IDependencyIntegratorOptions { rangeStyle: SemVerStyle; } +/** + * Configuration options for adding or updating a dependency in a single project + */ +export interface IUpdateProjectOptions { + /** + * The project which will have its package.json updated + */ + project: RushConfigurationProject; + /** + * The name of the dependency to be added or updated in the project + */ + packageName: string; + /** + * The new SemVer specifier that should be added to the project's package.json + */ + newVersion: string; + /** + * The type of dependency that should be updated. If left empty, this will be auto-detected. + * If it cannot be auto-detected an exception will be thrown. + */ + dependencyType?: DependencyType; +} + /** * A helper class for managing the dependencies of various package.json files. * @internal */ -export class DependencyIntegrator { +export class PackageJsonUpdater { private _rushConfiguration: RushConfiguration; public constructor(rushConfiguration: RushConfiguration) { @@ -114,7 +98,7 @@ export class DependencyIntegrator { /** * Adds a dependency to a particular project. The core business logic for "rush add". */ - public run(options: IDependencyIntegratorOptions): Promise { + public doRushAdd(options: IPackageJsonUpdaterRushAddOptions): Promise { const { currentProject, packageName, @@ -141,13 +125,11 @@ export class DependencyIntegrator { project: currentProject, packageName, newVersion: version, - dependencyKind: devDependency ? DependencyKind.DevDependency : DependencyKind.Dependency, - doNotSave: true + dependencyType: devDependency ? DependencyType.DevDependency : DependencyType.Dependency }; this.updateProject(currentProjectUpdate); - currentProjectUpdate.doNotSave = false; - const packageUpdates: Array = [currentProjectUpdate]; + const otherPackageUpdates: Array = []; if (this._rushConfiguration.ensureConsistentVersions) { // we need to do a mismatch check @@ -165,7 +147,7 @@ export class DependencyIntegrator { for (const mismatchedVersion of mismatchFinder.getVersionsOfMismatch(packageName)!) { for (const consumer of mismatchFinder.getConsumersOfMismatch(packageName, mismatchedVersion)!) { if (consumer !== currentProject.packageName) { - packageUpdates.push({ + otherPackageUpdates.push({ project: this._rushConfiguration.getProjectByName(consumer)!, packageName: packageName, newVersion: version @@ -176,7 +158,13 @@ export class DependencyIntegrator { } } - this.updateProjects(packageUpdates); + this.updateProjects(otherPackageUpdates); + + for (const project of this._rushConfiguration.projects) { + if (project.packageJsonEditor.saveIfModified()) { + console.log(colors.green('Wrote ') + project.packageJsonEditor.filePath); + } + } if (skipUpdate) { return Promise.resolve(); @@ -218,45 +206,26 @@ export class DependencyIntegrator { * Updates a single project's package.json file */ public updateProject(options: IUpdateProjectOptions): void { - let { dependencyKind } = options; + let { dependencyType } = options; const { project, packageName, - newVersion, - doNotSave + newVersion } = options; - const packageJson: IPackageJson = project.packageJson; + const packageJson: PackageJsonEditor = project.packageJsonEditor; - let oldDependencyKind: DependencyKind | undefined = undefined; - if (packageJson.dependencies && packageJson.dependencies[packageName]) { - oldDependencyKind = DependencyKind.Dependency; - } else if (packageJson.devDependencies && packageJson.devDependencies[packageName]) { - oldDependencyKind = DependencyKind.DevDependency; - } + const oldDependency: Dependency | undefined = packageJson.getDependency(packageName); + const oldDependencyType: DependencyType | undefined = oldDependency ? oldDependency.dependencyType : undefined; - if (!dependencyKind && !oldDependencyKind) { + if (!dependencyType && !oldDependencyType) { throw new Error(`Cannot auto-detect dependency type of "${packageName}" for project "${project.packageName}"`); } - if (!dependencyKind) { - dependencyKind = oldDependencyKind; + if (!dependencyType) { + dependencyType = oldDependencyType; } - // update the dependency - if (dependencyKind === DependencyKind.Dependency) { - packageJson.dependencies = this._updateDependency(packageJson.dependencies, packageName, newVersion); - } else if (dependencyKind === DependencyKind.DevDependency) { - packageJson.devDependencies = this._updateDependency(packageJson.devDependencies, packageName, newVersion); - } - - if (!doNotSave) { - // overwrite existing file - const packageJsonPath: string - = path.join(project.projectFolder, FileConstants.PackageJson); - JsonFile.save(project.packageJson, packageJsonPath); - - console.log(colors.green('Wrote ') + packageJsonPath); - } + packageJson.addOrUpdateDependency(packageName, newVersion, dependencyType!); } private _getNormalizedVersionSpec( @@ -343,13 +312,4 @@ export class DependencyIntegrator { return '~' + selectedVersion!; } } - - private _updateDependency(dependencies: { [key: string]: string } | undefined, - packageName: string, version: string): { [key: string]: string } { - if (!dependencies) { - dependencies = {}; - } - dependencies[packageName] = version!; - return dependencies; - } } \ No newline at end of file From ee299b19ffc5f245bf6eb3c1b3367f2b7052681c Mon Sep 17 00:00:00 2001 From: Nick Pape Date: Thu, 27 Sep 2018 18:45:51 -0700 Subject: [PATCH 13/27] Random comments --- .../rush-lib/src/api/VersionMismatchFinder.ts | 4 ++ apps/rush-lib/src/cli/actions/AddAction.ts | 26 ++++------- apps/rush-lib/src/logic/PackageJsonUpdater.ts | 45 +++++++++++++------ common/reviews/api/rush-lib.api.ts | 4 +- 4 files changed, 47 insertions(+), 32 deletions(-) diff --git a/apps/rush-lib/src/api/VersionMismatchFinder.ts b/apps/rush-lib/src/api/VersionMismatchFinder.ts index 80c7f3146e7..876d3fbd138 100644 --- a/apps/rush-lib/src/api/VersionMismatchFinder.ts +++ b/apps/rush-lib/src/api/VersionMismatchFinder.ts @@ -31,6 +31,10 @@ export class VersionMismatchFinder { VersionMismatchFinder._checkForInconsistentVersions(rushConfiguration, false); } + /** + * Populates a version mismatch finder object given a Rush Configuration. + * Intentionally considers preferred versions. + */ public static getMismatches(rushConfiguration: RushConfiguration): VersionMismatchFinder { // Collect all the preferred versions into a single table const allPreferredVersions: { [dependency: string]: string } = {}; diff --git a/apps/rush-lib/src/cli/actions/AddAction.ts b/apps/rush-lib/src/cli/actions/AddAction.ts index 87516db522e..882c8f5a72b 100644 --- a/apps/rush-lib/src/cli/actions/AddAction.ts +++ b/apps/rush-lib/src/cli/actions/AddAction.ts @@ -26,13 +26,12 @@ export class AddAction extends BaseRushAction { constructor(parser: RushCommandLineParser) { const documentation: string[] = [ - 'Adds a dependency on a certain package to the current project (detected using the current' - + ' working directory) and then runs rush update. If no version is specified, a version will' - + ' be automatically detected (typically either the latest version or a version that won\'t break' - + ' the ensureConsistentVersions policy). If a version range is specified, the latest version' - + ' in the range will be used. The version will be automatically prepended with a tilde, unless' - + ' the --exact or --caret flags are used. The --make-consistent flag can be used to update' - + ' all packages with the dependency.' + 'Adds a specified package as a dependency of the current project (as determined by the current working directory)' + + ' and then runs "rush update".If no version is specified, a version will be automatically detected (typically' + + ' either the latest version or a version that won\'t break the ensureConsistentVersions policy). If a version' + + ' range is specified, the latest version in the range will be used. The version will be automatically prepended' + + ' with a tilde, unless the "--exact" or "--caret" flags are used. The "--make-consistent" flag can be used to' + + ' update all packages with the dependency.' ]; super({ actionName: 'add', @@ -49,19 +48,13 @@ export class AddAction extends BaseRushAction { parameterShortName: '-p', required: true, argumentName: 'PACKAGE_NAME', - description: '(Required) The name of the package which should be added as a dependency' - }); - this._versionSpecifier = this.defineStringParameter({ - parameterLongName: '--version', - parameterShortName: '-v', - argumentName: 'VERSION_RANGE', - description: 'An optional version specifier. If specified, the largest version satisfying this range' - + ' will be added to the package.json.' + description: '(Required) The name of the package which should be added as a dependency.' + + ' Also, the version specifier can be appended after an "@" sign (similar to NPM\'s semantics).' }); this._exactFlag = this.defineFlagParameter({ parameterLongName: '--exact', description: 'If specified, the SemVer specifier added to the' - + ' package.json will be a locked, exact version.' + + ' package.json will be an exact version (e.g. without tilde or caret).' }); this._caretFlag = this.defineFlagParameter({ parameterLongName: '--caret', @@ -70,7 +63,6 @@ export class AddAction extends BaseRushAction { }); this._devDependencyFlag = this.defineFlagParameter({ parameterLongName: '--dev', - parameterShortName: '-d', description: 'If specified, the package will be added as a "devDependency"' + ' to the package.json' }); diff --git a/apps/rush-lib/src/logic/PackageJsonUpdater.ts b/apps/rush-lib/src/logic/PackageJsonUpdater.ts index 844c0d1a19a..2ad3097a975 100644 --- a/apps/rush-lib/src/logic/PackageJsonUpdater.ts +++ b/apps/rush-lib/src/logic/PackageJsonUpdater.ts @@ -118,7 +118,7 @@ export class PackageJsonUpdater { console.log(); console.log(colors.green(`Updating projects to use `) - + packageName + '@' + colors.magenta(version)); + + packageName + '@' + colors.cyan(version)); console.log(); const currentProjectUpdate: IUpdateProjectOptions = { @@ -228,6 +228,16 @@ export class PackageJsonUpdater { packageJson.addOrUpdateDependency(packageName, newVersion, dependencyType!); } + /** + * Selects an appropriate version number for a particular package, given an optional initial SemVer spec. + * If ensureConsistentVersions, tries to pick a version that will be consistent. + * Otherwise, will choose the latest semver matching the initialSpec and append the proper range style. + * @param packageName - the name of the package to be used + * @param initialSpec - a semver pattern that should be used to find the latest version matching the spec + * @param implicitlyPinnedVersion - the implicityly preferred (aka common/primary) version of the package in use + * @param rangeStyle - if this version is selected by querying registry, then this range specifier is prepended to + * the selected version. + */ private _getNormalizedVersionSpec( packageName: string, initialSpec: string | undefined, @@ -236,7 +246,7 @@ export class PackageJsonUpdater { console.log(colors.gray(`Determining new version for dependency: ${packageName}`)); if (initialSpec) { - console.log(`Specified version selector: ${colors.magenta(initialSpec)}`); + console.log(`Specified version selector: ${colors.cyan(initialSpec)}`); } else { console.log(`No version selector specified, will be automatically determined.`); } @@ -245,24 +255,29 @@ export class PackageJsonUpdater { // if ensureConsistentVersions => reuse the pinned version // else, query the registry and use the latest that satisfies semver spec if (initialSpec && implicitlyPinnedVersion && initialSpec === implicitlyPinnedVersion) { - console.log(colors.green('The specified version ') - + colors.magenta(initialSpec) - + colors.green(' has been selected as it matches the implicitly preferred version.')); + console.log(colors.green('Assigning "') + + colors.cyan(initialSpec) + + colors.green('" because it matches what other projects are using in this repo.')); return initialSpec; } if (this._rushConfiguration.ensureConsistentVersions && !initialSpec && implicitlyPinnedVersion) { - console.log(colors.grey('The enforceConsistentVersions policy is currently active.')); - console.log(`Using the implicitly preferred version ${colors.magenta(implicitlyPinnedVersion)}`); + console.log(`Assigning the version range "${colors.cyan(implicitlyPinnedVersion)}" for "${packageName}" because` + + ` it is already used by other projects in this repo.`); return implicitlyPinnedVersion; } let selectedVersion: string | undefined; + if (this._rushConfiguration.packageManager === 'yarn') { + throw new Error('The yarn package manager is not currently supported by the "rush add" command.'); + } + if (initialSpec && initialSpec !== 'latest') { console.log(colors.gray('Finding newest version that satisfies the selector: ') + initialSpec); console.log(); console.log(`Querying registry for all versions of ${packageName}...`); + const allVersions: string = Utilities.executeCommandAndCaptureOutput(this._rushConfiguration.packageManagerToolFilename, ['view', packageName, 'versions', '--json'], @@ -276,16 +291,16 @@ export class PackageJsonUpdater { for (const version of versionList) { if (semver.satisfies(version, initialSpec)) { selectedVersion = version; - console.log(`Found latest version: ${colors.magenta(selectedVersion)}`); + console.log(`Found latest version: ${colors.cyan(selectedVersion)}`); break; } } if (!selectedVersion) { - throw new Error(`Cannot find version for ${packageName} that satisfies '${initialSpec}'`); + throw new Error(`Unable to find a version of ${packageName} that satisfies the version range "${initialSpec}"`); } } else { if (initialSpec !== 'latest') { - console.log(colors.gray(`The enforceConsistentVersions policy is NOT active,` + console.log(colors.gray(`The ensureConsistentVersions policy is NOT active,` + ` therefore using the latest version.`)); console.log(); } @@ -296,19 +311,21 @@ export class PackageJsonUpdater { this._rushConfiguration.commonTempFolder).trim(); console.log(); - console.log(`Found latest version: ${colors.magenta(selectedVersion)}`); + console.log(`Found latest version: ${colors.cyan(selectedVersion)}`); } console.log(); if (rangeStyle === SemVerStyle.Caret) { - console.log(colors.gray('The --caret flag was specified, prepending ^ specifier to version.')); + console.log(colors.grey(`Assigning version "^${selectedVersion}" for "${packageName}" because the "--caret"` + + ` flag was specified.`)); return '^' + selectedVersion; } else if (rangeStyle === SemVerStyle.Exact) { - console.log(colors.gray('The --exact flag was specified, not prepending a specifier to version.')); + console.log(colors.grey(`Assigning version "${selectedVersion}" for "${packageName}" because the "--exact"` + + ` flag was specified.`)); return selectedVersion; } else { - console.log(colors.gray('Prepending ~ specifier to version.')); + console.log(colors.gray(`Assigning version "~${selectedVersion}" for "${packageName}".`)); return '~' + selectedVersion!; } } diff --git a/common/reviews/api/rush-lib.api.ts b/common/reviews/api/rush-lib.api.ts index 1c49e1433db..406a030bf73 100644 --- a/common/reviews/api/rush-lib.api.ts +++ b/common/reviews/api/rush-lib.api.ts @@ -157,7 +157,6 @@ class RushConfiguration { findProjectByShorthandName(shorthandProjectName: string): RushConfigurationProject | undefined; findProjectByTempName(tempProjectName: string): RushConfigurationProject | undefined; getProjectByName(projectName: string): RushConfigurationProject | undefined; - // @beta getProjectForPath(currentFolderPath: string): RushConfigurationProject | undefined; readonly gitAllowedEmailRegExps: string[]; readonly gitSampleEmail: string; @@ -204,6 +203,9 @@ class RushConfigurationProject { // @beta readonly isMainProject: boolean; readonly packageJson: IPackageJson; + // WARNING: The type "PackageJsonEditor" needs to be exported by the package (e.g. added to index.ts) + // @alpha + readonly packageJsonEditor: PackageJsonEditor; readonly packageName: string; readonly projectFolder: string; readonly projectRelativeFolder: string; From 1c8e54a46db8c4dfba52a35e1636b0a3a5388d1f Mon Sep 17 00:00:00 2001 From: Nick Pape Date: Thu, 27 Sep 2018 19:07:09 -0700 Subject: [PATCH 14/27] Some PR feedback --- apps/rush-lib/src/api/PackageJsonEditor.ts | 86 ++++++++++--------- .../rush-lib/src/api/VersionMismatchFinder.ts | 2 +- .../CommandLineHelp.test.ts.snap | 36 ++++---- apps/rush-lib/src/index.ts | 6 ++ apps/rush-lib/src/logic/PackageJsonUpdater.ts | 6 +- 5 files changed, 71 insertions(+), 65 deletions(-) diff --git a/apps/rush-lib/src/api/PackageJsonEditor.ts b/apps/rush-lib/src/api/PackageJsonEditor.ts index c0b8bc07298..e1d25e86859 100644 --- a/apps/rush-lib/src/api/PackageJsonEditor.ts +++ b/apps/rush-lib/src/api/PackageJsonEditor.ts @@ -5,44 +5,40 @@ import { JsonFile } from '@microsoft/node-core-library'; +/** + * @beta + */ export const enum DependencyType { - Dependency = 'dependency', - DevDependency = 'devDependency', - OptionalDependency = 'optionalDependency', - PeerOnly = 'peerDependency' + Regular = 'dependencies', + Dev = 'devDependencies', + Optional = 'optionalDependencies', + Peer = 'peerDependencies' } +/** + * @beta + */ export class Dependency { private _type: DependencyType; private _name: string; - private _version: string | undefined; - private _peerVersion: string | undefined; + private _version: string; private _onChange: () => void; public constructor(name: string, - version: string | undefined, + version: string, type: DependencyType, - peerVersion: string | undefined, onChange: () => void) { this._name = name; this._version = version; this._type = type; - this._peerVersion = peerVersion; this._onChange = onChange; - - if (this._version && this._type === DependencyType.PeerOnly) { - throw new Error(`Cannot specify a primary version if the dependency type is peer-only.`); - } - if (!this._peerVersion && this._type === DependencyType.PeerOnly) { - throw new Error(`Must specify a peer version if the dependency type if peer-only.`); - } } public get name(): string { return this._name; } - public get version(): string | undefined { + public get version(): string { return this._version; } @@ -62,16 +58,16 @@ export class Dependency { this._type = newType; this._onChange(); } - - public get peerVersion(): string | undefined { - return this._peerVersion; - } } +/** + * @beta + */ export class PackageJsonEditor { private readonly _filepath: string; private readonly _data: IPackageJson; private readonly _dependencies: Map; + private readonly _peerDependencies: Map; private _onChange: () => void; private _modified: boolean; @@ -105,13 +101,17 @@ export class PackageJsonEditor { } public addOrUpdateDependency(packageName: string, newVersion: string, dependencyType: DependencyType): void { + if (dependencyType === DependencyType.Peer) { + throw new Error(`This function cannot be used to modify peer dependencies.`); + } + if (this._dependencies.has(packageName)) { const dependency: Dependency = this._dependencies.get(packageName)!; dependency.setVersion(newVersion); dependency.setDependencyType(dependencyType); } else { const dependency: Dependency - = new Dependency(packageName, newVersion, dependencyType, undefined, this._onChange); + = new Dependency(packageName, newVersion, dependencyType, this._onChange); this._dependencies.set(packageName, dependency); } } @@ -130,6 +130,7 @@ export class PackageJsonEditor { this._data = data; this._dependencies = new Map(); + this._peerDependencies = new Map(); const dependencies: { [key: string]: string } = data.dependencies || {}; const devDependencies: { [key: string]: string } = data.devDependencies || {}; @@ -148,8 +149,8 @@ export class PackageJsonEditor { throw new Error(`The package "${dependency}" is listed as both a dev and a regular dependency`); } - this._dependencies.set(dependency, new Dependency(dependency, dependencies[dependency], - DependencyType.Dependency, peerDependencies[dependency], this._onChange)); + this._dependencies.set(dependency, + new Dependency(dependency, dependencies[dependency], DependencyType.Regular, this._onChange)); }); Object.keys(devDependencies || {}).forEach((dependency: string) => { @@ -157,20 +158,18 @@ export class PackageJsonEditor { throw new Error(`The package "${dependency}" is listed as both a dev and an optional dependency`); } - this._dependencies.set(dependency, new Dependency(dependency, devDependencies[dependency], - DependencyType.Dependency, peerDependencies[dependency], this._onChange)); + this._dependencies.set(dependency, + new Dependency(dependency, devDependencies[dependency], DependencyType.Dev, this._onChange)); }); Object.keys(optionalDependencies || {}).forEach((dependency: string) => { this._dependencies.set(dependency, new Dependency(dependency, optionalDependencies[dependency], - DependencyType.OptionalDependency, peerDependencies[dependency], this._onChange)); + DependencyType.Optional, this._onChange)); }); Object.keys(peerDependencies || {}).forEach((dependency: string) => { - if (!this._dependencies.has(dependency)) { - this._dependencies.set(dependency, new Dependency(dependency, undefined, - DependencyType.PeerOnly, peerDependencies[dependency], this._onChange)); - } + this._peerDependencies.set(dependency, + new Dependency(dependency, peerDependencies[dependency], DependencyType.Peer, this._onChange)); }); } @@ -185,33 +184,36 @@ export class PackageJsonEditor { for (const packageName of keys) { const dependency: Dependency = this._dependencies.get(packageName)!; - if (dependency.dependencyType === DependencyType.Dependency) { + if (dependency.dependencyType === DependencyType.Regular) { if (!this._data.dependencies) { this._data.dependencies = {}; } - this._data.dependencies[dependency.name] = dependency.version!; + this._data.dependencies[dependency.name] = dependency.version; } - if (dependency.dependencyType === DependencyType.DevDependency) { + if (dependency.dependencyType === DependencyType.Dev) { if (!this._data.devDependencies) { this._data.devDependencies = {}; } - this._data.devDependencies[dependency.name] = dependency.version!; + this._data.devDependencies[dependency.name] = dependency.version; } - if (dependency.dependencyType === DependencyType.OptionalDependency) { + if (dependency.dependencyType === DependencyType.Optional) { if (!this._data.optionalDependencies) { this._data.optionalDependencies = {}; } - this._data.optionalDependencies[dependency.name] = dependency.version!; + this._data.optionalDependencies[dependency.name] = dependency.version; } + } - if (dependency.peerVersion) { - if (!this._data.peerDependencies) { - this._data.peerDependencies = {}; - } - this._data.peerDependencies[dependency.name] = dependency.peerVersion; + const peerKeys: Array = [...this._peerDependencies.keys()].sort(); + + for (const packageName of peerKeys) { + const dependency: Dependency = this._peerDependencies.get(packageName)!; + if (!this._data.peerDependencies) { + this._data.peerDependencies = {}; } + this._data.peerDependencies[dependency.name] = dependency.version; } return this._data; diff --git a/apps/rush-lib/src/api/VersionMismatchFinder.ts b/apps/rush-lib/src/api/VersionMismatchFinder.ts index 876d3fbd138..1344d05f484 100644 --- a/apps/rush-lib/src/api/VersionMismatchFinder.ts +++ b/apps/rush-lib/src/api/VersionMismatchFinder.ts @@ -136,7 +136,7 @@ export class VersionMismatchFinder { // levels of compatibility -- we should wait for someone to actually request this feature // before we get into that.) project.packageJsonEditor.forEachDependency((dependency: Dependency) => { - if (dependency.dependencyType !== DependencyType.PeerOnly + if (dependency.dependencyType !== DependencyType.Peer && !project.cyclicDependencyProjects.has(dependency.name)) { const version: string = dependency.version!; diff --git a/apps/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap b/apps/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap index 509509fe1c2..3a69706498b 100644 --- a/apps/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap +++ b/apps/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap @@ -53,34 +53,32 @@ For detailed help about a specific command, use: rush -h `; exports[`CommandLineHelp prints the help for "rush add" 1`] = ` -"usage: rush add [-h] -p PACKAGE_NAME [-v VERSION_RANGE] [--exact] [--caret] - [-d] [-m] [-s] - - -Adds a dependency on a certain package to the current project (detected using -the current working directory) and then runs rush update. If no version is -specified, a version will be automatically detected (typically either the -latest version or a version that won't break the ensureConsistentVersions -policy). If a version range is specified, the latest version in the range -will be used. The version will be automatically prepended with a tilde, -unless the --exact or --caret flags are used. The --make-consistent flag can -be used to update all packages with the dependency. +"usage: rush add [-h] -p PACKAGE_NAME [--exact] [--caret] [--dev] [-m] [-s] + +Adds a specified package as a dependency of the current project (as +determined by the current working directory) and then runs \\"rush update\\".If +no version is specified, a version will be automatically detected (typically +either the latest version or a version that won't break the +ensureConsistentVersions policy). If a version range is specified, the latest +version in the range will be used. The version will be automatically +prepended with a tilde, unless the \\"--exact\\" or \\"--caret\\" flags are used. The +\\"--make-consistent\\" flag can be used to update all packages with the +dependency. Optional arguments: -h, --help Show this help message and exit. -p PACKAGE_NAME, --package PACKAGE_NAME (Required) The name of the package which should be - added as a dependency - -v VERSION_RANGE, --version VERSION_RANGE - An optional version specifier. If specified, the - largest version satisfying this range will be added - to the package.json. + added as a dependency. Also, the version specifier + can be appended after an \\"@\\" sign (similar to NPM's + semantics). --exact If specified, the SemVer specifier added to the - package.json will be a locked, exact version. + package.json will be an exact version (e.g. without + tilde or caret). --caret If specified, the SemVer specifier added to the package.json will be a prepended with a \\"caret\\" specifier (\\"^\\"). - -d, --dev If specified, the package will be added as a + --dev If specified, the package will be added as a \\"devDependency\\" to the package.json -m, --make-consistent If specified, other packages with this dependency diff --git a/apps/rush-lib/src/index.ts b/apps/rush-lib/src/index.ts index cf6e774a450..5d21f2776ae 100644 --- a/apps/rush-lib/src/index.ts +++ b/apps/rush-lib/src/index.ts @@ -33,6 +33,12 @@ export { CommonVersionsConfiguration } from './api/CommonVersionsConfiguration'; +export { + PackageJsonEditor, + Dependency, + DependencyType +} from './api/PackageJsonEditor'; + export { EventHooks, Event diff --git a/apps/rush-lib/src/logic/PackageJsonUpdater.ts b/apps/rush-lib/src/logic/PackageJsonUpdater.ts index 2ad3097a975..fe817098f7c 100644 --- a/apps/rush-lib/src/logic/PackageJsonUpdater.ts +++ b/apps/rush-lib/src/logic/PackageJsonUpdater.ts @@ -125,7 +125,7 @@ export class PackageJsonUpdater { project: currentProject, packageName, newVersion: version, - dependencyType: devDependency ? DependencyType.DevDependency : DependencyType.Dependency + dependencyType: devDependency ? DependencyType.Dev : DependencyType.Regular }; this.updateProject(currentProjectUpdate); @@ -139,8 +139,8 @@ export class PackageJsonUpdater { if (mismatches.length) { if (!updateOtherPackages) { return Promise.reject(new Error(`Adding '${packageName}@${version}' to ${currentProject.packageName}` - + ` causes mismatched dependencies. Use the --make-consistent flag to update other packages to use this` - + ` version, or do not specify the --version flag.`)); + + ` causes mismatched dependencies. Use the "--make-consistent" flag to update other packages to use this` + + ` version, or do not specify the "--version" flag.`)); } // otherwise we need to go update a bunch of other projects From abfabf25c3a5c277aea496a7a9cb2e022a7ad638 Mon Sep 17 00:00:00 2001 From: Nick Pape Date: Thu, 27 Sep 2018 19:09:12 -0700 Subject: [PATCH 15/27] PR Feedback --- apps/rush-lib/src/cli/actions/AddAction.ts | 8 +-- .../CommandLineHelp.test.ts.snap | 6 +-- common/reviews/api/rush-lib.api.ts | 50 ++++++++++++++++++- 3 files changed, 56 insertions(+), 8 deletions(-) diff --git a/apps/rush-lib/src/cli/actions/AddAction.ts b/apps/rush-lib/src/cli/actions/AddAction.ts index 882c8f5a72b..a10a605f3a4 100644 --- a/apps/rush-lib/src/cli/actions/AddAction.ts +++ b/apps/rush-lib/src/cli/actions/AddAction.ts @@ -27,7 +27,7 @@ export class AddAction extends BaseRushAction { constructor(parser: RushCommandLineParser) { const documentation: string[] = [ 'Adds a specified package as a dependency of the current project (as determined by the current working directory)' - + ' and then runs "rush update".If no version is specified, a version will be automatically detected (typically' + + ' and then runs "rush update". If no version is specified, a version will be automatically detected (typically' + ' either the latest version or a version that won\'t break the ensureConsistentVersions policy). If a version' + ' range is specified, the latest version in the range will be used. The version will be automatically prepended' + ' with a tilde, unless the "--exact" or "--caret" flags are used. The "--make-consistent" flag can be used to' @@ -63,8 +63,8 @@ export class AddAction extends BaseRushAction { }); this._devDependencyFlag = this.defineFlagParameter({ parameterLongName: '--dev', - description: 'If specified, the package will be added as a "devDependency"' - + ' to the package.json' + description: 'If specified, the package will be added to the "devDependencies" section of' + + ' the package.json' }); this._makeConsistentFlag = this.defineFlagParameter({ parameterLongName: '--make-consistent', @@ -90,7 +90,7 @@ export class AddAction extends BaseRushAction { } if (this._caretFlag.value && this._exactFlag.value) { - return Promise.reject(new Error('Only one of --caret and --exact should be specified')); + return Promise.reject(new Error('Only one of "--caret" and "--exact" should be specified')); } const packageName: string = this._packageName.value!; diff --git a/apps/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap b/apps/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap index 3a69706498b..1f9b1a9078c 100644 --- a/apps/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap +++ b/apps/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap @@ -56,7 +56,7 @@ exports[`CommandLineHelp prints the help for "rush add" 1`] = ` "usage: rush add [-h] -p PACKAGE_NAME [--exact] [--caret] [--dev] [-m] [-s] Adds a specified package as a dependency of the current project (as -determined by the current working directory) and then runs \\"rush update\\".If +determined by the current working directory) and then runs \\"rush update\\". If no version is specified, a version will be automatically detected (typically either the latest version or a version that won't break the ensureConsistentVersions policy). If a version range is specified, the latest @@ -78,8 +78,8 @@ Optional arguments: --caret If specified, the SemVer specifier added to the package.json will be a prepended with a \\"caret\\" specifier (\\"^\\"). - --dev If specified, the package will be added as a - \\"devDependency\\" to the package.json + --dev If specified, the package will be added to the + \\"devDependencies\\" section of the package.json -m, --make-consistent If specified, other packages with this dependency will have their package.json files updated to use the diff --git a/common/reviews/api/rush-lib.api.ts b/common/reviews/api/rush-lib.api.ts index 406a030bf73..38231fe2392 100644 --- a/common/reviews/api/rush-lib.api.ts +++ b/common/reviews/api/rush-lib.api.ts @@ -72,6 +72,33 @@ class CommonVersionsConfiguration { readonly xstitchPreferredVersions: Map; } +// @beta (undocumented) +class Dependency { + constructor(name: string, version: string, type: DependencyType, onChange: () => void); + // (undocumented) + readonly dependencyType: DependencyType; + // (undocumented) + readonly name: string; + // (undocumented) + setDependencyType(newType: DependencyType): void; + // (undocumented) + setVersion(newVersion: string): void; + // (undocumented) + readonly version: string; +} + +// @beta (undocumented) +enum DependencyType { + // (undocumented) + Dev = "devDependencies", + // (undocumented) + Optional = "optionalDependencies", + // (undocumented) + Peer = "peerDependencies", + // (undocumented) + Regular = "dependencies" +} + // @public enum EnvironmentVariableNames { RUSH_PREVIEW_VERSION = "RUSH_PREVIEW_VERSION", @@ -125,6 +152,28 @@ class LockStepVersionPolicy extends VersionPolicy { readonly version: string; } +// @beta (undocumented) +class PackageJsonEditor { + // (undocumented) + addOrUpdateDependency(packageName: string, newVersion: string, dependencyType: DependencyType): void; + // (undocumented) + readonly filePath: string; + // (undocumented) + forEachDependency(cb: (dependency: Dependency) => void): void; + // (undocumented) + static fromObject(object: IPackageJson, filename: string): PackageJsonEditor; + // (undocumented) + getDependency(packageName: string): Dependency | undefined; + // (undocumented) + static load(filepath: string): PackageJsonEditor; + // (undocumented) + readonly name: string; + // (undocumented) + saveIfModified(): boolean; + // (undocumented) + readonly version: string; +} + // @public class PnpmOptionsConfiguration { // WARNING: The type "IPnpmOptionsJson" needs to be exported by the package (e.g. added to index.ts) @@ -203,7 +252,6 @@ class RushConfigurationProject { // @beta readonly isMainProject: boolean; readonly packageJson: IPackageJson; - // WARNING: The type "PackageJsonEditor" needs to be exported by the package (e.g. added to index.ts) // @alpha readonly packageJsonEditor: PackageJsonEditor; readonly packageName: string; From 9d66f66fb37ef0b3a0ae4f0bfa2586ded40000bd Mon Sep 17 00:00:00 2001 From: Nick Pape Date: Thu, 27 Sep 2018 19:12:17 -0700 Subject: [PATCH 16/27] More feedback --- apps/rush-lib/src/api/PackageJsonEditor.ts | 35 +++++++++-------- apps/rush-lib/src/api/RushConfiguration.ts | 2 +- .../src/api/RushConfigurationProject.ts | 4 +- .../rush-lib/src/api/VersionMismatchFinder.ts | 4 +- apps/rush-lib/src/cli/actions/AddAction.ts | 2 +- apps/rush-lib/src/index.ts | 2 +- apps/rush-lib/src/logic/PackageJsonUpdater.ts | 4 +- common/reviews/api/rush-lib.api.ts | 39 ++++++++++--------- 8 files changed, 48 insertions(+), 44 deletions(-) diff --git a/apps/rush-lib/src/api/PackageJsonEditor.ts b/apps/rush-lib/src/api/PackageJsonEditor.ts index e1d25e86859..647112f2eb4 100644 --- a/apps/rush-lib/src/api/PackageJsonEditor.ts +++ b/apps/rush-lib/src/api/PackageJsonEditor.ts @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + import * as semver from 'semver'; import { @@ -18,7 +21,7 @@ export const enum DependencyType { /** * @beta */ -export class Dependency { +export class PackageJsonDependency { private _type: DependencyType; private _name: string; private _version: string; @@ -66,8 +69,8 @@ export class Dependency { export class PackageJsonEditor { private readonly _filepath: string; private readonly _data: IPackageJson; - private readonly _dependencies: Map; - private readonly _peerDependencies: Map; + private readonly _dependencies: Map; + private readonly _peerDependencies: Map; private _onChange: () => void; private _modified: boolean; @@ -92,11 +95,11 @@ export class PackageJsonEditor { return this._filepath; } - public getDependency(packageName: string): Dependency | undefined { + public getDependency(packageName: string): PackageJsonDependency | undefined { return this._dependencies.get(packageName); } - public forEachDependency(cb: (dependency: Dependency) => void): void { + public forEachDependency(cb: (dependency: PackageJsonDependency) => void): void { this._dependencies.forEach(cb); } @@ -106,12 +109,12 @@ export class PackageJsonEditor { } if (this._dependencies.has(packageName)) { - const dependency: Dependency = this._dependencies.get(packageName)!; + const dependency: PackageJsonDependency = this._dependencies.get(packageName)!; dependency.setVersion(newVersion); dependency.setDependencyType(dependencyType); } else { - const dependency: Dependency - = new Dependency(packageName, newVersion, dependencyType, this._onChange); + const dependency: PackageJsonDependency + = new PackageJsonDependency(packageName, newVersion, dependencyType, this._onChange); this._dependencies.set(packageName, dependency); } } @@ -129,8 +132,8 @@ export class PackageJsonEditor { this._filepath = filepath; this._data = data; - this._dependencies = new Map(); - this._peerDependencies = new Map(); + this._dependencies = new Map(); + this._peerDependencies = new Map(); const dependencies: { [key: string]: string } = data.dependencies || {}; const devDependencies: { [key: string]: string } = data.devDependencies || {}; @@ -150,7 +153,7 @@ export class PackageJsonEditor { } this._dependencies.set(dependency, - new Dependency(dependency, dependencies[dependency], DependencyType.Regular, this._onChange)); + new PackageJsonDependency(dependency, dependencies[dependency], DependencyType.Regular, this._onChange)); }); Object.keys(devDependencies || {}).forEach((dependency: string) => { @@ -159,17 +162,17 @@ export class PackageJsonEditor { } this._dependencies.set(dependency, - new Dependency(dependency, devDependencies[dependency], DependencyType.Dev, this._onChange)); + new PackageJsonDependency(dependency, devDependencies[dependency], DependencyType.Dev, this._onChange)); }); Object.keys(optionalDependencies || {}).forEach((dependency: string) => { - this._dependencies.set(dependency, new Dependency(dependency, optionalDependencies[dependency], + this._dependencies.set(dependency, new PackageJsonDependency(dependency, optionalDependencies[dependency], DependencyType.Optional, this._onChange)); }); Object.keys(peerDependencies || {}).forEach((dependency: string) => { this._peerDependencies.set(dependency, - new Dependency(dependency, peerDependencies[dependency], DependencyType.Peer, this._onChange)); + new PackageJsonDependency(dependency, peerDependencies[dependency], DependencyType.Peer, this._onChange)); }); } @@ -182,7 +185,7 @@ export class PackageJsonEditor { const keys: Array = [...this._dependencies.keys()].sort(); for (const packageName of keys) { - const dependency: Dependency = this._dependencies.get(packageName)!; + const dependency: PackageJsonDependency = this._dependencies.get(packageName)!; if (dependency.dependencyType === DependencyType.Regular) { if (!this._data.dependencies) { @@ -209,7 +212,7 @@ export class PackageJsonEditor { const peerKeys: Array = [...this._peerDependencies.keys()].sort(); for (const packageName of peerKeys) { - const dependency: Dependency = this._peerDependencies.get(packageName)!; + const dependency: PackageJsonDependency = this._peerDependencies.get(packageName)!; if (!this._data.peerDependencies) { this._data.peerDependencies = {}; } diff --git a/apps/rush-lib/src/api/RushConfiguration.ts b/apps/rush-lib/src/api/RushConfiguration.ts index c5aa9f106b0..e6cedf87dca 100644 --- a/apps/rush-lib/src/api/RushConfiguration.ts +++ b/apps/rush-lib/src/api/RushConfiguration.ts @@ -753,7 +753,7 @@ export class RushConfiguration { * Returns the project for which the specified path is underneath that project's folder. * If the path is not under any project's folder, returns undefined. */ - public getProjectForPath(currentFolderPath: string): RushConfigurationProject | undefined { + public tryGetProjectForPath(currentFolderPath: string): RushConfigurationProject | undefined { const resolvedPath: string = path.resolve(currentFolderPath); for (const project of this.projects) { if (Path.isUnder(project.projectFolder, resolvedPath)) { diff --git a/apps/rush-lib/src/api/RushConfigurationProject.ts b/apps/rush-lib/src/api/RushConfigurationProject.ts index f64b12c4da9..d947640ff8d 100644 --- a/apps/rush-lib/src/api/RushConfigurationProject.ts +++ b/apps/rush-lib/src/api/RushConfigurationProject.ts @@ -176,7 +176,7 @@ export class RushConfigurationProject { /** * The parsed NPM "package.json" file from projectFolder. - * Will be deprecated soon in favor of packageJsonEditor + * @deprecated Use packageJsonEditor instead */ public get packageJson(): IPackageJson { return this._packageJson; @@ -184,7 +184,7 @@ export class RushConfigurationProject { /** * A useful wrapper around the package.json file for making modifications - * @alpha + * @beta */ public get packageJsonEditor(): PackageJsonEditor { return this._packageJsonEditor; diff --git a/apps/rush-lib/src/api/VersionMismatchFinder.ts b/apps/rush-lib/src/api/VersionMismatchFinder.ts index 1344d05f484..f1d4b7776b2 100644 --- a/apps/rush-lib/src/api/VersionMismatchFinder.ts +++ b/apps/rush-lib/src/api/VersionMismatchFinder.ts @@ -6,7 +6,7 @@ import * as colors from 'colors'; import { RushConfiguration } from './RushConfiguration'; import { RushConfigurationProject } from './RushConfigurationProject'; import { RushConstants } from '../logic/RushConstants'; -import { Dependency, DependencyType, PackageJsonEditor } from './PackageJsonEditor'; +import { PackageJsonDependency, DependencyType, PackageJsonEditor } from './PackageJsonEditor'; /** * @public @@ -135,7 +135,7 @@ export class VersionMismatchFinder { // patterns consistent, but on the other hand different projects may have different // levels of compatibility -- we should wait for someone to actually request this feature // before we get into that.) - project.packageJsonEditor.forEachDependency((dependency: Dependency) => { + project.packageJsonEditor.forEachDependency((dependency: PackageJsonDependency) => { if (dependency.dependencyType !== DependencyType.Peer && !project.cyclicDependencyProjects.has(dependency.name)) { diff --git a/apps/rush-lib/src/cli/actions/AddAction.ts b/apps/rush-lib/src/cli/actions/AddAction.ts index a10a605f3a4..068bf862446 100644 --- a/apps/rush-lib/src/cli/actions/AddAction.ts +++ b/apps/rush-lib/src/cli/actions/AddAction.ts @@ -82,7 +82,7 @@ export class AddAction extends BaseRushAction { public run(): Promise { const project: RushConfigurationProject | undefined - = this.rushConfiguration.getProjectForPath(process.cwd()); + = this.rushConfiguration.tryGetProjectForPath(process.cwd()); if (!project) { return Promise.reject(new Error('The "rush add" command must be invoked under a project' diff --git a/apps/rush-lib/src/index.ts b/apps/rush-lib/src/index.ts index 5d21f2776ae..438e92675a7 100644 --- a/apps/rush-lib/src/index.ts +++ b/apps/rush-lib/src/index.ts @@ -35,7 +35,7 @@ export { export { PackageJsonEditor, - Dependency, + PackageJsonDependency, DependencyType } from './api/PackageJsonEditor'; diff --git a/apps/rush-lib/src/logic/PackageJsonUpdater.ts b/apps/rush-lib/src/logic/PackageJsonUpdater.ts index fe817098f7c..6c26b586e94 100644 --- a/apps/rush-lib/src/logic/PackageJsonUpdater.ts +++ b/apps/rush-lib/src/logic/PackageJsonUpdater.ts @@ -10,7 +10,7 @@ import { RushConfigurationProject } from '../api/RushConfigurationProject'; import { VersionMismatchFinder } from '../api/VersionMismatchFinder'; import { PurgeManager } from './PurgeManager'; import { Utilities } from '../utilities/Utilities'; -import { DependencyType, PackageJsonEditor, Dependency } from '../api/PackageJsonEditor'; +import { DependencyType, PackageJsonEditor, PackageJsonDependency } from '../api/PackageJsonEditor'; /** * The type of SemVer range specifier that is prepended to the version @@ -214,7 +214,7 @@ export class PackageJsonUpdater { } = options; const packageJson: PackageJsonEditor = project.packageJsonEditor; - const oldDependency: Dependency | undefined = packageJson.getDependency(packageName); + const oldDependency: PackageJsonDependency | undefined = packageJson.getDependency(packageName); const oldDependencyType: DependencyType | undefined = oldDependency ? oldDependency.dependencyType : undefined; if (!dependencyType && !oldDependencyType) { diff --git a/common/reviews/api/rush-lib.api.ts b/common/reviews/api/rush-lib.api.ts index 38231fe2392..c95679b3ddc 100644 --- a/common/reviews/api/rush-lib.api.ts +++ b/common/reviews/api/rush-lib.api.ts @@ -72,21 +72,6 @@ class CommonVersionsConfiguration { readonly xstitchPreferredVersions: Map; } -// @beta (undocumented) -class Dependency { - constructor(name: string, version: string, type: DependencyType, onChange: () => void); - // (undocumented) - readonly dependencyType: DependencyType; - // (undocumented) - readonly name: string; - // (undocumented) - setDependencyType(newType: DependencyType): void; - // (undocumented) - setVersion(newVersion: string): void; - // (undocumented) - readonly version: string; -} - // @beta (undocumented) enum DependencyType { // (undocumented) @@ -152,6 +137,21 @@ class LockStepVersionPolicy extends VersionPolicy { readonly version: string; } +// @beta (undocumented) +class PackageJsonDependency { + constructor(name: string, version: string, type: DependencyType, onChange: () => void); + // (undocumented) + readonly dependencyType: DependencyType; + // (undocumented) + readonly name: string; + // (undocumented) + setDependencyType(newType: DependencyType): void; + // (undocumented) + setVersion(newVersion: string): void; + // (undocumented) + readonly version: string; +} + // @beta (undocumented) class PackageJsonEditor { // (undocumented) @@ -159,11 +159,11 @@ class PackageJsonEditor { // (undocumented) readonly filePath: string; // (undocumented) - forEachDependency(cb: (dependency: Dependency) => void): void; + forEachDependency(cb: (dependency: PackageJsonDependency) => void): void; // (undocumented) static fromObject(object: IPackageJson, filename: string): PackageJsonEditor; // (undocumented) - getDependency(packageName: string): Dependency | undefined; + getDependency(packageName: string): PackageJsonDependency | undefined; // (undocumented) static load(filepath: string): PackageJsonEditor; // (undocumented) @@ -206,7 +206,6 @@ class RushConfiguration { findProjectByShorthandName(shorthandProjectName: string): RushConfigurationProject | undefined; findProjectByTempName(tempProjectName: string): RushConfigurationProject | undefined; getProjectByName(projectName: string): RushConfigurationProject | undefined; - getProjectForPath(currentFolderPath: string): RushConfigurationProject | undefined; readonly gitAllowedEmailRegExps: string[]; readonly gitSampleEmail: string; readonly hotfixChangeEnabled: boolean; @@ -237,6 +236,7 @@ class RushConfiguration { readonly tempShrinkwrapFilename: string; readonly tempShrinkwrapPreinstallFilename: string; static tryFindRushJsonLocation(verbose?: boolean): string | undefined; + tryGetProjectForPath(currentFolderPath: string): RushConfigurationProject | undefined; // @beta (undocumented) readonly versionPolicyConfiguration: VersionPolicyConfiguration; readonly yarnCacheFolder: string; @@ -251,8 +251,9 @@ class RushConfigurationProject { readonly downstreamDependencyProjects: string[]; // @beta readonly isMainProject: boolean; + // @deprecated readonly packageJson: IPackageJson; - // @alpha + // @beta readonly packageJsonEditor: PackageJsonEditor; readonly packageName: string; readonly projectFolder: string; From 14e448f3c653f4e0dd15adbff0b81fe9ee104ee2 Mon Sep 17 00:00:00 2001 From: Nick Pape Date: Thu, 27 Sep 2018 19:18:06 -0700 Subject: [PATCH 17/27] More feedback --- apps/rush-lib/src/cli/actions/AddAction.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/apps/rush-lib/src/cli/actions/AddAction.ts b/apps/rush-lib/src/cli/actions/AddAction.ts index 068bf862446..774a19f268d 100644 --- a/apps/rush-lib/src/cli/actions/AddAction.ts +++ b/apps/rush-lib/src/cli/actions/AddAction.ts @@ -22,7 +22,6 @@ export class AddAction extends BaseRushAction { private _makeConsistentFlag: CommandLineFlagParameter; private _skipUpdateFlag: CommandLineFlagParameter; private _packageName: CommandLineStringParameter; - private _versionSpecifier: CommandLineStringParameter; constructor(parser: RushCommandLineParser) { const documentation: string[] = [ @@ -47,7 +46,7 @@ export class AddAction extends BaseRushAction { parameterLongName: '--package', parameterShortName: '-p', required: true, - argumentName: 'PACKAGE_NAME', + argumentName: 'PACKAGE', description: '(Required) The name of the package which should be added as a dependency.' + ' Also, the version specifier can be appended after an "@" sign (similar to NPM\'s semantics).' }); @@ -98,7 +97,13 @@ export class AddAction extends BaseRushAction { return Promise.reject(new Error(`The package name "${packageName}" is not valid.`)); } - const version: string | undefined = this._versionSpecifier.value; + let version: string | undefined = undefined; + const scopedPackage: boolean = packageName.charAt(0) === '@'; + const parts: Array = packageName.split('@'); + if (parts.length === 3) { + version = parts[scopedPackage ? 2 : 1]; + } + if (version && !semver.validRange(version) && !semver.valid(version)) { return Promise.reject(new Error(`The SemVer specifier "${version}" is not valid.`)); } From 6e62fdc052981eb7e0767467e52a16cc87e590e0 Mon Sep 17 00:00:00 2001 From: Nick Pape Date: Thu, 27 Sep 2018 19:19:16 -0700 Subject: [PATCH 18/27] Snapshot --- .../src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap b/apps/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap index 1f9b1a9078c..942b076e450 100644 --- a/apps/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap +++ b/apps/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap @@ -53,7 +53,7 @@ For detailed help about a specific command, use: rush -h `; exports[`CommandLineHelp prints the help for "rush add" 1`] = ` -"usage: rush add [-h] -p PACKAGE_NAME [--exact] [--caret] [--dev] [-m] [-s] +"usage: rush add [-h] -p PACKAGE [--exact] [--caret] [--dev] [-m] [-s] Adds a specified package as a dependency of the current project (as determined by the current working directory) and then runs \\"rush update\\". If @@ -67,7 +67,7 @@ dependency. Optional arguments: -h, --help Show this help message and exit. - -p PACKAGE_NAME, --package PACKAGE_NAME + -p PACKAGE, --package PACKAGE (Required) The name of the package which should be added as a dependency. Also, the version specifier can be appended after an \\"@\\" sign (similar to NPM's From 836c9a23b269f934a05a5c333ba21b012858c9c7 Mon Sep 17 00:00:00 2001 From: pgonzal Date: Thu, 27 Sep 2018 19:22:58 -0700 Subject: [PATCH 19/27] Updating strings --- apps/rush-lib/src/cli/actions/AddAction.ts | 2 +- .../CommandLineHelp.test.ts.snap | 4 ++-- apps/rush-lib/src/logic/PackageJsonUpdater.ts | 24 ++++++++++--------- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/apps/rush-lib/src/cli/actions/AddAction.ts b/apps/rush-lib/src/cli/actions/AddAction.ts index a10a605f3a4..a9a3900c20a 100644 --- a/apps/rush-lib/src/cli/actions/AddAction.ts +++ b/apps/rush-lib/src/cli/actions/AddAction.ts @@ -28,7 +28,7 @@ export class AddAction extends BaseRushAction { const documentation: string[] = [ 'Adds a specified package as a dependency of the current project (as determined by the current working directory)' + ' and then runs "rush update". If no version is specified, a version will be automatically detected (typically' - + ' either the latest version or a version that won\'t break the ensureConsistentVersions policy). If a version' + + ' either the latest version or a version that won\'t break the "ensureConsistentVersions" policy). If a version' + ' range is specified, the latest version in the range will be used. The version will be automatically prepended' + ' with a tilde, unless the "--exact" or "--caret" flags are used. The "--make-consistent" flag can be used to' + ' update all packages with the dependency.' diff --git a/apps/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap b/apps/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap index 1f9b1a9078c..35fcdd49034 100644 --- a/apps/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap +++ b/apps/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap @@ -59,8 +59,8 @@ Adds a specified package as a dependency of the current project (as determined by the current working directory) and then runs \\"rush update\\". If no version is specified, a version will be automatically detected (typically either the latest version or a version that won't break the -ensureConsistentVersions policy). If a version range is specified, the latest -version in the range will be used. The version will be automatically +\\"ensureConsistentVersions\\" policy). If a version range is specified, the +latest version in the range will be used. The version will be automatically prepended with a tilde, unless the \\"--exact\\" or \\"--caret\\" flags are used. The \\"--make-consistent\\" flag can be used to update all packages with the dependency. diff --git a/apps/rush-lib/src/logic/PackageJsonUpdater.ts b/apps/rush-lib/src/logic/PackageJsonUpdater.ts index fe817098f7c..ebe90591deb 100644 --- a/apps/rush-lib/src/logic/PackageJsonUpdater.ts +++ b/apps/rush-lib/src/logic/PackageJsonUpdater.ts @@ -140,7 +140,7 @@ export class PackageJsonUpdater { if (!updateOtherPackages) { return Promise.reject(new Error(`Adding '${packageName}@${version}' to ${currentProject.packageName}` + ` causes mismatched dependencies. Use the "--make-consistent" flag to update other packages to use this` - + ` version, or do not specify the "--version" flag.`)); + + ` version, or do not specify a SemVer range.`)); } // otherwise we need to go update a bunch of other projects @@ -218,7 +218,8 @@ export class PackageJsonUpdater { const oldDependencyType: DependencyType | undefined = oldDependency ? oldDependency.dependencyType : undefined; if (!dependencyType && !oldDependencyType) { - throw new Error(`Cannot auto-detect dependency type of "${packageName}" for project "${project.packageName}"`); + throw new Error(`The IUpdateProjectOptions.dependencyType option cannot be omitted, because` + + ` project "${project.packageName}" does not have an existing dependency on "${packageName}"`); } if (!dependencyType) { @@ -248,7 +249,7 @@ export class PackageJsonUpdater { if (initialSpec) { console.log(`Specified version selector: ${colors.cyan(initialSpec)}`); } else { - console.log(`No version selector specified, will be automatically determined.`); + console.log(`No version selector was specified, so the version will be determined automatically.`); } console.log(); @@ -257,7 +258,7 @@ export class PackageJsonUpdater { if (initialSpec && implicitlyPinnedVersion && initialSpec === implicitlyPinnedVersion) { console.log(colors.green('Assigning "') + colors.cyan(initialSpec) - + colors.green('" because it matches what other projects are using in this repo.')); + + colors.green(`" for "${packageName}" because it matches what other projects are using in this repo.`)); return initialSpec; } @@ -270,13 +271,13 @@ export class PackageJsonUpdater { let selectedVersion: string | undefined; if (this._rushConfiguration.packageManager === 'yarn') { - throw new Error('The yarn package manager is not currently supported by the "rush add" command.'); + throw new Error('The Yarn package manager is not currently supported by the "rush add" command.'); } if (initialSpec && initialSpec !== 'latest') { console.log(colors.gray('Finding newest version that satisfies the selector: ') + initialSpec); console.log(); - console.log(`Querying registry for all versions of ${packageName}...`); + console.log(`Querying registry for all versions of "${packageName}"...`); const allVersions: string = Utilities.executeCommandAndCaptureOutput(this._rushConfiguration.packageManagerToolFilename, @@ -296,15 +297,16 @@ export class PackageJsonUpdater { } } if (!selectedVersion) { - throw new Error(`Unable to find a version of ${packageName} that satisfies the version range "${initialSpec}"`); + throw new Error(`Unable to find a version of "${packageName}" that satisfies` + + ` the version range "${initialSpec}"`); } } else { if (initialSpec !== 'latest') { - console.log(colors.gray(`The ensureConsistentVersions policy is NOT active,` - + ` therefore using the latest version.`)); + console.log(colors.gray(`The "ensureConsistentVersions" policy is NOT active,` + + ` so we will assign the latest version.`)); console.log(); } - console.log(`Querying NPM registry for latest version of ${packageName}...`); + console.log(`Querying NPM registry for latest version of "${packageName}"...`); selectedVersion = Utilities.executeCommandAndCaptureOutput(this._rushConfiguration.packageManagerToolFilename, ['view', `${packageName}@latest`, 'version'], @@ -329,4 +331,4 @@ export class PackageJsonUpdater { return '~' + selectedVersion!; } } -} \ No newline at end of file +} From e0793d087f05f6d97882c9ce5ab7cc0bdf83c12a Mon Sep 17 00:00:00 2001 From: pgonzal Date: Thu, 27 Sep 2018 19:24:45 -0700 Subject: [PATCH 20/27] Add change log --- .../rush/nickpape-rush-add_2018-09-28-02-24.json | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 common/changes/@microsoft/rush/nickpape-rush-add_2018-09-28-02-24.json diff --git a/common/changes/@microsoft/rush/nickpape-rush-add_2018-09-28-02-24.json b/common/changes/@microsoft/rush/nickpape-rush-add_2018-09-28-02-24.json new file mode 100644 index 00000000000..4a40823fe77 --- /dev/null +++ b/common/changes/@microsoft/rush/nickpape-rush-add_2018-09-28-02-24.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "Add a new command \"rush add\" for managing package.json dependencies", + "packageName": "@microsoft/rush", + "type": "none" + } + ], + "packageName": "@microsoft/rush", + "email": "pgonzal@users.noreply.github.com" +} \ No newline at end of file From ddf3e0b03f3a3acf7ae85fbce9e35e961b9e5f75 Mon Sep 17 00:00:00 2001 From: Nick Pape Date: Thu, 27 Sep 2018 19:43:32 -0700 Subject: [PATCH 21/27] Some fixes --- apps/rush-lib/src/api/RushConfiguration.ts | 2 +- apps/rush-lib/src/cli/actions/AddAction.ts | 21 +++++++++++++-------- common/reviews/api/node-core-library.api.ts | 2 ++ libraries/node-core-library/src/Path.ts | 9 ++++++++- 4 files changed, 24 insertions(+), 10 deletions(-) diff --git a/apps/rush-lib/src/api/RushConfiguration.ts b/apps/rush-lib/src/api/RushConfiguration.ts index e6cedf87dca..5a341f18880 100644 --- a/apps/rush-lib/src/api/RushConfiguration.ts +++ b/apps/rush-lib/src/api/RushConfiguration.ts @@ -756,7 +756,7 @@ export class RushConfiguration { public tryGetProjectForPath(currentFolderPath: string): RushConfigurationProject | undefined { const resolvedPath: string = path.resolve(currentFolderPath); for (const project of this.projects) { - if (Path.isUnder(project.projectFolder, resolvedPath)) { + if (Path.isUnderOrEqual(resolvedPath, project.projectFolder)) { return project; } } diff --git a/apps/rush-lib/src/cli/actions/AddAction.ts b/apps/rush-lib/src/cli/actions/AddAction.ts index 774a19f268d..0e469c25aec 100644 --- a/apps/rush-lib/src/cli/actions/AddAction.ts +++ b/apps/rush-lib/src/cli/actions/AddAction.ts @@ -92,16 +92,21 @@ export class AddAction extends BaseRushAction { return Promise.reject(new Error('Only one of "--caret" and "--exact" should be specified')); } - const packageName: string = this._packageName.value!; - if (!PackageName.isValidName(packageName)) { - return Promise.reject(new Error(`The package name "${packageName}" is not valid.`)); - } - let version: string | undefined = undefined; - const scopedPackage: boolean = packageName.charAt(0) === '@'; + let packageName: string | undefined = this._packageName.value!; const parts: Array = packageName.split('@'); - if (parts.length === 3) { - version = parts[scopedPackage ? 2 : 1]; + + if (parts[0] === '') { + // this is a scoped package + packageName = '@' + parts[1]; + version = parts[2]; + } else { + packageName = parts[0]; + version = parts[1]; + } + + if (!PackageName.isValidName(packageName)) { + return Promise.reject(new Error(`The package name "${packageName}" is not valid.`)); } if (version && !semver.validRange(version) && !semver.valid(version)) { diff --git a/common/reviews/api/node-core-library.api.ts b/common/reviews/api/node-core-library.api.ts index 47a627a783f..637a1d0888d 100644 --- a/common/reviews/api/node-core-library.api.ts +++ b/common/reviews/api/node-core-library.api.ts @@ -278,6 +278,8 @@ class PackageName { // @public class Path { static isUnder(childPath: string, parentFolderPath: string): boolean; + // (undocumented) + static isUnderOrEqual(childPath: string, parentFolderPath: string): boolean; } // @public diff --git a/libraries/node-core-library/src/Path.ts b/libraries/node-core-library/src/Path.ts index 2cf827526b6..6c9f0be3447 100644 --- a/libraries/node-core-library/src/Path.ts +++ b/libraries/node-core-library/src/Path.ts @@ -10,6 +10,8 @@ import * as path from 'path'; * @public */ export class Path { + private static _relativePathRegex: RegExp = /^[.\/\\]+$/; + /** * Returns true if childPath refers to a location under parentFolderPath. * @remarks @@ -21,6 +23,11 @@ export class Path { // "../.." or "..\\..", which consists entirely of periods and slashes. // (Note that something like "....t" is actually a valid filename, but "...." is not.) const relativePath: string = path.relative(childPath, parentFolderPath); - return /^[.\/\\]+$/.test(relativePath); + return Path._relativePathRegex.test(relativePath); + } + + public static isUnderOrEqual(childPath: string, parentFolderPath: string): boolean { + const relativePath: string = path.relative(childPath, parentFolderPath); + return relativePath === '' || Path._relativePathRegex.test(relativePath); } } From d48c7ebbce88d6d86536ab41dd8e5eb670df6e79 Mon Sep 17 00:00:00 2001 From: Nick Pape Date: Thu, 27 Sep 2018 19:44:21 -0700 Subject: [PATCH 22/27] Fix small bug --- apps/rush-lib/src/cli/actions/AddAction.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/rush-lib/src/cli/actions/AddAction.ts b/apps/rush-lib/src/cli/actions/AddAction.ts index 0e469c25aec..0028ecc8cea 100644 --- a/apps/rush-lib/src/cli/actions/AddAction.ts +++ b/apps/rush-lib/src/cli/actions/AddAction.ts @@ -109,7 +109,7 @@ export class AddAction extends BaseRushAction { return Promise.reject(new Error(`The package name "${packageName}" is not valid.`)); } - if (version && !semver.validRange(version) && !semver.valid(version)) { + if (version && version !== 'latest' && !semver.validRange(version) && !semver.valid(version)) { return Promise.reject(new Error(`The SemVer specifier "${version}" is not valid.`)); } From 128da2fb77c19fd52e1566d02c9769036ea3d758 Mon Sep 17 00:00:00 2001 From: Nick Pape Date: Thu, 27 Sep 2018 19:46:52 -0700 Subject: [PATCH 23/27] Minor changes --- apps/rush-lib/src/api/PackageJsonEditor.ts | 12 ++++++------ .../test/__snapshots__/CommandLineHelp.test.ts.snap | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/rush-lib/src/api/PackageJsonEditor.ts b/apps/rush-lib/src/api/PackageJsonEditor.ts index 647112f2eb4..1f74c21da51 100644 --- a/apps/rush-lib/src/api/PackageJsonEditor.ts +++ b/apps/rush-lib/src/api/PackageJsonEditor.ts @@ -71,8 +71,6 @@ export class PackageJsonEditor { private readonly _data: IPackageJson; private readonly _dependencies: Map; private readonly _peerDependencies: Map; - - private _onChange: () => void; private _modified: boolean; public static load(filepath: string): PackageJsonEditor { @@ -114,7 +112,7 @@ export class PackageJsonEditor { dependency.setDependencyType(dependencyType); } else { const dependency: PackageJsonDependency - = new PackageJsonDependency(packageName, newVersion, dependencyType, this._onChange); + = new PackageJsonDependency(packageName, newVersion, dependencyType, this._onChange.bind(this)); this._dependencies.set(packageName, dependency); } } @@ -140,9 +138,7 @@ export class PackageJsonEditor { const optionalDependencies: { [key: string]: string } = data.optionalDependencies || {}; const peerDependencies: { [key: string]: string } = data.peerDependencies || {}; - this._onChange = () => { - this._modified = true; - }; + this._onChange = this._onChange.bind(this); Object.keys(dependencies || {}).forEach((dependency: string) => { if (devDependencies[dependency]) { @@ -176,6 +172,10 @@ export class PackageJsonEditor { }); } + private _onChange(): void { + this._modified = true; + } + private _normalize(): IPackageJson { delete this._data.dependencies; delete this._data.devDependencies; diff --git a/apps/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap b/apps/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap index 579c827bfd4..942b076e450 100644 --- a/apps/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap +++ b/apps/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap @@ -59,8 +59,8 @@ Adds a specified package as a dependency of the current project (as determined by the current working directory) and then runs \\"rush update\\". If no version is specified, a version will be automatically detected (typically either the latest version or a version that won't break the -\\"ensureConsistentVersions\\" policy). If a version range is specified, the -latest version in the range will be used. The version will be automatically +ensureConsistentVersions policy). If a version range is specified, the latest +version in the range will be used. The version will be automatically prepended with a tilde, unless the \\"--exact\\" or \\"--caret\\" flags are used. The \\"--make-consistent\\" flag can be used to update all packages with the dependency. From daf87a2144754a357ad33c800af07b21230d2f08 Mon Sep 17 00:00:00 2001 From: Nick Pape Date: Thu, 27 Sep 2018 19:47:45 -0700 Subject: [PATCH 24/27] more bugs --- apps/rush-lib/src/api/PackageJsonEditor.ts | 12 ++++++------ .../test/__snapshots__/CommandLineHelp.test.ts.snap | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/rush-lib/src/api/PackageJsonEditor.ts b/apps/rush-lib/src/api/PackageJsonEditor.ts index 1f74c21da51..fcbd6dd6e8c 100644 --- a/apps/rush-lib/src/api/PackageJsonEditor.ts +++ b/apps/rush-lib/src/api/PackageJsonEditor.ts @@ -138,7 +138,7 @@ export class PackageJsonEditor { const optionalDependencies: { [key: string]: string } = data.optionalDependencies || {}; const peerDependencies: { [key: string]: string } = data.peerDependencies || {}; - this._onChange = this._onChange.bind(this); + const _onChange: () => void = this._onChange.bind(this); Object.keys(dependencies || {}).forEach((dependency: string) => { if (devDependencies[dependency]) { @@ -149,7 +149,7 @@ export class PackageJsonEditor { } this._dependencies.set(dependency, - new PackageJsonDependency(dependency, dependencies[dependency], DependencyType.Regular, this._onChange)); + new PackageJsonDependency(dependency, dependencies[dependency], DependencyType.Regular, _onChange)); }); Object.keys(devDependencies || {}).forEach((dependency: string) => { @@ -158,17 +158,17 @@ export class PackageJsonEditor { } this._dependencies.set(dependency, - new PackageJsonDependency(dependency, devDependencies[dependency], DependencyType.Dev, this._onChange)); + new PackageJsonDependency(dependency, devDependencies[dependency], DependencyType.Dev, _onChange)); }); Object.keys(optionalDependencies || {}).forEach((dependency: string) => { - this._dependencies.set(dependency, new PackageJsonDependency(dependency, optionalDependencies[dependency], - DependencyType.Optional, this._onChange)); + this._dependencies.set(dependency, + new PackageJsonDependency(dependency, optionalDependencies[dependency], DependencyType.Optional, _onChange)); }); Object.keys(peerDependencies || {}).forEach((dependency: string) => { this._peerDependencies.set(dependency, - new PackageJsonDependency(dependency, peerDependencies[dependency], DependencyType.Peer, this._onChange)); + new PackageJsonDependency(dependency, peerDependencies[dependency], DependencyType.Peer, _onChange)); }); } diff --git a/apps/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap b/apps/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap index 942b076e450..579c827bfd4 100644 --- a/apps/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap +++ b/apps/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap @@ -59,8 +59,8 @@ Adds a specified package as a dependency of the current project (as determined by the current working directory) and then runs \\"rush update\\". If no version is specified, a version will be automatically detected (typically either the latest version or a version that won't break the -ensureConsistentVersions policy). If a version range is specified, the latest -version in the range will be used. The version will be automatically +\\"ensureConsistentVersions\\" policy). If a version range is specified, the +latest version in the range will be used. The version will be automatically prepended with a tilde, unless the \\"--exact\\" or \\"--caret\\" flags are used. The \\"--make-consistent\\" flag can be used to update all packages with the dependency. From 91b5592e2f719cb796b1f2d689a0afcb72189801 Mon Sep 17 00:00:00 2001 From: Nick Pape Date: Thu, 27 Sep 2018 19:51:28 -0700 Subject: [PATCH 25/27] small bug --- apps/rush-lib/src/api/PackageJsonEditor.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/rush-lib/src/api/PackageJsonEditor.ts b/apps/rush-lib/src/api/PackageJsonEditor.ts index fcbd6dd6e8c..e0f587cc16d 100644 --- a/apps/rush-lib/src/api/PackageJsonEditor.ts +++ b/apps/rush-lib/src/api/PackageJsonEditor.ts @@ -115,9 +115,11 @@ export class PackageJsonEditor { = new PackageJsonDependency(packageName, newVersion, dependencyType, this._onChange.bind(this)); this._dependencies.set(packageName, dependency); } + this._modified = true; } public saveIfModified(): boolean { + console.log(this.filePath + ': ' + this._modified); if (this._modified) { JsonFile.save(this._normalize(), this._filepath); this._modified = false; @@ -129,6 +131,7 @@ export class PackageJsonEditor { private constructor(filepath: string, data: IPackageJson) { this._filepath = filepath; this._data = data; + this._modified = false; this._dependencies = new Map(); this._peerDependencies = new Map(); From a115ad5c4472174386ab5fbda959535a225557b5 Mon Sep 17 00:00:00 2001 From: Nick Pape Date: Thu, 27 Sep 2018 19:51:56 -0700 Subject: [PATCH 26/27] remove debugging: --- apps/rush-lib/src/api/PackageJsonEditor.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/rush-lib/src/api/PackageJsonEditor.ts b/apps/rush-lib/src/api/PackageJsonEditor.ts index e0f587cc16d..0c48d03886e 100644 --- a/apps/rush-lib/src/api/PackageJsonEditor.ts +++ b/apps/rush-lib/src/api/PackageJsonEditor.ts @@ -119,7 +119,6 @@ export class PackageJsonEditor { } public saveIfModified(): boolean { - console.log(this.filePath + ': ' + this._modified); if (this._modified) { JsonFile.save(this._normalize(), this._filepath); this._modified = false; From 2ac10441b8c2d3a36e81b0905cb4bb28092d26a8 Mon Sep 17 00:00:00 2001 From: Nick Pape Date: Thu, 27 Sep 2018 19:58:47 -0700 Subject: [PATCH 27/27] Missing changefile --- .../nickpape-rush-add_2018-09-28-02-58.json | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 common/changes/@microsoft/node-core-library/nickpape-rush-add_2018-09-28-02-58.json diff --git a/common/changes/@microsoft/node-core-library/nickpape-rush-add_2018-09-28-02-58.json b/common/changes/@microsoft/node-core-library/nickpape-rush-add_2018-09-28-02-58.json new file mode 100644 index 00000000000..47a43de9eb2 --- /dev/null +++ b/common/changes/@microsoft/node-core-library/nickpape-rush-add_2018-09-28-02-58.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@microsoft/node-core-library", + "comment": "Add `Path.isUnderOrEquals()`", + "type": "minor" + } + ], + "packageName": "@microsoft/node-core-library", + "email": "nickpape-msft@users.noreply.github.com" +} \ No newline at end of file