From 4175e60f99ececc03d00a7840caa8391527e4c1b Mon Sep 17 00:00:00 2001 From: Zoltan Kochan Date: Sun, 26 Feb 2023 18:41:10 +0200 Subject: [PATCH] feat: install --update (#7085) ## Proposed Changes - `bit install --update` updates all dependencies (direct and indirect). Existing semver ranges are respected. - Newly installed dependency are saved in `workspace.jsonc` with the `^` prefix by default. --- e2e/harmony/deduplication.e2e.ts | 2 +- e2e/harmony/import-harmony.e2e.ts | 4 +- e2e/harmony/imported-component-deps.e2e.ts | 16 ++- e2e/harmony/install.e2e.ts | 117 ++++++++++++++++++ .../dependency-resolver.main.runtime.ts | 15 ++- .../dependency-resolver/package-manager.ts | 3 + scopes/dependencies/pnpm/lynx.ts | 10 +- .../dependencies/pnpm/pnpm.package-manager.ts | 1 + .../dependencies/yarn/yarn.package-manager.ts | 2 + scopes/workspace/install/install.cmd.tsx | 5 +- .../workspace/install/install.main.runtime.ts | 11 +- 11 files changed, 170 insertions(+), 16 deletions(-) diff --git a/e2e/harmony/deduplication.e2e.ts b/e2e/harmony/deduplication.e2e.ts index 21e1eda3a1ec..fcd0c461b4f1 100644 --- a/e2e/harmony/deduplication.e2e.ts +++ b/e2e/harmony/deduplication.e2e.ts @@ -194,7 +194,7 @@ const get = require("lodash.get");` }); it("should install the package from the root manifest when the component doesn't have a policy for it", () => { const comp4Output = helper.command.showComponentParsed('comp4'); - expect(comp4Output.packageDependencies['lodash.get']).to.equal('4.4.2'); + expect(comp4Output.packageDependencies['lodash.get']).to.equal('^4.4.2'); }); }); }); diff --git a/e2e/harmony/import-harmony.e2e.ts b/e2e/harmony/import-harmony.e2e.ts index 3e3bdb3e2828..dc5537980ccf 100644 --- a/e2e/harmony/import-harmony.e2e.ts +++ b/e2e/harmony/import-harmony.e2e.ts @@ -289,7 +289,7 @@ describe('import functionality on Harmony', function () { // intermediate step, make sure the types are saved in the const show = helper.command.showComponentParsed('bar'); - expect(show.devPackageDependencies).to.include({ '@types/cors': '2.8.10' }); + expect(show.devPackageDependencies).to.include({ '@types/cors': '^2.8.10' }); helper.command.tagAllWithoutBuild(); helper.command.export(); @@ -303,7 +303,7 @@ describe('import functionality on Harmony', function () { }); it('bit show should show the typed dependency', () => { const show = helper.command.showComponentParsed('bar'); - expect(show.devPackageDependencies).to.include({ '@types/cors': '2.8.10' }); + expect(show.devPackageDependencies).to.include({ '@types/cors': '^2.8.10' }); }); }); }); diff --git a/e2e/harmony/imported-component-deps.e2e.ts b/e2e/harmony/imported-component-deps.e2e.ts index 14e1f8d436c8..27972653285c 100644 --- a/e2e/harmony/imported-component-deps.e2e.ts +++ b/e2e/harmony/imported-component-deps.e2e.ts @@ -1,4 +1,6 @@ +import { resolveFrom } from '@teambit/toolbox.modules.module-resolver'; import { expect } from 'chai'; +import fs from 'fs-extra'; import path from 'path'; import Helper from '../../src/e2e-helper/e2e-helper'; import NpmCiRegistry, { supportNpmCiRegistryTesting } from '../npm-ci-registry'; @@ -49,10 +51,18 @@ const isPositive = require('is-positive'); ).to.eq('0.0.2'); }); it('should install package dependencies from their respective models to the imported components', () => { - expect(helper.fs.readJsonFile(`node_modules/is-positive/package.json`).version).to.eq('1.0.0'); expect( - helper.fs.readJsonFile( - path.join(helper.scopes.remoteWithoutOwner, `comp2/node_modules/is-positive/package.json`) + fs.readJsonSync( + resolveFrom(path.join(helper.fixtures.scopes.localPath, helper.scopes.remoteWithoutOwner, 'comp1'), [ + 'is-positive/package.json', + ]) + ).version + ).to.eq('1.0.0'); + expect( + fs.readJsonSync( + resolveFrom(path.join(helper.fixtures.scopes.localPath, helper.scopes.remoteWithoutOwner, 'comp2'), [ + 'is-positive/package.json', + ]) ).version ).to.eq('2.0.0'); }); diff --git a/e2e/harmony/install.e2e.ts b/e2e/harmony/install.e2e.ts index 62c9b5eda8bd..d794c54f3da9 100644 --- a/e2e/harmony/install.e2e.ts +++ b/e2e/harmony/install.e2e.ts @@ -1,5 +1,6 @@ import path from 'path'; import fs from 'fs-extra'; +import { addDistTag } from '@pnpm/registry-mock'; import { IssuesClasses } from '@teambit/component-issues'; import { expect } from 'chai'; import Helper from '../../src/e2e-helper/e2e-helper'; @@ -65,3 +66,119 @@ describe('install command', function () { }); }); }); + +(supportNpmCiRegistryTesting ? describe : describe.skip)('install --update', function () { + this.timeout(0); + let helper: Helper; + describe('using pnpm', () => { + let npmCiRegistry: NpmCiRegistry; + before(async () => { + helper = new Helper({ scopesOptions: { remoteScopeWithDot: true } }); + helper.scopeHelper.setNewLocalAndRemoteScopes(); + helper.bitJsonc.setPackageManager(`teambit.dependencies/pnpm`); + npmCiRegistry = new NpmCiRegistry(helper); + await npmCiRegistry.init(); + + helper.command.setConfig('registry', npmCiRegistry.getRegistryUrl()); + await addDistTag({ package: '@pnpm.e2e/pkg-with-1-dep', version: '100.0.0', distTag: 'latest' }); + await addDistTag({ package: '@pnpm.e2e/dep-of-pkg-with-1-dep', version: '100.0.0', distTag: 'latest' }); + helper.command.install('@pnpm.e2e/dep-of-pkg-with-1-dep @pnpm.e2e/parent-of-pkg-with-1-dep'); + await addDistTag({ package: '@pnpm.e2e/pkg-with-1-dep', version: '100.1.0', distTag: 'latest' }); + await addDistTag({ package: '@pnpm.e2e/dep-of-pkg-with-1-dep', version: '101.0.0', distTag: 'latest' }); + helper.command.install('--update'); + }); + after(() => { + helper.command.delConfig('registry'); + npmCiRegistry.destroy(); + helper.scopeHelper.destroy(); + }); + it('should update direct dependency inside existing range', async () => { + const manifest = fs.readJSONSync( + path.join(helper.fixtures.scopes.localPath, 'node_modules/@pnpm.e2e/dep-of-pkg-with-1-dep/package.json') + ); + expect(manifest.version).to.eq('100.1.0'); + }); + it('should update subdependency inside existing range', async () => { + const dirs = fs.readdirSync(path.join(helper.fixtures.scopes.localPath, 'node_modules/.pnpm')); + expect(dirs).to.include('@pnpm.e2e+pkg-with-1-dep@100.1.0'); + }); + }); +}); + +describe('install new dependencies', function () { + this.timeout(0); + let helper: Helper; + let bitJsonc; + describe('using pnpm', () => { + before(() => { + helper = new Helper({ scopesOptions: { remoteScopeWithDot: true } }); + helper.scopeHelper.setNewLocalAndRemoteScopes(); + helper.extensions.bitJsonc.setPackageManager('teambit.dependencies/pnpm'); + helper.command.install('is-positive@~1.0.0 is-odd@1.0.0 is-even@1 is-negative'); + bitJsonc = helper.bitJsonc.read(); + }); + after(() => { + helper.scopeHelper.destroy(); + }); + it('should add new dependency preserving the ~ prefix', () => { + expect(bitJsonc['teambit.dependencies/dependency-resolver'].policy.dependencies['is-positive']).to.equal( + '~1.0.0' + ); + }); + it('should add new dependency with ^ prefix if the dependency was installed by specifying the exact version', () => { + expect(bitJsonc['teambit.dependencies/dependency-resolver'].policy.dependencies['is-odd']).to.equal('^1.0.0'); + }); + it('should add new dependency with ^ prefix if the dependency was installed by specifying a range not using ~', () => { + expect(bitJsonc['teambit.dependencies/dependency-resolver'].policy.dependencies['is-even']).to.equal('^1.0.0'); + }); + it('should add new dependency with ^ prefix by default', () => { + expect(bitJsonc['teambit.dependencies/dependency-resolver'].policy.dependencies['is-negative'][0]).to.equal('^'); + }); + }); + describe('using yarn', () => { + before(() => { + helper = new Helper({ scopesOptions: { remoteScopeWithDot: true } }); + helper.scopeHelper.setNewLocalAndRemoteScopes(); + helper.extensions.bitJsonc.setPackageManager('teambit.dependencies/yarn'); + helper.command.install('is-positive@~1.0.0 is-odd@1.0.0 is-even@1 is-negative'); + bitJsonc = helper.bitJsonc.read(); + }); + after(() => { + helper.scopeHelper.destroy(); + }); + it('should add new dependency preserving the ~ prefix', () => { + expect(bitJsonc['teambit.dependencies/dependency-resolver'].policy.dependencies['is-positive']).to.equal( + '~1.0.0' + ); + }); + it('should add new dependency with ^ prefix if the dependency was installed by specifying the exact version', () => { + expect(bitJsonc['teambit.dependencies/dependency-resolver'].policy.dependencies['is-odd']).to.equal('^1.0.0'); + }); + it('should add new dependency with ^ prefix if the dependency was installed by specifying a range not using ~', () => { + expect(bitJsonc['teambit.dependencies/dependency-resolver'].policy.dependencies['is-even']).to.equal('^1.0.0'); + }); + it('should add new dependency with ^ prefix by default', () => { + expect(bitJsonc['teambit.dependencies/dependency-resolver'].policy.dependencies['is-negative'][0]).to.equal('^'); + }); + }); +}); + +describe('named install', function () { + this.timeout(0); + let helper: Helper; + let bitJsonc; + before(() => { + helper = new Helper({ scopesOptions: { remoteScopeWithDot: true } }); + helper.scopeHelper.setNewLocalAndRemoteScopes(); + helper.extensions.bitJsonc.setPackageManager('teambit.dependencies/pnpm'); + helper.command.install('is-positive@1.0.0'); + helper.command.install('is-positive'); + bitJsonc = helper.bitJsonc.read(); + }); + after(() => { + helper.scopeHelper.destroy(); + }); + it('should override already existing dependency with the latest version', () => { + expect(bitJsonc['teambit.dependencies/dependency-resolver'].policy.dependencies['is-positive']).to.equal('^3.1.0'); + }); +}); diff --git a/scopes/dependencies/dependency-resolver/dependency-resolver.main.runtime.ts b/scopes/dependencies/dependency-resolver/dependency-resolver.main.runtime.ts index 787087007da8..8f1129a85484 100644 --- a/scopes/dependencies/dependency-resolver/dependency-resolver.main.runtime.ts +++ b/scopes/dependencies/dependency-resolver/dependency-resolver.main.runtime.ts @@ -403,10 +403,21 @@ export class DependencyResolverMain { } getSavePrefix(): string { - return this.config.savePrefix || ''; + return this.config.savePrefix || '^'; } - getVersionWithSavePrefix(version: string, overridePrefix?: string): string { + getVersionWithSavePrefix({ + version, + overridePrefix, + wantedRange, + }: { + version: string; + overridePrefix?: string; + wantedRange?: string; + }): string { + if (wantedRange && ['~', '^'].includes(wantedRange[0])) { + return wantedRange; + } const prefix = overridePrefix || this.getSavePrefix(); const versionWithPrefix = `${prefix}${version}`; if (!semver.validRange(versionWithPrefix)) { diff --git a/scopes/dependencies/dependency-resolver/package-manager.ts b/scopes/dependencies/dependency-resolver/package-manager.ts index a2b4cd7f16ee..f550cd8e36ac 100644 --- a/scopes/dependencies/dependency-resolver/package-manager.ts +++ b/scopes/dependencies/dependency-resolver/package-manager.ts @@ -49,6 +49,8 @@ export type PackageManagerInstallOptions = { peerDependencyRules?: PeerDependencyRules; includeOptionalDeps?: boolean; + + updateAll?: boolean; }; export type PackageManagerGetPeerDependencyIssuesOptions = PackageManagerInstallOptions; @@ -56,6 +58,7 @@ export type PackageManagerGetPeerDependencyIssuesOptions = PackageManagerInstall export type ResolvedPackageVersion = { packageName: string; version: string | null; + wantedRange?: string; isSemver: boolean; resolvedVia?: string; }; diff --git a/scopes/dependencies/pnpm/lynx.ts b/scopes/dependencies/pnpm/lynx.ts index c4d746d2c9e0..6ba1eb2a012c 100644 --- a/scopes/dependencies/pnpm/lynx.ts +++ b/scopes/dependencies/pnpm/lynx.ts @@ -163,6 +163,7 @@ export async function install( proxyConfig: PackageManagerProxyConfig = {}, networkConfig: PackageManagerNetworkConfig = {}, options: { + updateAll?: boolean; nodeLinker?: 'hoisted' | 'isolated'; overrides?: Record; rootComponents?: boolean; @@ -240,6 +241,8 @@ export async function install( ignoreMissing: ['*'], ...options?.peerDependencyRules, }, + update: options.updateAll, + depth: options.updateAll ? Infinity : 0, }; const stopReporting = initDefaultReporter({ @@ -452,17 +455,18 @@ export async function resolveRemoteVersion( alias: parsedPackage.name, pref: parsedPackage.version, }; - const isValidRange = parsedPackage.version ? !!semver.validRange(parsedPackage.version) : false; resolveOpts.registry = registry; const val = await resolve(wantedDep, resolveOpts); if (!val.manifest) { throw new BitError('The resolved package has no manifest'); } - const version = isValidRange ? parsedPackage.version : val.manifest.version; + const wantedRange = + parsedPackage.version && semver.validRange(parsedPackage.version) ? parsedPackage.version : undefined; return { packageName: val.manifest.name, - version, + version: val.manifest.version, + wantedRange, isSemver: true, resolvedVia: val.resolvedVia, }; diff --git a/scopes/dependencies/pnpm/pnpm.package-manager.ts b/scopes/dependencies/pnpm/pnpm.package-manager.ts index 390a9342a55a..c7bb7eaba1fa 100644 --- a/scopes/dependencies/pnpm/pnpm.package-manager.ts +++ b/scopes/dependencies/pnpm/pnpm.package-manager.ts @@ -67,6 +67,7 @@ export class PnpmPackageManager implements PackageManager { sideEffectsCacheRead: installOptions.sideEffectsCache ?? true, sideEffectsCacheWrite: installOptions.sideEffectsCache ?? true, pnpmHomeDir: config.pnpmHomeDir, + updateAll: installOptions.updateAll, }, this.logger ); diff --git a/scopes/dependencies/yarn/yarn.package-manager.ts b/scopes/dependencies/yarn/yarn.package-manager.ts index a149f5044132..c34b6f1834ea 100644 --- a/scopes/dependencies/yarn/yarn.package-manager.ts +++ b/scopes/dependencies/yarn/yarn.package-manager.ts @@ -436,6 +436,7 @@ export class YarnPackageManager implements PackageManager { return { packageName: parsedPackage.name, version: parsedVersion, + wantedRange: parsedVersion, isSemver: true, }; } @@ -478,6 +479,7 @@ export class YarnPackageManager implements PackageManager { return { packageName: parsedPackage.name, version, + wantedRange: parsedVersion, isSemver: true, }; } diff --git a/scopes/workspace/install/install.cmd.tsx b/scopes/workspace/install/install.cmd.tsx index 244e85ce1f0c..ca67f76d9691 100644 --- a/scopes/workspace/install/install.cmd.tsx +++ b/scopes/workspace/install/install.cmd.tsx @@ -11,6 +11,7 @@ type InstallCmdOptions = { skipDedupe: boolean; skipImport: boolean; skipCompile: boolean; + update: boolean; updateExisting: boolean; savePrefix: string; addMissingPeers: boolean; @@ -29,8 +30,9 @@ export default class InstallCmd implements Command { options = [ ['v', 'variants ', 'add packages to specific variants'], ['t', 'type [lifecycleType]', '"runtime" (default) or "peer" (dev is not a valid option)'], + ['u', 'update', 'update all dependencies'], [ - 'u', + '', 'update-existing [updateExisting]', 'DEPRECATED (not needed anymore, it is the default now). update existing dependencies version and types', ], @@ -74,6 +76,7 @@ export default class InstallCmd implements Command { addMissingPeers: options.addMissingPeers, compile: !options.skipCompile, includeOptionalDeps: !options.noOptional, + updateAll: options.update, }; const components = await this.install.install(packages, installOpts); const endTime = Date.now(); diff --git a/scopes/workspace/install/install.main.runtime.ts b/scopes/workspace/install/install.main.runtime.ts index 2a5f1fc0b9ad..afb92ad601e0 100644 --- a/scopes/workspace/install/install.main.runtime.ts +++ b/scopes/workspace/install/install.main.runtime.ts @@ -67,6 +67,7 @@ export type WorkspaceInstallOptions = { savePrefix?: string; compile?: boolean; includeOptionalDeps?: boolean; + updateAll?: boolean; }; export type ModulesInstallOptions = Omit; @@ -159,10 +160,11 @@ export class InstallMain { const newWorkspacePolicyEntries: WorkspacePolicyEntry[] = []; resolvedPackages.forEach((resolvedPackage) => { if (resolvedPackage.version) { - const versionWithPrefix = this.dependencyResolver.getVersionWithSavePrefix( - resolvedPackage.version, - options?.savePrefix - ); + const versionWithPrefix = this.dependencyResolver.getVersionWithSavePrefix({ + version: resolvedPackage.version, + overridePrefix: options?.savePrefix, + wantedRange: resolvedPackage.wantedRange, + }); newWorkspacePolicyEntries.push({ dependencyId: resolvedPackage.packageName, value: { @@ -200,6 +202,7 @@ export class InstallMain { packageImportMethod: this.dependencyResolver.config.packageImportMethod, rootComponents: hasRootComponents, nodeLinker: this.dependencyResolver.nodeLinker(), + updateAll: options?.updateAll, }; // TODO: pass get install options const installer = this.dependencyResolver.getInstaller({});