Workspace phase 3 & 4 (#3516)

* Workspaces phase 3 & 4

* fixed check command

* addressed comments from @arcanis

* returned if worksapce condititon
This commit is contained in:
Konstantin Raev
2017-06-02 19:57:23 +01:00
committed by GitHub
parent bebe4cc36f
commit 4463175c5c
96 changed files with 931 additions and 145 deletions

View File

@@ -42,7 +42,7 @@ const runAdd = buildRun.bind(
},
);
test.concurrent('adds any new package to the current workspace, but install from the worktree', async () => {
test.concurrent('adds any new package to the current workspace, but install from the workspace', async () => {
await runInstall({}, 'simple-worktree', async (config): Promise<void> => {
const inOut = new stream.PassThrough();
const reporter = new reporters.JSONReporter({stdout: inOut});

View File

@@ -117,5 +117,3 @@ test('Only top level (after hoisting) bin links should be linked', (): Promise<v
expect(await linkAt(config, 'node_modules', '.bin', 'eslint')).toEqual('../eslint/bin/eslint.js');
});
});

View File

@@ -1,34 +1,74 @@
/* @flow */
import {run as check} from '../../../src/cli/commands/check.js';
import {Install} from '../../../src/cli/commands/install.js';
import * as reporters from '../../../src/reporters/index.js';
import * as fs from '../../../src/util/fs.js';
import {runInstall} from '../_helpers.js';
import {runInstall, run as buildRun} from '../_helpers.js';
jasmine.DEFAULT_TIMEOUT_INTERVAL = 150000;
const path = require('path');
test.concurrent("workspaces don't work without a configuration in .yarnrc", async (): Promise<void> => {
let thrown = false;
let error = '';
const reporter = new reporters.ConsoleReporter({});
try {
await runInstall({}, 'workspaces-install-enabled');
} catch (e) {
thrown = true;
error = e.message;
}
expect(thrown).toBe(true);
expect(error).toContain(reporter.lang('workspaceExperimentalDisabled'));
});
test.concurrent("workspaces don't work on non private projects", async (): Promise<void> => {
let thrown = false;
let error = '';
const reporter = new reporters.ConsoleReporter({});
try {
await runInstall({}, 'workspaces-install-private');
} catch (e) {
thrown = true;
error = e.message;
}
expect(thrown).toBe(true);
expect(error).toContain(reporter.lang('workspacesRequirePrivateProjects'));
});
test.concurrent('installs workspaces into root folder', (): Promise<void> => {
test.concurrent("workspaces don't work with duplicate names", async (): Promise<void> => {
let error = '';
const reporter = new reporters.ConsoleReporter({});
try {
await runInstall({}, 'workspaces-install-duplicate');
} catch (e) {
error = e.message;
}
expect(error).toContain(reporter.lang('workspaceNameDuplicate', 'workspace-1'));
});
test.concurrent("workspaces warn and get ignored if they don't have a name and a version", (): Promise<void> => {
return buildRun(
reporters.BufferReporter,
path.join(__dirname, '..', '..', 'fixtures', 'install'),
async (args, flags, config, reporter, lockfile): Promise<void> => {
const install = new Install(flags, config, reporter, lockfile);
await install.init();
const warnings = reporter.getBuffer();
expect(
warnings.some(warning => {
return warning.data.toString().toLowerCase().includes('missing version in workspace');
}),
).toEqual(true);
expect(
warnings.some(warning => {
return warning.data.toString().toLowerCase().includes('missing name in workspace');
}),
).toEqual(true);
},
[],
{},
'workspaces-install-mandatory-fields',
);
});
test.concurrent('installs workspaces dependencies into root folder', (): Promise<void> => {
return runInstall({}, 'workspaces-install-basic', async (config): Promise<void> => {
const lockfile = await fs.readFile(path.join(config.cwd, 'yarn.lock'));
expect(lockfile.indexOf('isarray')).toBeGreaterThanOrEqual(0);
@@ -48,6 +88,109 @@ test.concurrent('installs workspaces into root folder', (): Promise<void> => {
});
});
test.concurrent('install should install unhoistable dependencies in workspace node_modules', (): Promise<void> => {
return runInstall({}, 'workspaces-install-conflict', async (config): Promise<void> => {
// node_modules/left-pad@1.1.3
let packageFile = await fs.readFile(path.join(config.cwd, 'node_modules', 'left-pad', 'package.json'));
expect(JSON.parse(packageFile).version).toEqual('1.1.3');
// node_modules/workspace-child/left-pad@1.1.1
packageFile = await fs.readFile(
path.join(config.cwd, 'workspace-child', 'node_modules', 'left-pad', 'package.json'),
);
expect(JSON.parse(packageFile).version).toEqual('1.1.1');
});
});
test.concurrent('install should link workspaces that refer each other', (): Promise<void> => {
return runInstall({}, 'workspaces-install-link', async (config): Promise<void> => {
// packages/workspace-1/node_modules/left-pad - missing because it is hoisted to the root
expect(await fs.exists(path.join(config.cwd, 'packages', 'workspace-1', 'node_modules'))).toBe(false);
// node_modules/left-pad - link
const packageFile = await fs.readFile(path.join(config.cwd, 'node_modules', 'left-pad', 'package.json'));
expect(JSON.parse(packageFile).version).toEqual('1.1.2');
const readme = await fs.readFile(path.join(config.cwd, 'node_modules', 'left-pad', 'README.md'));
expect(readme.split('\n')[0]).toEqual('WORKSPACES ROCK!');
});
});
test.concurrent(
'install should not link workspaces that refer not compatible version of another workspace',
(): Promise<void> => {
return runInstall({}, 'workspaces-install-link', async (config): Promise<void> => {
// packages/workspace-2/node_modules/left-pad - from npm
const packageFile = await fs.readFile(
path.join(config.cwd, 'packages', 'workspace-2', 'node_modules', 'left-pad', 'package.json'),
);
const readme = await fs.readFile(
path.join(config.cwd, 'packages', 'workspace-2', 'node_modules', 'left-pad', 'README.md'),
);
expect(JSON.parse(packageFile).version).not.toBe('1.1.2');
expect(readme.split('\n')[0]).not.toEqual('WORKSPACES ROCK!');
});
},
);
test.concurrent('install should prioritize non workspace dependency at root over the workspace symlink', (): Promise<
void,
> => {
return runInstall({}, 'workspaces-install-link-root', async (config): Promise<void> => {
// node_modules/left-pad - from npm
let packageFile = await fs.readFile(path.join(config.cwd, 'node_modules', 'left-pad', 'package.json'));
expect(JSON.parse(packageFile).version).toEqual('1.1.3');
let readme = await fs.readFile(path.join(config.cwd, 'node_modules', 'left-pad', 'README.md'));
expect(readme.split('\n')[0]).not.toEqual('WORKSPACES ROCK!');
// node_modules/workspace-1/left-pad - link
packageFile = await fs.readFile(
path.join(config.cwd, 'packages', 'workspace-1', 'node_modules', 'left-pad', 'package.json'),
);
expect(JSON.parse(packageFile).version).toEqual('1.1.2');
readme = await fs.readFile(
path.join(config.cwd, 'packages', 'workspace-1', 'node_modules', 'left-pad', 'README.md'),
);
expect(readme.split('\n')[0]).toEqual('WORKSPACES ROCK!');
// packages/workspace-2/node_modules/left-pad - missing because it is hoisted to the root
expect(await fs.exists(path.join(config.cwd, 'packages', 'workspace-2', 'node_modules'))).toBe(false);
});
});
test.concurrent('install should install subedependencies of workspaces', (): Promise<void> => {
// the tricky part is that isarray is a subdependency of left-pad that is not referenced in the root
// but another workspace
return runInstall({}, 'workspaces-install-subdeps', async (config): Promise<void> => {
expect(await fs.exists(path.join(config.cwd, 'node_modules', 'isarray'))).toBe(true);
});
});
test.concurrent(
'install should install subedependencies of workspaces that are not referenced in other workspaces',
(): Promise<void> => {
// the tricky part is that left-pad is not a dependency of root
return runInstall({}, 'workspaces-install-subdeps-2', async (config): Promise<void> => {
expect(await fs.exists(path.join(config.cwd, 'node_modules', 'isarray'))).toBe(true);
});
},
);
test.concurrent('install should install dev dependencies of workspaces', (): Promise<void> => {
// the tricky part is that left-pad is not a dependency of root
return runInstall({}, 'workspaces-install-subdeps-dev', async (config): Promise<void> => {
expect(await fs.exists(path.join(config.cwd, 'node_modules', 'left-pad'))).toBe(true);
expect(await fs.exists(path.join(config.cwd, 'packages', 'workspace-1', 'node_modules', 'left-pad'))).toBe(true);
});
});
test.concurrent('install should not install dev dependencies of workspaces in production mode', (): Promise<void> => {
// the tricky part is that left-pad is not a dependency of root
return runInstall({production: true}, 'workspaces-install-subdeps-dev', async (config): Promise<void> => {
expect(await fs.exists(path.join(config.cwd, 'node_modules', 'left-pad'))).toBe(true);
expect(await fs.exists(path.join(config.cwd, 'packages', 'workspace-1', 'node_modules', 'left-pad'))).toBe(false);
});
});
test.concurrent('check command should work', (): Promise<void> => {
return runInstall({checkFiles: true}, 'workspaces-install-basic', async (config, reporter): Promise<void> => {
// check command + integrity check
@@ -62,14 +205,4 @@ test.concurrent('check command should work', (): Promise<void> => {
});
});
test.concurrent('install should fail if a workspace has a conflicting version of a dependency', async (): Promise<
void,
> => {
let thrown = false;
try {
await runInstall({}, 'workspaces-install-conflict');
} catch (e) {
thrown = true;
}
expect(thrown).toBe(true);
});
// TODO need more thorough tests for all kinds of checks: integrity, verify-tree

View File

@@ -1,4 +1,5 @@
{
"name": "package-a",
"version": "1.0.0",
"license": "MIT"
}

View File

@@ -1,4 +1,5 @@
{
"name": "package-b",
"version": "1.0.0",
"license": "MIT"
}

View File

@@ -1,5 +1,6 @@
{
"name": "workspace-2",
"version": "1.0.0",
"dependencies": {
"right-pad": "1.0.1"
}

View File

@@ -1,5 +1,6 @@
{
"name": "workspace-3",
"version": "1.0.0",
"dependencies": {
"repeat-string": "1.6.1"
}

View File

@@ -1,5 +1,6 @@
{
"name": "workspace-1",
"version": "1.0.0",
"dependencies": {
"isarray": "2.0.1"
}

View File

@@ -1,5 +1,6 @@
{
"name": "workspace-1",
"version": "1.0.0",
"dependencies": {
"left-pad": "1.1.1"
}

View File

@@ -0,0 +1 @@
workspaces-experimental true

View File

@@ -0,0 +1,7 @@
{
"name": "my-project",
"private": true,
"workspaces": [
"packages/*"
]
}

View File

@@ -0,0 +1,7 @@
{
"name": "workspace-1",
"version": "1.0.0",
"dependencies": {
"right-pad": "1.0.1"
}
}

View File

@@ -0,0 +1,7 @@
{
"name": "workspace-1",
"version": "1.0.0",
"dependencies": {
"right-pad": "1.0.1"
}
}

View File

@@ -0,0 +1 @@
workspaces-experimental true

View File

@@ -0,0 +1,10 @@
{
"name": "my-project",
"private": true,
"dependencies": {
"left-pad": "^1.1.3"
},
"workspaces": [
"packages/*"
]
}

View File

@@ -0,0 +1 @@
WORKSPACES ROCK!

View File

@@ -0,0 +1,37 @@
'use strict';
module.exports = leftPad;
var cache = ['', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '];
function leftPad(str, len, ch) {
// convert `str` to `string`
str = str + '';
// `len` is the `pad`'s length now
len = len - str.length;
// doesn't need to pad
if (len <= 0) return str;
// `ch` defaults to `' '`
if (!ch && ch !== 0) ch = ' ';
// convert `ch` to `string`
ch = ch + '';
// cache common use cases
if (ch === ' ' && len < 10) return cache[len] + str;
// `pad` starts with an empty string
var pad = '';
// loop
while (true) {
// add `ch` to `pad` if `len` is odd
if (len & 1) pad += ch;
// devide `len` by 2, ditch the fraction
len >>= 1;
// "double" the `ch` so this operation count grows logarithmically on `len`
// each time `ch` is "doubled", the `len` would need to be "doubled" too
// similar to finding a value in binary search tree, hence O(log(n))
if (len) ch += ch;
else
// `len` is 0, exit the loop
break;
}
// pad `str`!
return pad + str;
}

View File

@@ -0,0 +1,30 @@
{
"name": "left-pad",
"version": "1.1.2",
"description": "String left pad",
"main": "index.js",
"scripts": {
"test": "node test",
"bench": "node perf/perf.js"
},
"keywords": [
"leftpad",
"left",
"pad",
"padding",
"string",
"repeat"
],
"repository": {
"url": "git@github.com:stevemao/left-pad.git",
"type": "git"
},
"author": "azer",
"maintainers": [
{
"name": "Cameron Westland",
"email": "camwest@gmail.com"
}
],
"license": "WTFPL"
}

View File

@@ -0,0 +1,13 @@
var leftPad = require('./');
var test = require('tape');
test('left pad', function(assert) {
assert.plan(7);
assert.strictEqual(leftPad('foo', 5), ' foo');
assert.strictEqual(leftPad('foobar', 6), 'foobar');
assert.strictEqual(leftPad(1, 2, 0), '01');
assert.strictEqual(leftPad(1, 2, '-'), '-1');
assert.strictEqual(leftPad('foo', 2, ' '), 'foo');
assert.strictEqual(leftPad('foo', -1, ' '), 'foo');
assert.strictEqual(leftPad('foo', 7, 1), '1111foo');
});

View File

@@ -0,0 +1,7 @@
{
"name": "workspace-1",
"version": "1.0.0",
"dependencies": {
"left-pad": "1.1.2"
}
}

View File

@@ -0,0 +1,7 @@
{
"name": "workspace-2",
"version": "1.0.0",
"dependencies": {
"left-pad": "^1.1.3"
}
}

View File

@@ -0,0 +1 @@
workspaces-experimental true

View File

@@ -0,0 +1,10 @@
{
"name": "my-project",
"private": true,
"dependencies": {
"left-pad": "^1.0.0"
},
"workspaces": [
"packages/*"
]
}

View File

@@ -0,0 +1 @@
WORKSPACES ROCK!

View File

@@ -0,0 +1,37 @@
'use strict';
module.exports = leftPad;
var cache = ['', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '];
function leftPad(str, len, ch) {
// convert `str` to `string`
str = str + '';
// `len` is the `pad`'s length now
len = len - str.length;
// doesn't need to pad
if (len <= 0) return str;
// `ch` defaults to `' '`
if (!ch && ch !== 0) ch = ' ';
// convert `ch` to `string`
ch = ch + '';
// cache common use cases
if (ch === ' ' && len < 10) return cache[len] + str;
// `pad` starts with an empty string
var pad = '';
// loop
while (true) {
// add `ch` to `pad` if `len` is odd
if (len & 1) pad += ch;
// devide `len` by 2, ditch the fraction
len >>= 1;
// "double" the `ch` so this operation count grows logarithmically on `len`
// each time `ch` is "doubled", the `len` would need to be "doubled" too
// similar to finding a value in binary search tree, hence O(log(n))
if (len) ch += ch;
else
// `len` is 0, exit the loop
break;
}
// pad `str`!
return pad + str;
}

View File

@@ -0,0 +1,30 @@
{
"name": "left-pad",
"version": "1.1.2",
"description": "String left pad",
"main": "index.js",
"scripts": {
"test": "node test",
"bench": "node perf/perf.js"
},
"keywords": [
"leftpad",
"left",
"pad",
"padding",
"string",
"repeat"
],
"repository": {
"url": "git@github.com:stevemao/left-pad.git",
"type": "git"
},
"author": "azer",
"maintainers": [
{
"name": "Cameron Westland",
"email": "camwest@gmail.com"
}
],
"license": "WTFPL"
}

View File

@@ -0,0 +1,13 @@
var leftPad = require('./');
var test = require('tape');
test('left pad', function(assert) {
assert.plan(7);
assert.strictEqual(leftPad('foo', 5), ' foo');
assert.strictEqual(leftPad('foobar', 6), 'foobar');
assert.strictEqual(leftPad(1, 2, 0), '01');
assert.strictEqual(leftPad(1, 2, '-'), '-1');
assert.strictEqual(leftPad('foo', 2, ' '), 'foo');
assert.strictEqual(leftPad('foo', -1, ' '), 'foo');
assert.strictEqual(leftPad('foo', 7, 1), '1111foo');
});

View File

@@ -0,0 +1,7 @@
{
"name": "workspace-1",
"version": "1.0.0",
"dependencies": {
"left-pad": "1.1.2"
}
}

View File

@@ -0,0 +1,7 @@
{
"name": "workspace-2",
"version": "1.0.0",
"dependencies": {
"left-pad": "^1.1.3"
}
}

View File

@@ -0,0 +1 @@
workspaces-experimental true

View File

@@ -0,0 +1,7 @@
{
"name": "my-project",
"private": true,
"workspaces": [
"packages/*"
]
}

View File

@@ -0,0 +1,6 @@
{
"name": "workspace-1",
"dependencies": {
"right-pad": "1.0.1"
}
}

View File

@@ -0,0 +1,6 @@
{
"version": "1.0.0",
"dependencies": {
"right-pad": "1.0.1"
}
}

View File

@@ -1,5 +1,6 @@
{
"name": "workspace-1",
"version": "1.0.0",
"dependencies": {
"isarray": "2.0.1"
}

View File

@@ -0,0 +1 @@
workspaces-experimental true

View File

@@ -0,0 +1,10 @@
{
"name": "my-project",
"private": true,
"dependencies": {
"left-pad": "^1.1.3"
},
"workspaces": [
"packages/*"
]
}

View File

@@ -0,0 +1 @@
WORKSPACES ROCK!

View File

@@ -0,0 +1,37 @@
'use strict';
module.exports = leftPad;
var cache = ['', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '];
function leftPad(str, len, ch) {
// convert `str` to `string`
str = str + '';
// `len` is the `pad`'s length now
len = len - str.length;
// doesn't need to pad
if (len <= 0) return str;
// `ch` defaults to `' '`
if (!ch && ch !== 0) ch = ' ';
// convert `ch` to `string`
ch = ch + '';
// cache common use cases
if (ch === ' ' && len < 10) return cache[len] + str;
// `pad` starts with an empty string
var pad = '';
// loop
while (true) {
// add `ch` to `pad` if `len` is odd
if (len & 1) pad += ch;
// devide `len` by 2, ditch the fraction
len >>= 1;
// "double" the `ch` so this operation count grows logarithmically on `len`
// each time `ch` is "doubled", the `len` would need to be "doubled" too
// similar to finding a value in binary search tree, hence O(log(n))
if (len) ch += ch;
else
// `len` is 0, exit the loop
break;
}
// pad `str`!
return pad + str;
}

View File

@@ -0,0 +1,33 @@
{
"name": "left-pad",
"version": "1.1.2",
"description": "String left pad",
"main": "index.js",
"scripts": {
"test": "node test",
"bench": "node perf/perf.js"
},
"keywords": [
"leftpad",
"left",
"pad",
"padding",
"string",
"repeat"
],
"dependencies": {
"isarray": "^1.0.0"
},
"repository": {
"url": "git@github.com:stevemao/left-pad.git",
"type": "git"
},
"author": "azer",
"maintainers": [
{
"name": "Cameron Westland",
"email": "camwest@gmail.com"
}
],
"license": "WTFPL"
}

View File

@@ -0,0 +1,13 @@
var leftPad = require('./');
var test = require('tape');
test('left pad', function(assert) {
assert.plan(7);
assert.strictEqual(leftPad('foo', 5), ' foo');
assert.strictEqual(leftPad('foobar', 6), 'foobar');
assert.strictEqual(leftPad(1, 2, 0), '01');
assert.strictEqual(leftPad(1, 2, '-'), '-1');
assert.strictEqual(leftPad('foo', 2, ' '), 'foo');
assert.strictEqual(leftPad('foo', -1, ' '), 'foo');
assert.strictEqual(leftPad('foo', 7, 1), '1111foo');
});

View File

@@ -0,0 +1 @@
workspaces-experimental true

View File

@@ -0,0 +1,10 @@
{
"name": "my-project",
"private": true,
"dependencies": {
"left-pad": "^1.1.3"
},
"workspaces": [
"packages/*"
]
}

View File

@@ -0,0 +1,7 @@
{
"name": "workspace-1",
"version": "1.0.0",
"devDependencies": {
"left-pad": "1.1.2"
}
}

View File

@@ -0,0 +1 @@
workspaces-experimental true

View File

@@ -0,0 +1,10 @@
{
"name": "my-project",
"private": true,
"dependencies": {
"left-pad": "^1.1.3"
},
"workspaces": [
"packages/*"
]
}

View File

@@ -0,0 +1 @@
WORKSPACES ROCK!

View File

@@ -0,0 +1,37 @@
'use strict';
module.exports = leftPad;
var cache = ['', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '];
function leftPad(str, len, ch) {
// convert `str` to `string`
str = str + '';
// `len` is the `pad`'s length now
len = len - str.length;
// doesn't need to pad
if (len <= 0) return str;
// `ch` defaults to `' '`
if (!ch && ch !== 0) ch = ' ';
// convert `ch` to `string`
ch = ch + '';
// cache common use cases
if (ch === ' ' && len < 10) return cache[len] + str;
// `pad` starts with an empty string
var pad = '';
// loop
while (true) {
// add `ch` to `pad` if `len` is odd
if (len & 1) pad += ch;
// devide `len` by 2, ditch the fraction
len >>= 1;
// "double" the `ch` so this operation count grows logarithmically on `len`
// each time `ch` is "doubled", the `len` would need to be "doubled" too
// similar to finding a value in binary search tree, hence O(log(n))
if (len) ch += ch;
else
// `len` is 0, exit the loop
break;
}
// pad `str`!
return pad + str;
}

View File

@@ -0,0 +1,33 @@
{
"name": "left-pad",
"version": "1.1.2",
"description": "String left pad",
"main": "index.js",
"scripts": {
"test": "node test",
"bench": "node perf/perf.js"
},
"keywords": [
"leftpad",
"left",
"pad",
"padding",
"string",
"repeat"
],
"dependencies": {
"isarray": "^1.0.0"
},
"repository": {
"url": "git@github.com:stevemao/left-pad.git",
"type": "git"
},
"author": "azer",
"maintainers": [
{
"name": "Cameron Westland",
"email": "camwest@gmail.com"
}
],
"license": "WTFPL"
}

View File

@@ -0,0 +1,13 @@
var leftPad = require('./');
var test = require('tape');
test('left pad', function(assert) {
assert.plan(7);
assert.strictEqual(leftPad('foo', 5), ' foo');
assert.strictEqual(leftPad('foobar', 6), 'foobar');
assert.strictEqual(leftPad(1, 2, 0), '01');
assert.strictEqual(leftPad(1, 2, '-'), '-1');
assert.strictEqual(leftPad('foo', 2, ' '), 'foo');
assert.strictEqual(leftPad('foo', -1, ' '), 'foo');
assert.strictEqual(leftPad('foo', 7, 1), '1111foo');
});

View File

@@ -0,0 +1,7 @@
{
"name": "workspace-1",
"version": "1.0.0",
"dependencies": {
"left-pad": "1.1.2"
}
}

View File

@@ -0,0 +1,7 @@
{
"name": "workspace-2",
"version": "1.0.0",
"dependencies": {
"left-pad": "^1.1.3"
}
}

View File

@@ -18,7 +18,10 @@ const prettier = isWindows ? 'prettier.cmd' : 'prettier';
const prettierCmd = path.resolve(__dirname, '../node_modules/.bin/' + prettier);
const config = {
ignore: ['**/node_modules/**'],
ignore: [
'**/node_modules/**',
'__tests__/fixtures/**'
],
options: {
'bracket-spacing': 'false',
'print-width': 120,

View File

@@ -165,9 +165,9 @@ async function integrityHashCheck(
const install = new Install(flags, config, reporter, lockfile);
// get patterns that are installed when running `yarn install`
const {patterns} = await install.fetchRequestFromCwd();
const {patterns, workspaceLayout} = await install.fetchRequestFromCwd();
const match = await integrityChecker.check(patterns, lockfile.cache, flags);
const match = await integrityChecker.check(patterns, lockfile.cache, flags, workspaceLayout);
for (const pattern of match.missingPatterns) {
reportError('lockfileNotContainPattern', pattern);
}
@@ -220,12 +220,12 @@ export async function run(config: Config, reporter: Reporter, flags: Object, arg
}
// get patterns that are installed when running `yarn install`
const {patterns: rawPatterns} = await install.hydrate(true);
const {patterns: rawPatterns, workspaceLayout} = await install.hydrate();
const patterns = await install.flatten(rawPatterns);
// check if patterns exist in lockfile
for (const pattern of patterns) {
if (!lockfile.getLocked(pattern)) {
if (!lockfile.getLocked(pattern) && (!workspaceLayout || !workspaceLayout.getManifestByPattern(pattern))) {
reportError('lockfileNotContainPattern', pattern);
}
}
@@ -266,7 +266,8 @@ export async function run(config: Config, reporter: Reporter, flags: Object, arg
// skip unnecessary checks for linked dependencies
const remoteType = pkg._reference.remote.type;
const isLinkedDepencency = remoteType === 'link' || (remoteType === 'file' && config.linkFileDependencies);
const isLinkedDepencency =
remoteType === 'link' || remoteType === 'workspace' || (remoteType === 'file' && config.linkFileDependencies);
if (isLinkedDepencency) {
continue;
}

View File

@@ -254,9 +254,8 @@ class ImportPackageResolver extends PackageResolver {
}
}
async init(deps: DependencyRequestPatterns, isFlat: boolean, rootName?: string): Promise<void> {
async init(deps: DependencyRequestPatterns, isFlat: boolean): Promise<void> {
this.flat = isFlat;
this.rootName = rootName || this.rootName;
const activity = (this.activity = this.reporter.activity());
await this.findAll(deps);
this.resetOptional();
@@ -278,7 +277,10 @@ export class Import extends Install {
}
await verifyTreeCheck(this.config, this.reporter, {}, []);
const {requests, patterns, manifest} = await this.fetchRequestFromCwd();
await this.resolver.init(requests, this.flags.flat, manifest.name);
if (manifest.name && this.resolver instanceof ImportPackageResolver) {
this.resolver.rootName = manifest.name;
}
await this.resolver.init(requests, this.flags.flat);
const manifests: Array<Manifest> = await fetcher.fetch(this.resolver.getManifests(), this.config);
this.resolver.updateManifests(manifests);
await compatibility.check(this.resolver.getManifests(), this.config, this.flags.ignoreEngines);

View File

@@ -23,12 +23,14 @@ import * as constants from '../../constants.js';
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';
const emoji = require('node-emoji');
const invariant = require('invariant');
const isCI = require('is-ci');
const path = require('path');
const semver = require('semver');
const uuid = require('uuid');
const ONE_DAY = 1000 * 60 * 60 * 24;
@@ -38,6 +40,7 @@ export type InstallCwdRequest = {
ignorePatterns: Array<string>,
usedPatterns: Array<string>,
manifest: Object,
workspaceLayout?: WorkspaceLayout,
};
type Flags = {
@@ -194,11 +197,12 @@ export class Install {
ignoreUnusedPatterns?: boolean = false,
): Promise<InstallCwdRequest> {
const patterns = [];
const deps = [];
const deps: DependencyRequestPatterns = [];
const manifest = {};
const ignorePatterns = [];
const usedPatterns = [];
let workspaceLayout;
// exclude package names that are in install args
const excludeNames = [];
@@ -224,42 +228,10 @@ export class Install {
const projectManifestJson = await this.config.readJson(loc);
await normalizeManifest(projectManifestJson, this.config.cwd, this.config, true);
const rootCwd = this.config.cwd;
// if project has workspaces we aggreagate all dependences from workspaces into root
if (projectManifestJson.workspaces) {
if (!projectManifestJson.private) {
throw new MessageError(this.reporter.lang('workspacesRequirePrivateProjects'));
}
const workspaces = await this.config.resolveWorkspaces(path.dirname(loc), projectManifestJson.workspaces);
const workspaceEntries = Object.keys(workspaces).map(name => workspaces[name]);
for (const {loc: workspaceLoc, manifest: workspaceManifest} of workspaceEntries) {
for (const type of ['dependencies', 'devDependencies', 'optionalDependencies']) {
if (workspaceManifest[type]) {
for (const key of Object.keys(workspaceManifest[type])) {
if (
projectManifestJson[type] &&
projectManifestJson[type][key] &&
projectManifestJson[type][key] !== workspaceManifest[type][key]
) {
// TODO conflicts should still be installed inside workspaces' folders
throw new MessageError(
this.reporter.lang('workspacesIncompatibleDependencies', key, workspaceLoc, rootCwd),
);
}
if (!projectManifestJson[type]) {
projectManifestJson[type] = {};
}
projectManifestJson[type][key] = workspaceManifest[type][key];
}
}
}
}
}
Object.assign(this.resolutions, projectManifestJson.resolutions);
Object.assign(manifest, projectManifestJson);
const pushDeps = (depType, {hint, optional}, isUsed) => {
const pushDeps = (depType, manifest: Object, {hint, optional}, isUsed) => {
if (ignoreUnusedPatterns && !isUsed) {
return;
}
@@ -269,7 +241,7 @@ export class Install {
if (this.flags.flat && !isUsed) {
return;
}
const depMap = projectManifestJson[depType];
const depMap = manifest[depType];
for (const name in depMap) {
if (excludeNames.indexOf(name) >= 0) {
continue;
@@ -295,9 +267,38 @@ export class Install {
}
};
pushDeps('dependencies', {hint: null, optional: false}, true);
pushDeps('devDependencies', {hint: 'dev', optional: false}, !this.config.production);
pushDeps('optionalDependencies', {hint: 'optional', optional: true}, !this.flags.ignoreOptional);
pushDeps('dependencies', projectManifestJson, {hint: null, optional: false}, true);
pushDeps('devDependencies', projectManifestJson, {hint: 'dev', optional: false}, !this.config.production);
pushDeps(
'optionalDependencies',
projectManifestJson,
{hint: 'optional', optional: true},
!this.flags.ignoreOptional,
);
if (this.config.workspacesEnabled) {
const workspaces = await this.config.resolveWorkspaces(path.dirname(loc), projectManifestJson);
workspaceLayout = new WorkspaceLayout(workspaces, this.config);
// add virtual manifest that depends on all workspaces, this way package hoisters and resolvers will work fine
const virtualDependencyManifest: Manifest = {
_uid: '',
name: `workspace-aggregator-${uuid.v4()}`,
version: '1.0.0',
_registry: 'npm',
_loc: '.',
dependencies: {},
};
workspaceLayout.virtualManifestName = virtualDependencyManifest.name;
virtualDependencyManifest.dependencies = {};
for (const workspaceName of Object.keys(workspaces)) {
virtualDependencyManifest.dependencies[workspaceName] = workspaces[workspaceName].manifest.version;
}
const virtualDep = {};
virtualDep[virtualDependencyManifest.name] = virtualDependencyManifest.version;
workspaces[virtualDependencyManifest.name] = {loc: '', manifest: virtualDependencyManifest};
pushDeps('workspaces', {workspaces: virtualDep}, {hint: 'workspaces', optional: false}, true);
}
break;
}
@@ -313,6 +314,7 @@ export class Install {
manifest,
usedPatterns,
ignorePatterns,
workspaceLayout,
};
}
@@ -328,7 +330,7 @@ export class Install {
return patterns;
}
async bailout(patterns: Array<string>): Promise<boolean> {
async bailout(patterns: Array<string>, workspaceLayout: ?WorkspaceLayout): Promise<boolean> {
if (this.flags.skipIntegrityCheck || this.flags.force) {
return false;
}
@@ -336,7 +338,7 @@ export class Install {
if (!lockfileCache) {
return false;
}
const match = await this.integrityChecker.check(patterns, lockfileCache, this.flags);
const match = await this.integrityChecker.check(patterns, lockfileCache, this.flags, workspaceLayout);
if (this.flags.frozenLockfile && match.missingPatterns.length > 0) {
throw new MessageError(this.reporter.lang('frozenLockfileError'));
}
@@ -404,7 +406,12 @@ export class Install {
let flattenedTopLevelPatterns: Array<string> = [];
const steps: Array<(curr: number, total: number) => Promise<{bailout: boolean} | void>> = [];
const {requests: depRequests, patterns: rawPatterns, ignorePatterns} = await this.fetchRequestFromCwd();
const {
requests: depRequests,
patterns: rawPatterns,
ignorePatterns,
workspaceLayout,
} = await this.fetchRequestFromCwd();
let topLevelPatterns: Array<string> = [];
const artifacts = await this.integrityChecker.getArtifacts();
@@ -415,10 +422,10 @@ export class Install {
steps.push(async (curr: number, total: number) => {
this.reporter.step(curr, total, this.reporter.lang('resolvingPackages'), emoji.get('mag'));
await this.resolver.init(this.prepareRequests(depRequests), this.flags.flat);
await this.resolver.init(this.prepareRequests(depRequests), this.flags.flat, workspaceLayout);
topLevelPatterns = this.preparePatterns(rawPatterns);
flattenedTopLevelPatterns = await this.flatten(topLevelPatterns);
return {bailout: await this.bailout(topLevelPatterns)};
return {bailout: await this.bailout(topLevelPatterns, workspaceLayout)};
});
steps.push(async (curr: number, total: number) => {
@@ -433,7 +440,7 @@ export class Install {
// remove integrity hash to make this operation atomic
await this.integrityChecker.removeIntegrityFile();
this.reporter.step(curr, total, this.reporter.lang('linkingDependencies'), emoji.get('link'));
await this.linker.init(flattenedTopLevelPatterns, this.flags.linkDuplicates);
await this.linker.init(flattenedTopLevelPatterns, this.flags.linkDuplicates, workspaceLayout);
});
steps.push(async (curr: number, total: number) => {
@@ -482,7 +489,7 @@ export class Install {
}
// fin!
await this.saveLockfileAndIntegrity(topLevelPatterns);
await this.saveLockfileAndIntegrity(topLevelPatterns, workspaceLayout);
this.maybeOutputUpdate();
this.config.requestManager.clearCache();
return flattenedTopLevelPatterns;
@@ -624,13 +631,23 @@ export class Install {
* Save updated integrity and lockfiles.
*/
async saveLockfileAndIntegrity(patterns: Array<string>): Promise<void> {
async saveLockfileAndIntegrity(patterns: Array<string>, workspaceLayout: ?WorkspaceLayout): Promise<void> {
// --no-lockfile or --pure-lockfile flag
if (this.flags.lockfile === false || this.flags.pureLockfile) {
return;
}
const lockfileBasedOnResolver = this.lockfile.getLockfile(this.resolver.patterns);
const resolvedPatterns: {[packagePattern: string]: Manifest} = {};
Object.keys(this.resolver.patterns).forEach(pattern => {
if (!workspaceLayout || !workspaceLayout.getManifestByPattern(pattern)) {
resolvedPatterns[pattern] = this.resolver.patterns[pattern];
}
});
// TODO this code is duplicated in a few places, need a common way to filter out workspace patterns from lockfile
patterns = patterns.filter(p => !workspaceLayout || !workspaceLayout.getManifestByPattern(p));
const lockfileBasedOnResolver = this.lockfile.getLockfile(resolvedPatterns);
if (this.config.pruneOfflineMirror) {
await this.pruneOfflineMirror(lockfileBasedOnResolver);
@@ -674,33 +691,38 @@ export class Install {
/**
* Load the dependency graph of the current install. Only does package resolving and wont write to the cwd.
*/
async hydrate(fetch?: boolean, ignoreUnusedPatterns?: boolean): Promise<InstallCwdRequest> {
async hydrate(ignoreUnusedPatterns?: boolean): Promise<InstallCwdRequest> {
const request = await this.fetchRequestFromCwd([], ignoreUnusedPatterns);
const {requests: depRequests, patterns: rawPatterns, ignorePatterns} = request;
const {requests: depRequests, patterns: rawPatterns, ignorePatterns, workspaceLayout} = request;
await this.resolver.init(depRequests, this.flags.flat);
await this.resolver.init(depRequests, this.flags.flat, workspaceLayout);
await this.flatten(rawPatterns);
this.markIgnored(ignorePatterns);
if (fetch) {
// fetch packages, should hit cache most of the time
const manifests: Array<Manifest> = await fetcher.fetch(this.resolver.getManifests(), this.config);
this.resolver.updateManifests(manifests);
await compatibility.check(this.resolver.getManifests(), this.config, this.flags.ignoreEngines);
// fetch packages, should hit cache most of the time
const manifests: Array<Manifest> = await fetcher.fetch(this.resolver.getManifests(), this.config);
this.resolver.updateManifests(manifests);
await compatibility.check(this.resolver.getManifests(), this.config, this.flags.ignoreEngines);
// expand minimal manifests
for (const manifest of this.resolver.getManifests()) {
const ref = manifest._reference;
invariant(ref, 'expected reference');
const {type} = ref.remote;
// link specifier won't ever hit cache
if (type === 'link') {
// expand minimal manifests
for (const manifest of this.resolver.getManifests()) {
const ref = manifest._reference;
invariant(ref, 'expected reference');
const {type} = ref.remote;
// link specifier won't ever hit cache
let loc = '';
if (type === 'link') {
continue;
} else if (type === 'workspace') {
if (!ref.remote.reference) {
continue;
}
const loc = this.config.generateHardModulePath(ref);
const newPkg = await this.config.readManifest(loc);
await this.resolver.updateManifest(ref, newPkg);
loc = ref.remote.reference;
} else {
loc = this.config.generateHardModulePath(ref);
}
const newPkg = await this.config.readManifest(loc);
await this.resolver.updateManifest(ref, newPkg);
}
return request;

View File

@@ -17,7 +17,7 @@ export function hasWrapper(flags: Object, args: Array<string>): boolean {
async function getManifests(config: Config, flags: Object): Promise<Array<Manifest>> {
const lockfile = await Lockfile.fromDirectory(config.cwd);
const install = new Install({skipIntegrityCheck: true, ...flags}, config, new NoopReporter(), lockfile);
await install.hydrate(true, true);
await install.hydrate(true);
let manifests = install.resolver.getManifests();

View File

@@ -14,29 +14,29 @@ export function hasWrapper(): boolean {
}
export async function run(config: Config, reporter: Reporter, flags: Object, args: Array<string>): Promise<void> {
const {worktreeFolder} = config;
const {workspaceRootFolder} = config;
if (!worktreeFolder) {
throw new MessageError(reporter.lang('worktreeRootNotFound', config.cwd));
if (!workspaceRootFolder) {
throw new MessageError(reporter.lang('workspaceRootNotFound', config.cwd));
}
if (args.length < 1) {
throw new MessageError(reporter.lang('worktreeMissingWorkspace'));
throw new MessageError(reporter.lang('workspaceMissingWorkspace'));
}
if (args.length < 2) {
throw new MessageError(reporter.lang('worktreeMissingCommand'));
throw new MessageError(reporter.lang('workspaceMissingCommand'));
}
const manifest = await config.findManifest(worktreeFolder, false);
const manifest = await config.findManifest(workspaceRootFolder, false);
invariant(manifest && manifest.workspaces, 'We must find a manifest with a "workspaces" property');
const workspaces = await config.resolveWorkspaces(worktreeFolder, manifest.workspaces);
const workspaces = await config.resolveWorkspaces(workspaceRootFolder, manifest);
const [workspaceName, ...rest] = args;
if (!Object.prototype.hasOwnProperty.call(workspaces, workspaceName)) {
throw new MessageError(reporter.lang('worktreeUnknownWorkspace', workspaceName));
throw new MessageError(reporter.lang('workspaceUnknownWorkspace', workspaceName));
}
try {

View File

@@ -2,7 +2,7 @@
import type {RegistryNames, ConfigRegistries} from './registries/index.js';
import type {Reporter} from './reporters/index.js';
import type {Manifest, PackageRemote} from './types.js';
import type {Manifest, PackageRemote, WorkspacesManifestMap} from './types.js';
import type PackageReference from './package-reference.js';
import {execFromManifest} from './util/execute-lifecycle-script.js';
import {expandPath} from './util/path.js';
@@ -145,11 +145,11 @@ export default class Config {
nonInteractive: boolean;
workspacesExperimental: boolean;
workspacesEnabled: boolean;
//
cwd: string;
worktreeFolder: ?string;
workspaceRootFolder: ?string;
lockfileFolder: string;
//
@@ -188,7 +188,7 @@ export default class Config {
getOption(key: string, expand: boolean = true): mixed {
const value = this.registries.yarn.getOption(key);
if (expand && (typeof value === 'string')) {
if (expand && typeof value === 'string') {
return expandPath(value);
}
@@ -210,8 +210,8 @@ export default class Config {
async init(opts: ConfigOptions = {}): Promise<void> {
this._init(opts);
this.worktreeFolder = await this.findWorktree(this.cwd);
this.lockfileFolder = this.worktreeFolder || this.cwd;
this.workspaceRootFolder = await this.findWorkspaceRoot(this.cwd);
this.lockfileFolder = this.workspaceRootFolder || this.cwd;
await fs.mkdirp(this.globalFolder);
await fs.mkdirp(this.linkFolder);
@@ -273,7 +273,7 @@ export default class Config {
this._cacheRootFolder = String(
opts.cacheFolder || this.getOption('cache-folder') || constants.MODULE_CACHE_DIRECTORY,
);
this.workspacesExperimental = Boolean(this.getOption('workspaces-experimental'));
this.workspacesEnabled = Boolean(this.getOption('workspaces-experimental'));
this.pruneOfflineMirror = Boolean(this.getOption('yarn-offline-mirror-pruning'));
this.enableMetaFolder = Boolean(this.getOption('enable-meta-folder'));
@@ -299,8 +299,8 @@ export default class Config {
this.production = !!opts.production;
}
if (this.worktreeFolder && !this.workspacesExperimental) {
throw new MessageError(this.reporter.lang('worktreeExperimentalDisabled'));
if (this.workspaceRootFolder && !this.workspacesEnabled) {
throw new MessageError(this.reporter.lang('workspaceExperimentalDisabled'));
}
}
@@ -552,7 +552,7 @@ export default class Config {
return null;
}
async findWorktree(initial: string): Promise<?string> {
async findWorkspaceRoot(initial: string): Promise<?string> {
let previous = null;
let current = path.normalize(initial);
@@ -570,11 +570,15 @@ export default class Config {
return null;
}
async resolveWorkspaces(
root: string,
patterns: Array<string>,
): Promise<{[string]: {loc: string, manifest: Manifest}}> {
async resolveWorkspaces(root: string, rootManifest: Manifest): Promise<WorkspacesManifestMap> {
const workspaces = {};
const patterns = rootManifest.workspaces || [];
if (!this.workspacesEnabled) {
return workspaces;
}
if (!rootManifest.private && patterns.length > 0) {
throw new MessageError(this.reporter.lang('workspacesRequirePrivateProjects'));
}
const registryFilenames = registryNames.map(registryName => this.registries[registryName].constructor.filename);
const trailingPattern = `/+(${registryFilenames.join(`|`)})`;
@@ -594,13 +598,16 @@ export default class Config {
}
if (!manifest.name) {
// TODO raise a warning?
this.reporter.warn(this.reporter.lang('workspaceNameMandatory', loc));
continue;
}
if (!manifest.version) {
this.reporter.warn(this.reporter.lang('workspaceVersionMandatory', loc));
continue;
}
if (Object.prototype.hasOwnProperty.call(workspaces, manifest.name)) {
// TODO raise a warning?
continue;
throw new MessageError(this.reporter.lang('workspaceNameDuplicate', manifest.name));
}
workspaces[manifest.name] = {loc, manifest};

View File

@@ -12,4 +12,4 @@ export {TarballFetcher as tarball};
export type Fetchers = BaseFetcher | CopyFetcher | GitFetcher | TarballFetcher;
export type FetcherNames = 'base' | 'copy' | 'git' | 'link' | 'tarball';
export type FetcherNames = 'base' | 'copy' | 'git' | 'link' | 'tarball' | 'workspace';

View File

@@ -8,6 +8,7 @@ import {registryNames} from './registries/index.js';
import * as fs from './util/fs.js';
import {sortAlpha, compareSortedArrays} from './util/misc.js';
import type {InstallArtifacts} from './package-install-scripts.js';
import WorkspaceLayout from './workspace-layout.js';
const invariant = require('invariant');
const path = require('path');
@@ -243,9 +244,12 @@ export default class InstallationIntegrityChecker {
patterns: Array<string>,
lockfile: {[key: string]: LockManifest},
flags: IntegrityFlags,
workspaceLayout: ?WorkspaceLayout,
): Promise<IntegrityCheckResult> {
// check if patterns exist in lockfile
const missingPatterns = patterns.filter(p => !lockfile[p]);
const missingPatterns = patterns.filter(
p => !lockfile[p] && (!workspaceLayout || !workspaceLayout.getManifestByPattern(p)),
);
const loc = await this._getIntegrityHashLocation();
if (missingPatterns.length || !loc.exists) {

View File

@@ -25,8 +25,8 @@ async function fetchOne(ref: PackageReference, config: Config): Promise<FetchedM
const remote = ref.remote;
// Mock metedata for linked dependencies
if (remote.type === 'link') {
// Mock metedata for symlinked dependencies
if (remote.type === 'link' || remote.type === 'workspace') {
const mockPkg: Manifest = {_uid: '', name: '', version: '0.0.0'};
return Promise.resolve({resolved: null, hash: '', dest, package: mockPkg, cached: false});
}

View File

@@ -14,6 +14,7 @@ import {entries} from './util/misc.js';
import * as fs from './util/fs.js';
import lockMutex from './util/mutex.js';
import {satisfiesWithPreleases} from './util/semver.js';
import WorkspaceLayout from './workspace-layout.js';
const invariant = require('invariant');
const cmdShim = promise.promisify(require('cmd-shim'));
@@ -124,7 +125,11 @@ export default class PackageLinker {
return Promise.resolve(hoister.init());
}
async copyModules(patterns: Array<string>, linkDuplicates: boolean): Promise<void> {
async copyModules(
patterns: Array<string>,
linkDuplicates: boolean,
workspaceLayout?: WorkspaceLayout,
): Promise<void> {
let flatTree = await this.getFlatHoistedTree(patterns);
// sorted tree makes file creation and copying not to interfere with each other
@@ -140,22 +145,47 @@ export default class PackageLinker {
const hardlinksEnabled = linkDuplicates && (await fs.hardlinksWork(this.config.cwd));
const copiedSrcs: Map<string, string> = new Map();
for (const [dest, {pkg, loc}] of flatTree) {
const symlinkPaths: Map<string, string> = new Map();
for (const [folder, {pkg, loc}] of flatTree) {
const remote = pkg._remote || {type: ''};
const ref = pkg._reference;
const src = remote.type === 'link' ? remote.reference : loc;
let dest = folder;
invariant(ref, 'expected package reference');
ref.setLocation(dest);
// backwards compatibility: get build artifacts from metadata
// does not apply to linked dependencies
if (remote.type !== 'link') {
let src = loc;
let type = '';
if (remote.type === 'link') {
// replace package source from incorrect cache location (workspaces and link: are not cached)
// with a symlink source
src = remote.reference;
type = 'symlink';
} else if (workspaceLayout && remote.type === 'workspace') {
src = remote.reference;
type = 'symlink';
if (dest.indexOf(workspaceLayout.virtualManifestName) !== -1) {
// we don't need to install virtual manifest
continue;
}
// to get real path for non hoisted dependencies
symlinkPaths.set(dest, src);
} else {
// backwards compatibility: get build artifacts from metadata
// does not apply to symlinked dependencies
const metadata = await this.config.readPackageMetadata(src);
for (const file of metadata.artifacts) {
artifactFiles.push(path.join(dest, file));
}
}
// fs copy can't copy through a symlink, so we replace those with real paths to workpsaces
for (const [symlink, realpath] of symlinkPaths.entries()) {
if (dest !== symlink && dest.indexOf(symlink) === 0) {
dest = dest.replace(symlink, realpath);
}
}
ref.setLocation(dest);
const integrityArtifacts = this.artifacts[`${pkg.name}@${pkg.version}`];
if (integrityArtifacts) {
for (const file of integrityArtifacts) {
@@ -171,7 +201,7 @@ export default class PackageLinker {
copyQueue.set(dest, {
src,
dest,
type: remote.type,
type,
onFresh() {
if (ref) {
ref.setFresh(true);
@@ -355,8 +385,8 @@ export default class PackageLinker {
return range === '*' || satisfiesWithPreleases(version, range, this.config.looseSemver);
}
async init(patterns: Array<string>, linkDuplicates: boolean): Promise<void> {
async init(patterns: Array<string>, linkDuplicates: boolean, workspaceLayout?: WorkspaceLayout): Promise<void> {
this.resolvePeerModules();
await this.copyModules(patterns, linkDuplicates);
await this.copyModules(patterns, linkDuplicates, workspaceLayout);
}
}

View File

@@ -13,6 +13,7 @@ import {MessageError} from './errors.js';
import {entries} from './util/misc.js';
import * as constants from './constants.js';
import * as versionUtil from './util/version.js';
import WorkspaceResolver from './resolvers/contextual/workspace-resolver.js';
import * as resolvers from './resolvers/index.js';
import * as fs from './util/fs.js';
@@ -221,6 +222,10 @@ export default class PackageRequest {
const exoticResolver = PackageRequest.getExoticResolver(this.pattern);
if (exoticResolver) {
return this.findExoticVersionInfo(exoticResolver, this.pattern);
} else if (WorkspaceResolver.isWorkspace(this.pattern, this.resolver.workspaceLayout)) {
invariant(this.resolver.workspaceLayout, 'expected workspaceLayout');
const resolver = new WorkspaceResolver(this, this.pattern, this.resolver.workspaceLayout);
return resolver.resolve();
} else {
return this.findVersionOnRegistry(this.pattern);
}
@@ -321,6 +326,21 @@ export default class PackageRequest {
}),
);
}
if (remote.type === 'workspace' && !this.config.production) {
// workspaces support dev dependencies
for (const depName in info.devDependencies) {
const depPattern = depName + '@' + info.devDependencies[depName];
deps.push(depPattern);
promises.push(
this.resolver.find({
pattern: depPattern,
registry: remote.registry,
optional: false,
parentRequest: this,
}),
);
}
}
await Promise.all(promises);
ref.addDependencies(deps);

View File

@@ -10,6 +10,7 @@ import RequestManager from './util/request-manager.js';
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';
const invariant = require('invariant');
const semver = require('semver');
@@ -32,6 +33,8 @@ export default class PackageResolver {
// whether the dependency graph will be flattened
flat: boolean;
workspaceLayout: ?WorkspaceLayout;
// list of registries that have been used in this resolution
usedRegistries: Set<RegistryNames>;
@@ -452,8 +455,9 @@ export default class PackageResolver {
* TODO description
*/
async init(deps: DependencyRequestPatterns, isFlat: boolean): Promise<void> {
async init(deps: DependencyRequestPatterns, isFlat: boolean, workspaceLayout?: WorkspaceLayout): Promise<void> {
this.flat = isFlat;
this.workspaceLayout = workspaceLayout;
const activity = (this.activity = this.reporter.activity());
await Promise.all(deps.map((req): Promise<void> => this.find(req)));

View File

@@ -97,8 +97,6 @@ const messages = {
fileWriteError: 'Could not write file $0: $1',
multiplePackagesCantUnpackInSameDestination: '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.',
workspacesIncompatibleDependencies: 'Dependency $0 has different versions in $1 and $2',
workspacesRequirePrivateProjects: 'Workspaces can only be enabled for private projects',
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.',
@@ -142,11 +140,15 @@ const messages = {
createInvalidBin: 'Invalid bin entry found in package $0.',
createMissingPackage: 'Package not found - this is probably an internal error, and should be reported at https://github.com/yarnpkg/yarn/issues.',
worktreeExperimentalDisabled: 'The worktree feature is currently experimental and needs to be manually enabled - please add "workspaces-experimental true" to your .yarnrc file.',
worktreeRootNotFound: "Cannot find the root of your worktree - are you sure you're currently in a workspace?",
worktreeMissingWorkspace: 'Missing workspace name.',
worktreeMissingCommand: 'Missing command name.',
worktreeUnknownWorkspace: 'Unknown workspace $0.',
workspacesRequirePrivateProjects: 'Workspaces can only be enabled in private projects',
workspaceExperimentalDisabled: 'The workspace feature is currently experimental and needs to be manually enabled - please add "workspaces-experimental true" to your .yarnrc file.',
workspaceRootNotFound: "Cannot find the root of your workspace - are you sure you're currently in a workspace?",
workspaceMissingWorkspace: 'Missing workspace name.',
workspaceMissingCommand: 'Missing command name.',
workspaceUnknownWorkspace: 'Unknown workspace $0.',
workspaceVersionMandatory: 'Missing version in workspace at $0, ignoring.',
workspaceNameMandatory: 'Missing name in workspace at $0, ignoring.',
workspaceNameDuplicate: 'There are more than one workspace with name $0',
execMissingCommand: 'Missing command name.',

View File

@@ -0,0 +1,40 @@
/* @flow */
import type {Manifest} from '../../types.js';
import PackageRequest from '../../package-request.js';
import BaseResolver from '../base-resolver.js';
import WorkspaceLayout from '../../workspace-layout.js';
const invariant = require('invariant');
export default class WorkspaceResolver extends BaseResolver {
static isWorkspace(pattern: string, workspaceLayout: ?WorkspaceLayout): boolean {
return !!workspaceLayout && !!workspaceLayout.getManifestByPattern(pattern);
}
constructor(request: PackageRequest, fragment: string, workspaceLayout: WorkspaceLayout) {
super(request, fragment);
this.workspaceLayout = workspaceLayout;
}
workspaceLayout: WorkspaceLayout;
resolve(): Promise<Manifest> {
const workspace = this.workspaceLayout.getManifestByPattern(this.request.pattern);
invariant(workspace, 'expected workspace');
const {manifest, loc} = workspace;
const registry = manifest._registry;
invariant(registry, 'expected reference');
manifest._remote = {
type: 'workspace',
registry,
hash: '',
reference: loc,
};
manifest._uid = manifest.version;
return Promise.resolve(manifest);
}
}

View File

@@ -53,6 +53,8 @@ export type Manifest = {
name: string,
version: string,
private?: boolean,
author?: {
name?: string,
email?: string,
@@ -152,3 +154,7 @@ export type Dependency = {
url: string,
hint: ?string,
};
export type WorkspacesManifestMap = {
[string]: {loc: string, manifest: Manifest},
};

View File

@@ -165,7 +165,7 @@ async function buildActionsForCopy(
const onDone = data.onDone || noop;
files.add(dest);
if (type === 'link') {
if (type === 'symlink') {
await mkdirp(path.dirname(dest));
onFresh();
actions.push({

31
src/workspace-layout.js Normal file
View File

@@ -0,0 +1,31 @@
/* @flow */
import type Config from './config.js';
import PackageRequest from './package-request.js';
import type {WorkspacesManifestMap, Manifest} from './types.js';
const semver = require('semver');
export default class WorkspaceLayout {
constructor(workspaces: WorkspacesManifestMap, config: Config) {
this.workspaces = workspaces;
this.config = config;
}
workspaces: WorkspacesManifestMap;
config: Config;
virtualManifestName: string;
getWorkspaceManifest(key: string): {loc: string, manifest: Manifest} {
return this.workspaces[key];
}
getManifestByPattern(pattern: string): ?{loc: string, manifest: Manifest} {
const {name, range} = PackageRequest.normalizePattern(pattern);
const workspace = this.getWorkspaceManifest(name);
if (!workspace || !semver.satisfies(workspace.manifest.version, range, this.config.looseSemver)) {
return null;
}
return workspace;
}
}