From 7010ee124611d471b03cec3ac49647ee49e4f236 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20No=C3=ABl?= Date: Fri, 26 May 2017 12:38:03 +0200 Subject: [PATCH 1/4] refactor some test helpers --- __tests__/commands/_helpers.js | 20 ++++++++++++++++--- .../commands/install/integration-deduping.js | 10 +++++----- .../commands/install/integration-hoisting.js | 9 +++------ __tests__/commands/install/lockfiles.js | 4 ++-- 4 files changed, 27 insertions(+), 16 deletions(-) diff --git a/__tests__/commands/_helpers.js b/__tests__/commands/_helpers.js index 92ea22e41b..7d496de79f 100644 --- a/__tests__/commands/_helpers.js +++ b/__tests__/commands/_helpers.js @@ -46,9 +46,23 @@ export function explodeLockfile(lockfile: string): Array { } export async function getPackageVersion(config: Config, packagePath: string): Promise { - const loc = path.join(config.cwd, `node_modules/${packagePath.replace(/\//g, '/node_modules/')}/package.json`); - const json = JSON.parse(await fs.readFile(loc)); - return json.version; + return (await getPackageManifest(config, packagePath)).version; +} + +export function getPackageManifest(config: Config, packagePath: string): Promise { + return fs.readJson(getPackageManifestPath(config, packagePath)); +} + +export function getPackageManifestPath(config: Config, packagePath: string): string { + return path.join(getPackagePath(config, packagePath), 'package.json'); +} + +export function isPackagePresent(config: Config, packagePath: string): Promise { + return fs.exists(getPackagePath(config, packagePath)); +} + +export function getPackagePath(config: Config, packagePath: string): string { + return path.join(config.cwd, `node_modules/${packagePath.replace(/\//g, '/node_modules/')}`); } export function makeConfigFromDirectory(cwd: string, reporter: Reporter, flags: Object = {}): Promise { diff --git a/__tests__/commands/install/integration-deduping.js b/__tests__/commands/install/integration-deduping.js index 51055c229d..06a9d2b93b 100644 --- a/__tests__/commands/install/integration-deduping.js +++ b/__tests__/commands/install/integration-deduping.js @@ -1,6 +1,6 @@ /* @flow */ -import {getPackageVersion, runInstall} from '../_helpers.js'; +import {getPackageVersion, getPackageManifestPath, runInstall} from '../_helpers.js'; import * as fs from '../../../src/util/fs.js'; const path = require('path'); @@ -204,8 +204,8 @@ test.concurrent('install should hardlink repeated dependencies', (): Promise A@2 // C@1 -> A@2 (this is hardlink to B@1->A@2) return runInstall({linkDuplicates: true}, 'hardlink-repeated-dependencies', async config => { - const b_a = await fs.stat(path.join(config.cwd, 'node_modules/b/node_modules/a/package.json')); - const c_a = await fs.stat(path.join(config.cwd, 'node_modules/c/node_modules/a/package.json')); + const b_a = await fs.stat(getPackageManifestPath(config, 'b/a')); + const c_a = await fs.stat(getPackageManifestPath(config, 'c/a')); expect(b_a.ino).toEqual(c_a.ino); }); }); @@ -215,8 +215,8 @@ test.concurrent('install should not hardlink repeated dependencies if linkDuplic // B@1 -> A@2 // C@1 -> A@2 return runInstall({linkDuplicates: false}, 'hardlink-repeated-dependencies', async config => { - const b_a = await fs.stat(path.join(config.cwd, 'node_modules/b/node_modules/a/package.json')); - const c_a = await fs.stat(path.join(config.cwd, 'node_modules/c/node_modules/a/package.json')); + const b_a = await fs.stat(getPackageManifestPath(config, 'b/a')); + const c_a = await fs.stat(getPackageManifestPath(config, 'c/a')); expect(b_a.ino).not.toEqual(c_a.ino); }); }); diff --git a/__tests__/commands/install/integration-hoisting.js b/__tests__/commands/install/integration-hoisting.js index de07a0903a..b9623b1959 100644 --- a/__tests__/commands/install/integration-hoisting.js +++ b/__tests__/commands/install/integration-hoisting.js @@ -1,9 +1,6 @@ /* @flow */ -import {getPackageVersion, runInstall} from '../_helpers.js'; -import * as fs from '../../../src/util/fs.js'; - -const path = require('path'); +import {getPackageVersion, isPackagePresent, runInstall} from '../_helpers.js'; jasmine.DEFAULT_TIMEOUT_INTERVAL = 120000; @@ -21,8 +18,8 @@ test.concurrent( 'install hoister should not install prioritised popular transitive devDependencies in --prod mode', (): Promise => { return runInstall({production: true}, 'install-prod-prioritized-popular-transitive-dev-dep', async config => { - expect(await fs.exists(path.join(config.cwd, 'node_modules', 'a'))).toEqual(false); - expect(await fs.exists(path.join(config.cwd, 'node_modules', 'b'))).toEqual(false); + expect(await isPackagePresent(config, 'a')).toEqual(false); + expect(await isPackagePresent(config, 'b')).toEqual(false); }); }, ); diff --git a/__tests__/commands/install/lockfiles.js b/__tests__/commands/install/lockfiles.js index dfa63fd517..7302560cce 100644 --- a/__tests__/commands/install/lockfiles.js +++ b/__tests__/commands/install/lockfiles.js @@ -6,7 +6,7 @@ import * as reporters from '../../../src/reporters/index.js'; import {Install} from '../../../src/cli/commands/install.js'; import Lockfile from '../../../src/lockfile/wrapper.js'; import * as fs from '../../../src/util/fs.js'; -import {getPackageVersion, runInstall} from '../_helpers.js'; +import {getPackageVersion, isPackagePresent, runInstall} from '../_helpers.js'; import {promisify} from '../../../src/util/promise'; jasmine.DEFAULT_TIMEOUT_INTERVAL = 150000; @@ -152,7 +152,7 @@ test.concurrent('install have a clean node_modules after lockfile update (branch await reinstall.init(); expect(await getPackageVersion(config, 'dep-a')).toEqual('1.2.0'); - expect(await fs.exists(path.join(config.cwd, 'node_modules/dep-b'))).toEqual(false); + expect(await isPackagePresent(config, 'dep-b')).toEqual(false); }); }); From f40f7c1915664de5bb2a434c3771ae6aedf7f9d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20No=C3=ABl?= Date: Sun, 9 Jul 2017 11:58:20 +0200 Subject: [PATCH 2/4] first failing tests --- __tests__/commands/install/resolutions.js | 29 +++++++++++++++++++ .../install/resolutions/a-1/package.json | 7 +++++ .../install/resolutions/a-2/package.json | 7 +++++ .../install/resolutions/b-1/package.json | 7 +++++ .../install/resolutions/c-1/package.json | 7 +++++ .../install/resolutions/d1-1/package.json | 7 +++++ .../install/resolutions/d1-2/package.json | 7 +++++ .../install/resolutions/d1-3/package.json | 7 +++++ .../install/resolutions/d2-1/package.json | 4 +++ .../resolutions/simple-exact/package.json | 11 +++++++ .../resolutions/subtree-exact/package.json | 11 +++++++ 11 files changed, 104 insertions(+) create mode 100644 __tests__/commands/install/resolutions.js create mode 100644 __tests__/fixtures/install/resolutions/a-1/package.json create mode 100644 __tests__/fixtures/install/resolutions/a-2/package.json create mode 100644 __tests__/fixtures/install/resolutions/b-1/package.json create mode 100644 __tests__/fixtures/install/resolutions/c-1/package.json create mode 100644 __tests__/fixtures/install/resolutions/d1-1/package.json create mode 100644 __tests__/fixtures/install/resolutions/d1-2/package.json create mode 100644 __tests__/fixtures/install/resolutions/d1-3/package.json create mode 100644 __tests__/fixtures/install/resolutions/d2-1/package.json create mode 100644 __tests__/fixtures/install/resolutions/simple-exact/package.json create mode 100644 __tests__/fixtures/install/resolutions/subtree-exact/package.json diff --git a/__tests__/commands/install/resolutions.js b/__tests__/commands/install/resolutions.js new file mode 100644 index 0000000000..ee3aface82 --- /dev/null +++ b/__tests__/commands/install/resolutions.js @@ -0,0 +1,29 @@ +/* @flow */ + +import {getPackageVersion, isPackagePresent, runInstall} from '../_helpers.js'; + +test.concurrent('install with simple exact resolutions should override all versions', (): Promise => { + return runInstall({}, {source: 'resolutions', cwd: 'simple-exact'}, async config => { + expect(await getPackageVersion(config, 'a')).toEqual('1.0.0'); + expect(await getPackageVersion(config, 'b')).toEqual('1.0.0'); + expect(await getPackageVersion(config, 'd1')).toEqual('2.0.0'); + expect(await getPackageVersion(config, 'd2')).toEqual('1.0.0'); + expect(await isPackagePresent(config, 'a/d1')).toEqual(false); + expect(await isPackagePresent(config, 'a/d2')).toEqual(false); + expect(await isPackagePresent(config, 'b/d1')).toEqual(false); + expect(await isPackagePresent(config, 'b/d2')).toEqual(false); + }); +}); + +test.concurrent('install with subtree exact resolutions should override subtree versions', (): Promise => { + return runInstall({}, {source: 'resolutions', cwd: 'subtree-exact'}, async config => { + expect(await getPackageVersion(config, 'a')).toEqual('1.0.0'); + expect(await getPackageVersion(config, 'b')).toEqual('1.0.0'); + expect(await getPackageVersion(config, 'd1')).toEqual('3.0.0'); + expect(await getPackageVersion(config, 'b/d1')).toEqual('2.0.0'); + expect(await getPackageVersion(config, 'd2')).toEqual('1.0.0'); + expect(await isPackagePresent(config, 'a/d1')).toEqual(false); + expect(await isPackagePresent(config, 'a/d2')).toEqual(false); + expect(await isPackagePresent(config, 'b/d2')).toEqual(false); + }); +}); diff --git a/__tests__/fixtures/install/resolutions/a-1/package.json b/__tests__/fixtures/install/resolutions/a-1/package.json new file mode 100644 index 0000000000..645f410dd2 --- /dev/null +++ b/__tests__/fixtures/install/resolutions/a-1/package.json @@ -0,0 +1,7 @@ +{ + "name": "a", + "version": "1.0.0", + "dependencies": { + "d1": "file:../d1-1" + } +} diff --git a/__tests__/fixtures/install/resolutions/a-2/package.json b/__tests__/fixtures/install/resolutions/a-2/package.json new file mode 100644 index 0000000000..9ac781883c --- /dev/null +++ b/__tests__/fixtures/install/resolutions/a-2/package.json @@ -0,0 +1,7 @@ +{ + "name": "a", + "version": "2.0.0", + "dependencies": { + "d1": "file:../d1-2" + } +} diff --git a/__tests__/fixtures/install/resolutions/b-1/package.json b/__tests__/fixtures/install/resolutions/b-1/package.json new file mode 100644 index 0000000000..2b020f6f2a --- /dev/null +++ b/__tests__/fixtures/install/resolutions/b-1/package.json @@ -0,0 +1,7 @@ +{ + "name": "b", + "version": "1.0.0", + "dependencies": { + "d1": "file:../d1-2" + } +} diff --git a/__tests__/fixtures/install/resolutions/c-1/package.json b/__tests__/fixtures/install/resolutions/c-1/package.json new file mode 100644 index 0000000000..f630919d51 --- /dev/null +++ b/__tests__/fixtures/install/resolutions/c-1/package.json @@ -0,0 +1,7 @@ +{ + "name": "c", + "version": "1.0.0", + "dependencies": { + "a": "file:../a-2" + } +} diff --git a/__tests__/fixtures/install/resolutions/d1-1/package.json b/__tests__/fixtures/install/resolutions/d1-1/package.json new file mode 100644 index 0000000000..d59317b249 --- /dev/null +++ b/__tests__/fixtures/install/resolutions/d1-1/package.json @@ -0,0 +1,7 @@ +{ + "name": "d1", + "version": "1.0.0", + "dependencies": { + "d2": "file:../d2-1" + } +} diff --git a/__tests__/fixtures/install/resolutions/d1-2/package.json b/__tests__/fixtures/install/resolutions/d1-2/package.json new file mode 100644 index 0000000000..4f31cfe9d0 --- /dev/null +++ b/__tests__/fixtures/install/resolutions/d1-2/package.json @@ -0,0 +1,7 @@ +{ + "name": "d1", + "version": "2.0.0", + "dependencies": { + "d2": "file:../d2-1" + } +} diff --git a/__tests__/fixtures/install/resolutions/d1-3/package.json b/__tests__/fixtures/install/resolutions/d1-3/package.json new file mode 100644 index 0000000000..4ef2d38824 --- /dev/null +++ b/__tests__/fixtures/install/resolutions/d1-3/package.json @@ -0,0 +1,7 @@ +{ + "name": "d1", + "version": "3.0.0", + "dependencies": { + "d2": "file:../d2-1" + } +} diff --git a/__tests__/fixtures/install/resolutions/d2-1/package.json b/__tests__/fixtures/install/resolutions/d2-1/package.json new file mode 100644 index 0000000000..4f7cc7df78 --- /dev/null +++ b/__tests__/fixtures/install/resolutions/d2-1/package.json @@ -0,0 +1,4 @@ +{ + "name": "d2", + "version": "1.0.0" +} diff --git a/__tests__/fixtures/install/resolutions/simple-exact/package.json b/__tests__/fixtures/install/resolutions/simple-exact/package.json new file mode 100644 index 0000000000..d47addfbf3 --- /dev/null +++ b/__tests__/fixtures/install/resolutions/simple-exact/package.json @@ -0,0 +1,11 @@ +{ + "name": "project", + "version": "1.0.0", + "dependencies": { + "a": "file:../a-1", + "b": "file:../b-1" + }, + "resolutions": { + "**/d1": "file:../d1-2" + } +} diff --git a/__tests__/fixtures/install/resolutions/subtree-exact/package.json b/__tests__/fixtures/install/resolutions/subtree-exact/package.json new file mode 100644 index 0000000000..3dc0f01e72 --- /dev/null +++ b/__tests__/fixtures/install/resolutions/subtree-exact/package.json @@ -0,0 +1,11 @@ +{ + "name": "project", + "version": "1.0.0", + "dependencies": { + "a": "file:../a-1", + "b": "file:../b-1" + }, + "resolutions": { + "a/d1": "file:../d1-3" + } +} From 5c4644836b680f962225e61cd9371b59fba721c2 Mon Sep 17 00:00:00 2001 From: Kaylie Kwon Date: Sat, 5 Aug 2017 12:57:48 -0700 Subject: [PATCH 3/4] WIP --- __tests__/commands/install/resolutions.js | 11 +-- .../install/resolutions/c-1/package.json | 2 +- .../install/resolutions/d2-1/package.json | 5 +- .../resolutions/subtree-exact/package.json | 8 +- package.json | 1 + src/cli/commands/import.js | 2 +- src/cli/commands/install.js | 18 +++- src/package-request.js | 24 ++--- src/package-resolver.js | 63 ++++++++++-- src/reporters/lang/en.js | 4 + src/resolutions.js | 96 +++++++++++++++++++ src/resolvers/registries/npm-resolver.js | 2 +- src/types.js | 1 + 13 files changed, 192 insertions(+), 45 deletions(-) create mode 100644 src/resolutions.js diff --git a/__tests__/commands/install/resolutions.js b/__tests__/commands/install/resolutions.js index ee3aface82..4eb05b18ba 100644 --- a/__tests__/commands/install/resolutions.js +++ b/__tests__/commands/install/resolutions.js @@ -17,13 +17,10 @@ test.concurrent('install with simple exact resolutions should override all versi test.concurrent('install with subtree exact resolutions should override subtree versions', (): Promise => { return runInstall({}, {source: 'resolutions', cwd: 'subtree-exact'}, async config => { - expect(await getPackageVersion(config, 'a')).toEqual('1.0.0'); - expect(await getPackageVersion(config, 'b')).toEqual('1.0.0'); - expect(await getPackageVersion(config, 'd1')).toEqual('3.0.0'); - expect(await getPackageVersion(config, 'b/d1')).toEqual('2.0.0'); + expect(await getPackageVersion(config, 'left-pad')).toEqual('1.0.0'); expect(await getPackageVersion(config, 'd2')).toEqual('1.0.0'); - expect(await isPackagePresent(config, 'a/d1')).toEqual(false); - expect(await isPackagePresent(config, 'a/d2')).toEqual(false); - expect(await isPackagePresent(config, 'b/d2')).toEqual(false); + expect(await getPackageVersion(config, 'd2/left-pad')).toEqual('1.1.1'); + expect(await getPackageVersion(config, 'c')).toEqual('1.0.0'); + expect(await getPackageVersion(config, 'c/left-pad')).toEqual('1.1.2'); }); }); diff --git a/__tests__/fixtures/install/resolutions/c-1/package.json b/__tests__/fixtures/install/resolutions/c-1/package.json index f630919d51..8162048060 100644 --- a/__tests__/fixtures/install/resolutions/c-1/package.json +++ b/__tests__/fixtures/install/resolutions/c-1/package.json @@ -2,6 +2,6 @@ "name": "c", "version": "1.0.0", "dependencies": { - "a": "file:../a-2" + "left-pad": "~1.1.1" } } diff --git a/__tests__/fixtures/install/resolutions/d2-1/package.json b/__tests__/fixtures/install/resolutions/d2-1/package.json index 4f7cc7df78..c65502e71f 100644 --- a/__tests__/fixtures/install/resolutions/d2-1/package.json +++ b/__tests__/fixtures/install/resolutions/d2-1/package.json @@ -1,4 +1,7 @@ { "name": "d2", - "version": "1.0.0" + "version": "1.0.0", + "dependencies": { + "left-pad": "^1.0.0" + } } diff --git a/__tests__/fixtures/install/resolutions/subtree-exact/package.json b/__tests__/fixtures/install/resolutions/subtree-exact/package.json index 3dc0f01e72..56922159c8 100644 --- a/__tests__/fixtures/install/resolutions/subtree-exact/package.json +++ b/__tests__/fixtures/install/resolutions/subtree-exact/package.json @@ -2,10 +2,12 @@ "name": "project", "version": "1.0.0", "dependencies": { - "a": "file:../a-1", - "b": "file:../b-1" + "left-pad": "1.0.0", + "c": "file:../c-1", + "d2": "file:../d2-1" }, "resolutions": { - "a/d1": "file:../d1-3" + "d2/left-pad": "1.1.1", + "c/**/left-pad": "1.1.2" } } diff --git a/package.json b/package.json index 607769bd6c..e35269c2c4 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "gulp-util": "^3.0.7", "gulp-watch": "^4.3.5", "jest": "20.0.4", + "minimatch": "^3.0.4", "mock-stdin": "^0.3.0", "prettier": "^1.5.2", "temp": "^0.8.3", diff --git a/src/cli/commands/import.js b/src/cli/commands/import.js index 2940e3b6f6..6850745428 100644 --- a/src/cli/commands/import.js +++ b/src/cli/commands/import.js @@ -177,7 +177,7 @@ class ImportPackageRequest extends PackageRequest { } getParentHumanName(): string { - return [this.getRootName()].concat(this.getParentNames()).join(' > '); + return [this.getRootName()].concat(this.parentNames).join(' > '); } reportResolvedRangeMatch(info: Manifest, resolved: Manifest) { diff --git a/src/cli/commands/install.js b/src/cli/commands/install.js index 0b21bac553..67f1b6f595 100644 --- a/src/cli/commands/install.js +++ b/src/cli/commands/install.js @@ -26,6 +26,7 @@ import * as fs from '../../util/fs.js'; import map from '../../util/map.js'; import {version as YARN_VERSION, getInstallationMethod} from '../../util/yarn-version.js'; import WorkspaceLayout from '../../workspace-layout.js'; +import Resolutions from '../../resolutions.js'; const emoji = require('node-emoji'); const invariant = require('invariant'); @@ -165,13 +166,13 @@ export class Install { constructor(flags: Object, config: Config, reporter: Reporter, lockfile: Lockfile) { this.rootManifestRegistries = []; this.rootPatternsToOrigin = map(); - this.resolutions = map(); this.lockfile = lockfile; this.reporter = reporter; this.config = config; this.flags = normalizeFlags(config, flags); - - this.resolver = new PackageResolver(config, lockfile); + this.resolutions = map(); // Legacy resolutions field used for flat install mode + this._resolutions = new Resolutions(config); // Selective resolutions for nested dependencies + this.resolver = new PackageResolver(config, lockfile, this._resolutions); this.integrityChecker = new InstallationIntegrityChecker(config); this.linker = new PackageLinker(config, this.resolver); this.scripts = new PackageInstallScripts(config, this.resolver, this.flags.force); @@ -189,6 +190,7 @@ export class Install { linker: PackageLinker; rootPatternsToOrigin: {[pattern: string]: string}; integrityChecker: InstallationIntegrityChecker; + _resolutions: Resolutions; /** * Create a list of dependency requests from the current directories manifests. @@ -200,6 +202,7 @@ export class Install { ): Promise { const patterns = []; const deps: DependencyRequestPatterns = []; + let resolutionDeps: DependencyRequestPatterns = []; const manifest = {}; const ignorePatterns = []; @@ -234,6 +237,13 @@ export class Install { Object.assign(this.resolutions, projectManifestJson.resolutions); Object.assign(manifest, projectManifestJson); + this._resolutions.init(this.resolutions); + for (const packageName of Object.keys(this._resolutions.resolutionsByPackage)) { + for (const {pattern} of this._resolutions.resolutionsByPackage[packageName]) { + resolutionDeps = [...resolutionDeps, {registry, pattern, optional: false, hint: 'resolution'}]; + } + } + const pushDeps = (depType, manifest: Object, {hint, optional}, isUsed) => { if (ignoreUnusedPatterns && !isUsed) { return; @@ -308,7 +318,7 @@ export class Install { } return { - requests: deps, + requests: [...resolutionDeps, ...deps], patterns, manifest, usedPatterns, diff --git a/src/package-request.js b/src/package-request.js index 6a40f25f7e..213f7fa8b9 100644 --- a/src/package-request.js +++ b/src/package-request.js @@ -25,6 +25,7 @@ type ResolverRegistryNames = $Keys; export default class PackageRequest { constructor(req: DependencyRequestPattern, resolver: PackageResolver) { this.parentRequest = req.parentRequest; + this.parentNames = []; this.lockfile = resolver.lockfile; this.registry = req.registry; this.reporter = resolver.reporter; @@ -38,6 +39,7 @@ export default class PackageRequest { } parentRequest: ?PackageRequest; + parentNames: Array; lockfile: Lockfile; reporter: Reporter; resolver: PackageResolver; @@ -47,20 +49,6 @@ export default class PackageRequest { optional: boolean; foundInfo: ?Manifest; - getParentNames(): Array { - const chain = []; - - let request = this.parentRequest; - while (request) { - const info = this.resolver.getStrictResolvedPattern(request.pattern); - chain.unshift(info.name); - - request = request.parentRequest; - } - - return chain; - } - getLocked(remoteType: string): ?Object { // always prioritise root lockfile const shrunk = this.lockfile.getLocked(this.pattern); @@ -110,7 +98,6 @@ export default class PackageRequest { // "foo": "http://foo.com/bar.tar.gz" // then we use the foo name data.name = name; - return data; } @@ -267,6 +254,7 @@ export default class PackageRequest { !info.fresh || frozen ? this.resolver.getExactVersionMatch(name, solvedRange, info) : this.resolver.getHighestRangeVersionMatch(name, solvedRange, info); + if (resolved) { this.resolver.reportPackageWithExistingVersion(this, info); return; @@ -290,11 +278,10 @@ export default class PackageRequest { ref.setFresh(fresh); info._reference = ref; info._remote = remote; - // start installation of dependencies const promises = []; const deps = []; - + const parentNames = [...this.parentNames, name]; // normal deps for (const depName in info.dependencies) { const depPattern = depName + '@' + info.dependencies[depName]; @@ -306,6 +293,7 @@ export default class PackageRequest { // dependencies of optional dependencies should themselves be optional optional: this.optional, parentRequest: this, + parentNames, }), ); } @@ -320,6 +308,7 @@ export default class PackageRequest { registry: remote.registry, optional: true, parentRequest: this, + parentNames, }), ); } @@ -334,6 +323,7 @@ export default class PackageRequest { registry: remote.registry, optional: false, parentRequest: this, + parentNames, }), ); } diff --git a/src/package-resolver.js b/src/package-resolver.js index f59321ba32..62e37d9c47 100644 --- a/src/package-resolver.js +++ b/src/package-resolver.js @@ -12,6 +12,7 @@ import BlockingQueue from './util/blocking-queue.js'; import Lockfile from './lockfile/wrapper.js'; import map from './util/map.js'; import WorkspaceLayout from './workspace-layout.js'; +import Resolutions from './resolutions'; const invariant = require('invariant'); const semver = require('semver'); @@ -23,11 +24,12 @@ export type ResolverOptions = {| |}; export default class PackageResolver { - constructor(config: Config, lockfile: Lockfile) { + constructor(config: Config, lockfile: Lockfile, resolutions: ?Resolutions) { this.patternsByPackage = map(); this.fetchingPatterns = map(); this.fetchingQueue = new BlockingQueue('resolver fetching'); this.patterns = map(); + this.resolutions = resolutions || new Resolutions(config); this.usedRegistries = new Set(); this.flat = false; @@ -44,6 +46,8 @@ export default class PackageResolver { workspaceLayout: ?WorkspaceLayout; + resolutions: Resolutions; + // list of registries that have been used in this resolution usedRegistries: Set; @@ -401,6 +405,7 @@ export default class PackageResolver { getHighestRangeVersionMatch(name: string, range: string, manifest: ?Manifest): ?Manifest { const patterns = this.patternsByPackage[name]; + if (!patterns) { return null; } @@ -446,12 +451,33 @@ export default class PackageResolver { return matchedPkg; } + /** + * Determine if LockfileEntry is incorrect, remove it from lockfile cache and consider the pattern as new + */ + isLockfileEntryOutdated(version: string, range: string, hasVersion: boolean): boolean { + return !!( + semver.validRange(range) && + semver.valid(version) && + !getExoticResolver(range) && + hasVersion && + !semver.satisfies(version, range) + ); + } + /** * TODO description */ - async find(req: DependencyRequestPattern): Promise { + async find(initialReq: DependencyRequestPattern): Promise { + const req = this.resolveToResolution(initialReq); + + // we've already resolved it with a resolution + if (!req) { + return; + } + const fetchKey = `${req.registry}:${req.pattern}`; + if (this.fetchingPatterns[fetchKey]) { return; } else { @@ -464,16 +490,11 @@ export default class PackageResolver { const lockfileEntry = this.lockfile.getLocked(req.pattern); let fresh = false; + if (lockfileEntry) { const {range, hasVersion} = PackageRequest.normalizePattern(req.pattern); - // lockfileEntry is incorrect, remove it from lockfile cache and consider the pattern as new - if ( - semver.validRange(range) && - semver.valid(lockfileEntry.version) && - !semver.satisfies(lockfileEntry.version, range) && - !getExoticResolver(range) && - hasVersion - ) { + + if (this.isLockfileEntryOutdated(lockfileEntry.version, range, hasVersion)) { this.reporter.warn(this.reporter.lang('incorrectLockfileEntry', req.pattern)); this.removePattern(req.pattern); this.lockfile.removePattern(req.pattern); @@ -532,4 +553,26 @@ export default class PackageResolver { req.resolveToExistingVersion(info); } } + + resolveToResolution(req: DependencyRequestPattern): ?DependencyRequestPattern { + const {parentNames, pattern} = req; + + if (!parentNames) { + return req; + } + + const resolution = this.resolutions.find(pattern, parentNames); + + if (resolution) { + const resolutionManifest = this.getStrictResolvedPattern(resolution); + invariant(resolutionManifest._reference, 'resolutions should have a resolved reference'); + + resolutionManifest._reference.patterns.push(pattern); + this.lockfile.removePattern(pattern); + + return null; + } + + return req; + } } diff --git a/src/reporters/lang/en.js b/src/reporters/lang/en.js index 33f76362ce..49917b86ef 100644 --- a/src/reporters/lang/en.js +++ b/src/reporters/lang/en.js @@ -117,6 +117,10 @@ const messages = { 'Pattern $0 is trying to unpack in the same destination $1 as pattern $2. This could result in a non deterministic behavior, skipping.', incorrectLockfileEntry: 'Lockfile has incorrect entry for $0. Ignoring it.', + invalidResolutionName: 'Resolution field $0 does not end with a valid package name and will be ignored', + invalidResolutionVersion: 'Resolution field $0 has an invalid version entry and may be ignored', + incompatibleResolutionVersion: 'Resolution field $0 is incompatible with requested version $1', + yarnOutdated: "Your current version of Yarn is out of date. The latest version is $0 while you're on $1.", yarnOutdatedInstaller: 'To upgrade, download the latest installer at $0.', yarnOutdatedCommand: 'To upgrade, run the following command:', diff --git a/src/resolutions.js b/src/resolutions.js new file mode 100644 index 0000000000..0276fc4eae --- /dev/null +++ b/src/resolutions.js @@ -0,0 +1,96 @@ +/* @flow */ +import semver from 'semver'; +import minimatch from 'minimatch'; +import map from './util/map'; +import type Config from './config'; +import type {Reporter} from './reporters'; +import PackageRequest from './package-request'; +import {getExoticResolver} from './resolvers'; + +const DIRECTORY_SEPARATOR = '/'; +const GLOBAL_NESTED_DEP_PATTERN = '**/'; + +export type Resolution = { + name: string, + range: string, + pattern: string, + globPattern: string, +}; + +export type ResolutionMap = { + [packageName: string]: Array, +}; + +export type ResolutionEntry = { + [packageName: string]: string, +}; + +export default class Resolutions { + constructor(config: Config) { + this.resolutionsByPackage = map(); + this.config = config; + this.reporter = config.reporter; + } + + resolutionsByPackage: ResolutionMap; + config: Config; + reporter: Reporter; + + init(resolutions: ?ResolutionEntry = {}) { + for (const globPattern in resolutions) { + const info = this.parsePatternInfo(globPattern, resolutions[globPattern]); + + if (info) { + const resolution = this.resolutionsByPackage[info.name] || []; + this.resolutionsByPackage[info.name] = [...resolution, info]; + } + } + } + + parsePatternInfo(globPattern: string, range: string): ?Object { + const directories = globPattern.split(DIRECTORY_SEPARATOR); + const name = directories.pop(); + + if (!name) { + this.reporter.warn(this.reporter.lang('invalidResolutionName', globPattern)); + return null; + } + + if (!semver.validRange(range) && !getExoticResolver(range)) { + this.reporter.warn(this.reporter.lang('invalidResolutionVersion', range)); + return null; + } + + // For legacy support of resolutions, replace `name` with `**/name` + if (name === globPattern) { + globPattern = `${GLOBAL_NESTED_DEP_PATTERN}${name}`; + } + + return { + name, + range, + globPattern, + pattern: `${name}@${range}`, + }; + } + + find(reqPattern: string, parentNames: Array): ?string { + const {name, range: reqRange} = PackageRequest.normalizePattern(reqPattern); + const resolutions = this.resolutionsByPackage[name]; + + if (!resolutions) { + return ''; + } + + const modulePath = [...parentNames, name].join(DIRECTORY_SEPARATOR); + const {pattern, range} = resolutions.find(({globPattern}) => minimatch(modulePath, globPattern)) || {}; + + if (pattern) { + if (semver.validRange(range) && semver.valid(reqRange) && !semver.satisfies(range, reqRange)) { + this.reporter.warn(this.reporter.lang('incompatibleResolutionVersion', pattern, reqPattern)); + } + } + + return pattern; + } +} diff --git a/src/resolvers/registries/npm-resolver.js b/src/resolvers/registries/npm-resolver.js index ac25a4d904..b17fa7c9e7 100644 --- a/src/resolvers/registries/npm-resolver.js +++ b/src/resolvers/registries/npm-resolver.js @@ -178,7 +178,7 @@ export default class NpmResolver extends RegistryResolver { const {deprecated, dist} = info; if (typeof deprecated === 'string') { let human = `${info.name}@${info.version}`; - const parentNames = this.request.getParentNames(); + const parentNames = this.request.parentNames; if (parentNames.length) { human = parentNames.concat(human).join(' > '); } diff --git a/src/types.js b/src/types.js index 8cbd2e0adf..b259d9adfb 100644 --- a/src/types.js +++ b/src/types.js @@ -19,6 +19,7 @@ export type DependencyRequestPattern = { registry: RegistryNames, optional: boolean, hint?: ?string, + parentNames?: Array, parentRequest?: ?PackageRequest, }; export type DependencyRequestPatterns = Array; From d18c93ea8b1adc413440926038cd1af89c0669c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Nison?= Date: Mon, 14 Aug 2017 15:13:47 +0100 Subject: [PATCH 4/4] Uses "ResolutionMap" instead of the duplicate name "Resolutions" --- src/cli/commands/install.js | 14 +++++++------- src/package-resolver.js | 10 +++++----- src/{resolutions.js => resolution-map.js} | 6 +++--- 3 files changed, 15 insertions(+), 15 deletions(-) rename src/{resolutions.js => resolution-map.js} (95%) diff --git a/src/cli/commands/install.js b/src/cli/commands/install.js index 67f1b6f595..2498ae8d3c 100644 --- a/src/cli/commands/install.js +++ b/src/cli/commands/install.js @@ -26,7 +26,7 @@ import * as fs from '../../util/fs.js'; import map from '../../util/map.js'; import {version as YARN_VERSION, getInstallationMethod} from '../../util/yarn-version.js'; import WorkspaceLayout from '../../workspace-layout.js'; -import Resolutions from '../../resolutions.js'; +import ResolutionMap from '../../resolution-map.js'; const emoji = require('node-emoji'); const invariant = require('invariant'); @@ -171,8 +171,8 @@ export class Install { this.config = config; this.flags = normalizeFlags(config, flags); this.resolutions = map(); // Legacy resolutions field used for flat install mode - this._resolutions = new Resolutions(config); // Selective resolutions for nested dependencies - this.resolver = new PackageResolver(config, lockfile, this._resolutions); + this.resolutionMap = new ResolutionMap(config); // Selective resolutions for nested dependencies + this.resolver = new PackageResolver(config, lockfile, this.resolutionMap); this.integrityChecker = new InstallationIntegrityChecker(config); this.linker = new PackageLinker(config, this.resolver); this.scripts = new PackageInstallScripts(config, this.resolver, this.flags.force); @@ -190,7 +190,7 @@ export class Install { linker: PackageLinker; rootPatternsToOrigin: {[pattern: string]: string}; integrityChecker: InstallationIntegrityChecker; - _resolutions: Resolutions; + resolutionMap: ResolutionMap; /** * Create a list of dependency requests from the current directories manifests. @@ -237,9 +237,9 @@ export class Install { Object.assign(this.resolutions, projectManifestJson.resolutions); Object.assign(manifest, projectManifestJson); - this._resolutions.init(this.resolutions); - for (const packageName of Object.keys(this._resolutions.resolutionsByPackage)) { - for (const {pattern} of this._resolutions.resolutionsByPackage[packageName]) { + this.resolutionMap.init(this.resolutions); + for (const packageName of Object.keys(this.resolutionMap.resolutionsByPackage)) { + for (const {pattern} of this.resolutionMap.resolutionsByPackage[packageName]) { resolutionDeps = [...resolutionDeps, {registry, pattern, optional: false, hint: 'resolution'}]; } } diff --git a/src/package-resolver.js b/src/package-resolver.js index 62e37d9c47..89a29a79f9 100644 --- a/src/package-resolver.js +++ b/src/package-resolver.js @@ -12,7 +12,7 @@ import BlockingQueue from './util/blocking-queue.js'; import Lockfile from './lockfile/wrapper.js'; import map from './util/map.js'; import WorkspaceLayout from './workspace-layout.js'; -import Resolutions from './resolutions'; +import ResolutionMap from './resolution-map.js'; const invariant = require('invariant'); const semver = require('semver'); @@ -24,12 +24,12 @@ export type ResolverOptions = {| |}; export default class PackageResolver { - constructor(config: Config, lockfile: Lockfile, resolutions: ?Resolutions) { + constructor(config: Config, lockfile: Lockfile, resolutionMap: ResolutionMap = new ResolutionMap(config)) { this.patternsByPackage = map(); this.fetchingPatterns = map(); this.fetchingQueue = new BlockingQueue('resolver fetching'); this.patterns = map(); - this.resolutions = resolutions || new Resolutions(config); + this.resolutionMap = resolutionMap; this.usedRegistries = new Set(); this.flat = false; @@ -46,7 +46,7 @@ export default class PackageResolver { workspaceLayout: ?WorkspaceLayout; - resolutions: Resolutions; + resolutionMap: ResolutionMap; // list of registries that have been used in this resolution usedRegistries: Set; @@ -561,7 +561,7 @@ export default class PackageResolver { return req; } - const resolution = this.resolutions.find(pattern, parentNames); + const resolution = this.resolutionMap.find(pattern, parentNames); if (resolution) { const resolutionManifest = this.getStrictResolvedPattern(resolution); diff --git a/src/resolutions.js b/src/resolution-map.js similarity index 95% rename from src/resolutions.js rename to src/resolution-map.js index 0276fc4eae..15d1de7d91 100644 --- a/src/resolutions.js +++ b/src/resolution-map.js @@ -17,7 +17,7 @@ export type Resolution = { globPattern: string, }; -export type ResolutionMap = { +export type ResolutionInternalMap = { [packageName: string]: Array, }; @@ -25,14 +25,14 @@ export type ResolutionEntry = { [packageName: string]: string, }; -export default class Resolutions { +export default class ResolutionMap { constructor(config: Config) { this.resolutionsByPackage = map(); this.config = config; this.reporter = config.reporter; } - resolutionsByPackage: ResolutionMap; + resolutionsByPackage: ResolutionInternalMap; config: Config; reporter: Reporter;