Skip to content

Commit

Permalink
feat: support selective version resolutions (#291)
Browse files Browse the repository at this point in the history
  • Loading branch information
dead-horse authored and fengmk2 committed Jan 3, 2019
1 parent d4937f0 commit 9aa0a65
Show file tree
Hide file tree
Showing 10 changed files with 195 additions and 8 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ co(function* () {
- [x] `save`, `save-dev`, `save-optional`
- [x] support `ignore-scripts`
- [x] uninstall
- [x] resolutions

## Different with NPM

Expand Down Expand Up @@ -255,6 +256,10 @@ root/
both the same version: 1.1.2
```

## Resolutions

support [selective version resolutions](https://yarnpkg.com/en/docs/selective-version-resolutions) like yarn. which lets you define custom package versions inside your dependencies through the resolutions field in your `package.json` file.

## Benchmarks

### cnpmjs.org install
Expand Down
11 changes: 3 additions & 8 deletions lib/install.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ function* _install(parentDir, pkg, ancestors, options) {
pkg.version = '*';
}

pkg = options.resolution(pkg, ancestors);

debug('[%s/%s] install %s@%s in %s',
options.progresses.finishedInstallTasks,
options.progresses.installTasks,
Expand All @@ -58,7 +60,7 @@ function* _install(parentDir, pkg, ancestors, options) {
options.spinner.text = `[${options.progresses.finishedInstallTasks}/${options.progresses.installTasks}] Installing ${pkg.name}@${pkg.version}`;
}
let p = npa(pkg.name ? `${pkg.name}@${pkg.version}` : pkg.version);
const displayName = p.displayName = getDisplayName(pkg, ancestors);
const displayName = p.displayName = utils.getDisplayName(pkg, ancestors);

if (options.registryOnly && REMOTE_TYPES[p.type]) {
throw new Error(`Only allow install package from registry, but "${displayName}" is ${p.type}`);
Expand Down Expand Up @@ -325,13 +327,6 @@ function* bundleBin(name, parentDir, options) {
yield bin(parentDir, pkg, pkgDir, options);
}

function getDisplayName(pkg, ancestors) {
return ancestors
.map(ancestor => ancestor.displayName || ancestor)
.concat([ `${pkg.name}@${pkg.version}` ])
.join(chalk.gray(' › '));
}

function* matchAncestorDependencies(childPkg, ancestors, options) {
// only need check npm types
if (!fromNpm(childPkg.type)) return;
Expand Down
2 changes: 2 additions & 0 deletions lib/local_install.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const preinstall = require('./preinstall');
const prepublish = require('./prepublish');
const install = require('./install');
const dependencies = require('./dependencies');
const createResolution = require('./resolution');
const formatInstallOptions = require('./format_install_options');
const link = require('./link');
const bin = require('./bin');
Expand Down Expand Up @@ -109,6 +110,7 @@ function* _install(options) {
let pkgs = options.pkgs;
const rootPkgDependencies = dependencies(rootPkg, options);
options.rootPkgDependencies = rootPkgDependencies;
options.resolution = createResolution(rootPkg, options);
if (pkgs.length === 0) {
if (options.production) {
pkgs = rootPkgDependencies.prod;
Expand Down
66 changes: 66 additions & 0 deletions lib/resolution.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
'use strict';

const minimatch = require('minimatch');
const npa = require('npm-package-arg');
const utils = require('./utils');
const chalk = require('chalk');

module.exports = (pkg, options) => {
const resolutions = pkg && pkg.resolutions || {};
const resolutionMap = new Map();

// parse resolutions, generate resolutionMap:
// {
// debug: [
// "koa/accept", "1.0.0",
// "send": "2.0.0"
// ],
// less: [
// "**", "^1"
// ],
// }
for (const path in resolutions) {
const sections = path.split('/');
// debug => **/debug
if (sections.length === 1) sections.unshift('**');
// check
for (const section of sections) {
if (section !== '**' && !npa(section).name) {
throw new Error(`[resolutions] resolution package ${path} format error`);
}
}
const endpoint = sections.pop();
if (endpoint === '**') throw new Error(`[resolutions] resolution package ${path} format error`);

const version = resolutions[path];

if (!resolutionMap.has(endpoint)) resolutionMap.set(endpoint, []);
resolutionMap.get(endpoint).push([ sections.join('/'), version ]);
}

return (pkg, ancestors) => {
// only work for nested dependencies
if (!ancestors.length) return pkg;
// check pkg.name first to reduce calculate
const resolutions = resolutionMap.get(pkg.name);
if (!resolutions) return pkg;

const ancestorPath = ancestors.map(ancestor => ancestor.name).join('/');
for (const resolution of resolutions) {
const path = resolution[0];
const version = resolution[1];
if (minimatch(ancestorPath, path)) {
options.pendingMessages.push([
'warn',
'%s %s override by %s',
chalk.yellow('resolutions'),
chalk.gray(utils.getDisplayName(pkg, ancestors)),
chalk.magenta(`${path}/${pkg.name}@${version}`),
]);
return Object.assign({}, pkg, { version });
}
}

return pkg;
};
};
7 changes: 7 additions & 0 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -552,3 +552,10 @@ exports.getGlobalInstallMeta = prefix => {
};

exports.endsWithX = version => typeof version === 'string' && !!version.match(/^\d+\.(x|\d+\.x)$/);

exports.getDisplayName = (pkg, ancestors) => {
return ancestors
.map(ancestor => ancestor.displayName || ancestor)
.concat([ `${pkg.name}@${pkg.version}` ])
.join(' › ');
};
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"co-parallel": "^1.0.0",
"debug": "^2.6.8",
"destroy": "^1.0.4",
"minimatch": "^3.0.4",
"minimist": "^1.2.0",
"mkdirp": "^0.5.1",
"moment": "^2.18.1",
Expand Down
9 changes: 9 additions & 0 deletions test/fixtures/resolutions-error-endpoint/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "resolution-error-endpoint",
"dependencies": {
"koa": "^1"
},
"resolutions": {
"foo/bar-*": "1.0.0"
}
}
9 changes: 9 additions & 0 deletions test/fixtures/resolutions-error-format/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "resolution-error-endpoint",
"dependencies": {
"koa": "^1"
},
"resolutions": {
"foo/**": "1.0.0"
}
}
13 changes: 13 additions & 0 deletions test/fixtures/resolutions/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "resolutions",
"dependencies": {
"koa": "2",
"debug": "2.0.0",
"cookies": "0.7.1"
},
"resolutions": {
"debug": "1.0.0",
"**/http-errors": "1.0.0",
"koa/cookies": "0.7.0"
}
}
80 changes: 80 additions & 0 deletions test/resolutions.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
'use strict';

const assert = require('assert');
const rimraf = require('rimraf');
const path = require('path');
const readJSON = require('../lib/utils').readJSON;
const coffee = require('coffee');

const bin = path.join(__dirname, '../bin/install.js');

let root;
function cleanup() {
rimraf.sync(path.join(root, 'node_modules'));
}

function* checkPkg(name, version) {
const pkg = yield readJSON(path.join(root, 'node_modules', name, 'package.json'));
assert.equal(pkg.version, version);
}

describe('test/seperate-dependencies.test.js', () => {
describe('resolutions-error-format', () => {
before(() => {
root = path.join(__dirname, 'fixtures', 'resolutions-error-format');
cleanup();
});
afterEach(cleanup);

it('should install error', () => {
return coffee.fork(bin, {
cwd: root,
})
// .debug()
.expect('code', 1)
.expect('stderr', /resolution package foo\/\*\* format error/)
.end();
});
});

describe('resolutions-error-endpoint', () => {
before(() => {
root = path.join(__dirname, 'fixtures', 'resolutions-error-endpoint');
cleanup();
});
afterEach(cleanup);

it('should install error', () => {
return coffee.fork(bin, {
cwd: root,
})
// .debug()
.expect('code', 1)
.expect('stderr', /resolution package foo\/bar-\* format error/)
.end();
});
});

describe('resolutions', () => {
before(() => {
root = path.join(__dirname, 'fixtures', 'resolutions');
cleanup();
});
afterEach(cleanup);

it('should work', function* () {
yield coffee.fork(bin, {
cwd: root,
})
.debug()
.expect('code', 0)
.end();

yield checkPkg('debug', '2.0.0');
yield checkPkg('koa/node_modules/debug', '1.0.0');
yield checkPkg('koa/node_modules/cookies', '0.7.0');
yield checkPkg('cookies', '0.7.1');

});
});
});

0 comments on commit 9aa0a65

Please sign in to comment.