From 4696867337f97bd7a7cea96f7ce50d224099a5f0 Mon Sep 17 00:00:00 2001 From: Luke Batchelor Date: Wed, 20 Sep 2017 16:31:18 +1000 Subject: [PATCH 01/14] Adds utils/pyarn.add() command --- src/utils/__tests__/yarn.test.js | 61 ++++++++++++++++++++++++++++++-- src/utils/yarn.js | 37 +++++++++++++++++++ 2 files changed, 95 insertions(+), 3 deletions(-) diff --git a/src/utils/__tests__/yarn.test.js b/src/utils/__tests__/yarn.test.js index 85a6b2e2..0e2d8ea7 100644 --- a/src/utils/__tests__/yarn.test.js +++ b/src/utils/__tests__/yarn.test.js @@ -1,7 +1,62 @@ // @flow + +import { getFixturePath } from 'jest-fixtures'; + import * as yarn from '../yarn'; +import * as processes from '../processes'; +import Project from '../../Project'; + +jest.mock('../processes'); + +const unsafeProcesses: any & typeof processes = processes; + +function assertSpawnCalls(expectedArgs, expectedCwd) { + const spawnCalls = unsafeProcesses.spawn.mock.calls; + + expect(spawnCalls.length).toEqual(1); + expect(spawnCalls[0][0]).toEqual('yarn'); + expect(spawnCalls[0][1]).toEqual(expectedArgs); + expect(spawnCalls[0][2].cwd).toEqual(expectedCwd); +} + +describe('utils/yarn', () => { + describe('add()', () => { + let cwd; + let project; + + beforeEach(async () => { + cwd = await getFixturePath(__dirname, 'simple-project'); + project = await Project.init(cwd); + }); + + it('should be able to add a dependency', async () => { + await yarn.add(project.pkg, { name: 'chalk' }); + assertSpawnCalls(['add', 'chalk'], cwd); + }); + + it('should be able to add a dev dependency', async () => { + await yarn.add(project.pkg, { name: 'chalk', type: 'devDependencies' }); + assertSpawnCalls(['add', 'chalk', '--dev'], cwd); + }); + + it('should be able to add a peer dependency', async () => { + await yarn.add(project.pkg, { name: 'chalk', type: 'peerDependencies' }); + assertSpawnCalls(['add', 'chalk', '--peer'], cwd); + }); + + it('should be able to add an optional dependency', async () => { + await yarn.add(project.pkg, { + name: 'chalk', + type: 'optionalDependencies' + }); + assertSpawnCalls(['add', 'chalk', '--optional'], cwd); + }); -describe('yarn', () => { - test('run()'); - test('init()'); + it('should be able to add dependency with versions', async () => { + await yarn.add(project.pkg, { name: 'chalk', version: '^1.0.0' }); + assertSpawnCalls(['add', 'chalk@^1.0.0'], cwd); + }); + }); + describe('run()', () => {}); + describe('init()', () => {}); }); diff --git a/src/utils/yarn.js b/src/utils/yarn.js index f496e2ca..06aa9bed 100644 --- a/src/utils/yarn.js +++ b/src/utils/yarn.js @@ -5,6 +5,43 @@ import type Package from '../Package'; import * as processes from './processes'; import * as fs from '../utils/fs'; +export async function add( + pkg: Package, + dependency: { + name: string, + version?: string, + type?: + | 'dependencies' + | 'devDependencies' + | 'peerDependencies' + | 'optionalDependencies' + } +) { + const spawnArgs = ['add']; + if (dependency.version) { + spawnArgs.push(`${dependency.name}@${dependency.version}`); + } else { + spawnArgs.push(dependency.name); + } + + if (dependency.type) { + const typeToFlagMap = { + dependencies: '', + devDependencies: '--dev', + peerDependencies: '--peer', + optionalDependencies: '--optional' + }; + const flag = typeToFlagMap[dependency.type]; + if (flag) spawnArgs.push(flag); + } + + await processes.spawn('yarn', spawnArgs, { + cwd: pkg.dir, + pkg: pkg, + tty: true + }); +} + export async function run( pkg: Package, script: string, From 62d014d5a35b5eed73a0e926feb4e615924abcf6 Mon Sep 17 00:00:00 2001 From: Luke Batchelor Date: Wed, 20 Sep 2017 17:07:52 +1000 Subject: [PATCH 02/14] Allows yarn.add fn to add more than one dependency --- src/utils/__tests__/yarn.test.js | 18 ++++++++------- src/utils/yarn.js | 38 ++++++++++++++++++-------------- 2 files changed, 32 insertions(+), 24 deletions(-) diff --git a/src/utils/__tests__/yarn.test.js b/src/utils/__tests__/yarn.test.js index 0e2d8ea7..c2e59a5a 100644 --- a/src/utils/__tests__/yarn.test.js +++ b/src/utils/__tests__/yarn.test.js @@ -30,32 +30,34 @@ describe('utils/yarn', () => { }); it('should be able to add a dependency', async () => { - await yarn.add(project.pkg, { name: 'chalk' }); + await yarn.add(project.pkg, [{ name: 'chalk' }]); assertSpawnCalls(['add', 'chalk'], cwd); }); it('should be able to add a dev dependency', async () => { - await yarn.add(project.pkg, { name: 'chalk', type: 'devDependencies' }); + await yarn.add(project.pkg, [{ name: 'chalk' }], 'devDependencies'); assertSpawnCalls(['add', 'chalk', '--dev'], cwd); }); it('should be able to add a peer dependency', async () => { - await yarn.add(project.pkg, { name: 'chalk', type: 'peerDependencies' }); + await yarn.add(project.pkg, [{ name: 'chalk' }], 'peerDependencies'); assertSpawnCalls(['add', 'chalk', '--peer'], cwd); }); it('should be able to add an optional dependency', async () => { - await yarn.add(project.pkg, { - name: 'chalk', - type: 'optionalDependencies' - }); + await yarn.add(project.pkg, [{ name: 'chalk' }], 'optionalDependencies'); assertSpawnCalls(['add', 'chalk', '--optional'], cwd); }); it('should be able to add dependency with versions', async () => { - await yarn.add(project.pkg, { name: 'chalk', version: '^1.0.0' }); + await yarn.add(project.pkg, [{ name: 'chalk', version: '^1.0.0' }]); assertSpawnCalls(['add', 'chalk@^1.0.0'], cwd); }); + + it('should be able to add multiple dependencies', async () => { + await yarn.add(project.pkg, [{ name: 'chalk' }, { name: 'left-pad' }]); + assertSpawnCalls(['add', 'chalk', 'left-pad'], cwd); + }); }); describe('run()', () => {}); describe('init()', () => {}); diff --git a/src/utils/yarn.js b/src/utils/yarn.js index 06aa9bed..f39bf467 100644 --- a/src/utils/yarn.js +++ b/src/utils/yarn.js @@ -5,33 +5,39 @@ import type Package from '../Package'; import * as processes from './processes'; import * as fs from '../utils/fs'; +type Dependency = { + name: string, + version?: string +}; + export async function add( pkg: Package, - dependency: { - name: string, - version?: string, - type?: - | 'dependencies' - | 'devDependencies' - | 'peerDependencies' - | 'optionalDependencies' - } + dependencies: Array, + type?: + | 'dependencies' + | 'devDependencies' + | 'peerDependencies' + | 'optionalDependencies' ) { const spawnArgs = ['add']; - if (dependency.version) { - spawnArgs.push(`${dependency.name}@${dependency.version}`); - } else { - spawnArgs.push(dependency.name); - } + if (!dependencies.length) return; + + dependencies.forEach(dep => { + if (dep.version) { + spawnArgs.push(`${dep.name}@${dep.version}`); + } else { + spawnArgs.push(dep.name); + } + }); - if (dependency.type) { + if (type) { const typeToFlagMap = { dependencies: '', devDependencies: '--dev', peerDependencies: '--peer', optionalDependencies: '--optional' }; - const flag = typeToFlagMap[dependency.type]; + const flag = typeToFlagMap[type]; if (flag) spawnArgs.push(flag); } From 1481f26ee6035657b03c399401d625dcca866a87 Mon Sep 17 00:00:00 2001 From: Luke Batchelor Date: Thu, 21 Sep 2017 18:06:18 +1000 Subject: [PATCH 03/14] Intial implementation of symlinkPackageDependencies --- example-2/package.json | 14 ++ example-2/packages/bar/README.md | 3 + example-2/packages/bar/package.json | 13 ++ example-2/packages/foo/README.md | 3 + example-2/packages/foo/package.json | 19 ++ example-2/packages/foo/packages/baz/README.md | 3 + .../packages/foo/packages/baz/package.json | 14 ++ example-2/packages/foo/yarn.lock | 200 ++++++++++++++++++ example-2/yarn.lock | 146 +++++++++++++ package.json | 2 +- .../package.json | 18 ++ .../packages/bar/package.json | 8 + .../packages/foo/package.json | 9 + .../packages/foo/packages/baz/package.json | 9 + .../yarn.lock | 19 ++ .../nested-workspaces/package.json | 3 +- .../simple-project/packages/bar/package.json | 2 +- .../simple-project/packages/foo/package.json | 2 +- src/commands/install.js | 4 + src/types.js | 11 + .../addDependenciesToPackages.test.js | 77 +++++++ .../symlinkPackageDependencies.test.js | 111 ++++++++++ src/utils/addDependenciesToPackages.js | 31 +++ src/utils/symlinkPackageDependencies.js | 156 ++++++++++++++ src/utils/yarn.js | 12 +- yarn.lock | 27 ++- 26 files changed, 898 insertions(+), 18 deletions(-) create mode 100644 example-2/package.json create mode 100644 example-2/packages/bar/README.md create mode 100644 example-2/packages/bar/package.json create mode 100644 example-2/packages/foo/README.md create mode 100644 example-2/packages/foo/package.json create mode 100644 example-2/packages/foo/packages/baz/README.md create mode 100644 example-2/packages/foo/packages/baz/package.json create mode 100644 example-2/packages/foo/yarn.lock create mode 100644 example-2/yarn.lock create mode 100644 src/__fixtures__/nested-workspaces-with-dependencies-installed/package.json create mode 100644 src/__fixtures__/nested-workspaces-with-dependencies-installed/packages/bar/package.json create mode 100644 src/__fixtures__/nested-workspaces-with-dependencies-installed/packages/foo/package.json create mode 100644 src/__fixtures__/nested-workspaces-with-dependencies-installed/packages/foo/packages/baz/package.json create mode 100644 src/__fixtures__/nested-workspaces-with-dependencies-installed/yarn.lock create mode 100644 src/utils/__tests__/addDependenciesToPackages.test.js create mode 100644 src/utils/__tests__/symlinkPackageDependencies.test.js create mode 100644 src/utils/addDependenciesToPackages.js create mode 100644 src/utils/symlinkPackageDependencies.js diff --git a/example-2/package.json b/example-2/package.json new file mode 100644 index 00000000..d2a5e05d --- /dev/null +++ b/example-2/package.json @@ -0,0 +1,14 @@ +{ + "private": true, + "name": "fixture-project", + "scripts": { + "clean": "rm -rf packages/**/node_modules && rm -rf node_modules" + }, + "pworkspaces": [ + "packages/*" + ], + "dependencies": { + "chalk": "^2.1.0", + "react": "^15.6.1" + } +} diff --git a/example-2/packages/bar/README.md b/example-2/packages/bar/README.md new file mode 100644 index 00000000..e2dce07a --- /dev/null +++ b/example-2/packages/bar/README.md @@ -0,0 +1,3 @@ +# @lbatchelor/bar + +A placeholder package used for testing \ No newline at end of file diff --git a/example-2/packages/bar/package.json b/example-2/packages/bar/package.json new file mode 100644 index 00000000..403007d7 --- /dev/null +++ b/example-2/packages/bar/package.json @@ -0,0 +1,13 @@ +{ + "name": "@lbatchelor/bar", + "version": "1.0.0", + "license": "MIT", + "scripts": { + "preinstall": "echo preinstall", + "postinstall": "echo postinstall", + "prepublish": "echo prepublish" + }, + "dependencies": { + "react": "^15.6.1" + } +} diff --git a/example-2/packages/foo/README.md b/example-2/packages/foo/README.md new file mode 100644 index 00000000..5ea56f98 --- /dev/null +++ b/example-2/packages/foo/README.md @@ -0,0 +1,3 @@ +# @lbatchelor/foo + +A placeholder package used for testing \ No newline at end of file diff --git a/example-2/packages/foo/package.json b/example-2/packages/foo/package.json new file mode 100644 index 00000000..5d98e40f --- /dev/null +++ b/example-2/packages/foo/package.json @@ -0,0 +1,19 @@ +{ + "name": "@lbatchelor/foo", + "version": "1.0.11", + "license": "MIT", + "scripts": { + "preinstall": "echo preinstall from foo", + "postinstall": "echo postinstall from foo", + "prepublish": "echo prepublish from foo" + }, + "pworkspaces": [ + "packages/*" + ], + "dependencies": { + "@lbatchelor/bar": "^1.0.0", + "chalk": "^2.1.0", + "cowsay": "^1.2.1", + "react": "^15.6.1" + } +} diff --git a/example-2/packages/foo/packages/baz/README.md b/example-2/packages/foo/packages/baz/README.md new file mode 100644 index 00000000..0cfc3d79 --- /dev/null +++ b/example-2/packages/foo/packages/baz/README.md @@ -0,0 +1,3 @@ +# @lbatchelor/baz + +A placeholder package used for testing \ No newline at end of file diff --git a/example-2/packages/foo/packages/baz/package.json b/example-2/packages/foo/packages/baz/package.json new file mode 100644 index 00000000..24b9b422 --- /dev/null +++ b/example-2/packages/foo/packages/baz/package.json @@ -0,0 +1,14 @@ +{ + "name": "@lbatchelor/baz", + "version": "1.0.0", + "license": "MIT", + "scripts": { + "preinstall": "echo preinstall", + "postinstall": "echo postinstall", + "prepublish": "echo prepublish" + }, + "dependencies": { + "react": "^15.6.1", + "@lbatchelor/bar": "^1.0.0" + } +} diff --git a/example-2/packages/foo/yarn.lock b/example-2/packages/foo/yarn.lock new file mode 100644 index 00000000..a4643602 --- /dev/null +++ b/example-2/packages/foo/yarn.lock @@ -0,0 +1,200 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@lbatchelor/bar@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@lbatchelor/bar/-/bar-1.0.0.tgz#941ff5942f1386d03707752fc80d71d37efdb972" + dependencies: + react "^15.6.1" + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + +ansi-styles@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.0.tgz#c159b8d5be0f9e5a6f346dab94f16ce022161b88" + dependencies: + color-convert "^1.9.0" + +asap@~2.0.3: + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + +chalk@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.1.0.tgz#ac5becf14fa21b99c6c92ca7a7d7cfd5b17e743e" + dependencies: + ansi-styles "^3.1.0" + escape-string-regexp "^1.0.5" + supports-color "^4.0.0" + +color-convert@^1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.0.tgz#1accf97dd739b983bf994d56fec8f95853641b7a" + dependencies: + color-name "^1.1.1" + +color-name@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + +core-js@^1.0.0: + version "1.2.7" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" + +cowsay@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/cowsay/-/cowsay-1.2.1.tgz#3bde9f17ba64049bd359ff57b8916ec81d0332fe" + dependencies: + get-stdin "^5.0.1" + optimist "~0.6.1" + string-width "~2.1.1" + +create-react-class@^15.6.0: + version "15.6.0" + resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.6.0.tgz#ab448497c26566e1e29413e883207d57cfe7bed4" + dependencies: + fbjs "^0.8.9" + loose-envify "^1.3.1" + object-assign "^4.1.1" + +encoding@^0.1.11: + version "0.1.12" + resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb" + dependencies: + iconv-lite "~0.4.13" + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + +fbjs@^0.8.9: + version "0.8.15" + resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.15.tgz#4f0695fdfcc16c37c0b07facec8cb4c4091685b9" + dependencies: + core-js "^1.0.0" + isomorphic-fetch "^2.1.1" + loose-envify "^1.0.0" + object-assign "^4.1.0" + promise "^7.1.1" + setimmediate "^1.0.5" + ua-parser-js "^0.7.9" + +get-stdin@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-5.0.1.tgz#122e161591e21ff4c52530305693f20e6393a398" + +has-flag@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" + +iconv-lite@~0.4.13: + version "0.4.19" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + +is-stream@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + +isomorphic-fetch@^2.1.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9" + dependencies: + node-fetch "^1.0.1" + whatwg-fetch ">=0.10.0" + +js-tokens@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" + +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848" + dependencies: + js-tokens "^3.0.0" + +minimist@~0.0.1: + version "0.0.10" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" + +node-fetch@^1.0.1: + version "1.7.3" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" + dependencies: + encoding "^0.1.11" + is-stream "^1.0.1" + +object-assign@^4.1.0, object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + +optimist@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" + dependencies: + minimist "~0.0.1" + wordwrap "~0.0.2" + +promise@^7.1.1: + version "7.3.1" + resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" + dependencies: + asap "~2.0.3" + +prop-types@^15.5.10: + version "15.5.10" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.10.tgz#2797dfc3126182e3a95e3dfbb2e893ddd7456154" + dependencies: + fbjs "^0.8.9" + loose-envify "^1.3.1" + +react@^15.6.1: + version "15.6.1" + resolved "https://registry.yarnpkg.com/react/-/react-15.6.1.tgz#baa8434ec6780bde997cdc380b79cd33b96393df" + dependencies: + create-react-class "^15.6.0" + fbjs "^0.8.9" + loose-envify "^1.1.0" + object-assign "^4.1.0" + prop-types "^15.5.10" + +setimmediate@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + +string-width@~2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + dependencies: + ansi-regex "^3.0.0" + +supports-color@^4.0.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.4.0.tgz#883f7ddabc165142b2a61427f3352ded195d1a3e" + dependencies: + has-flag "^2.0.0" + +ua-parser-js@^0.7.9: + version "0.7.14" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.14.tgz#110d53fa4c3f326c121292bbeac904d2e03387ca" + +whatwg-fetch@>=0.10.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz#9c84ec2dcf68187ff00bc64e1274b442176e1c84" + +wordwrap@~0.0.2: + version "0.0.3" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" diff --git a/example-2/yarn.lock b/example-2/yarn.lock new file mode 100644 index 00000000..9ecf8c83 --- /dev/null +++ b/example-2/yarn.lock @@ -0,0 +1,146 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +ansi-styles@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.0.tgz#c159b8d5be0f9e5a6f346dab94f16ce022161b88" + dependencies: + color-convert "^1.9.0" + +asap@~2.0.3: + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + +chalk@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.1.0.tgz#ac5becf14fa21b99c6c92ca7a7d7cfd5b17e743e" + dependencies: + ansi-styles "^3.1.0" + escape-string-regexp "^1.0.5" + supports-color "^4.0.0" + +color-convert@^1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.0.tgz#1accf97dd739b983bf994d56fec8f95853641b7a" + dependencies: + color-name "^1.1.1" + +color-name@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + +core-js@^1.0.0: + version "1.2.7" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" + +create-react-class@^15.6.0: + version "15.6.0" + resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.6.0.tgz#ab448497c26566e1e29413e883207d57cfe7bed4" + dependencies: + fbjs "^0.8.9" + loose-envify "^1.3.1" + object-assign "^4.1.1" + +encoding@^0.1.11: + version "0.1.12" + resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb" + dependencies: + iconv-lite "~0.4.13" + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + +fbjs@^0.8.9: + version "0.8.14" + resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.14.tgz#d1dbe2be254c35a91e09f31f9cd50a40b2a0ed1c" + dependencies: + core-js "^1.0.0" + isomorphic-fetch "^2.1.1" + loose-envify "^1.0.0" + object-assign "^4.1.0" + promise "^7.1.1" + setimmediate "^1.0.5" + ua-parser-js "^0.7.9" + +has-flag@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" + +iconv-lite@~0.4.13: + version "0.4.18" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.18.tgz#23d8656b16aae6742ac29732ea8f0336a4789cf2" + +is-stream@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + +isomorphic-fetch@^2.1.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9" + dependencies: + node-fetch "^1.0.1" + whatwg-fetch ">=0.10.0" + +js-tokens@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" + +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848" + dependencies: + js-tokens "^3.0.0" + +node-fetch@^1.0.1: + version "1.7.1" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.1.tgz#899cb3d0a3c92f952c47f1b876f4c8aeabd400d5" + dependencies: + encoding "^0.1.11" + is-stream "^1.0.1" + +object-assign@^4.1.0, object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + +promise@^7.1.1: + version "7.3.1" + resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" + dependencies: + asap "~2.0.3" + +prop-types@^15.5.10: + version "15.5.10" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.10.tgz#2797dfc3126182e3a95e3dfbb2e893ddd7456154" + dependencies: + fbjs "^0.8.9" + loose-envify "^1.3.1" + +react@^15.6.1: + version "15.6.1" + resolved "https://registry.yarnpkg.com/react/-/react-15.6.1.tgz#baa8434ec6780bde997cdc380b79cd33b96393df" + dependencies: + create-react-class "^15.6.0" + fbjs "^0.8.9" + loose-envify "^1.1.0" + object-assign "^4.1.0" + prop-types "^15.5.10" + +setimmediate@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + +supports-color@^4.0.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.4.0.tgz#883f7ddabc165142b2a61427f3352ded195d1a3e" + dependencies: + has-flag "^2.0.0" + +ua-parser-js@^0.7.9: + version "0.7.14" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.14.tgz#110d53fa4c3f326c121292bbeac904d2e03387ca" + +whatwg-fetch@>=0.10.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz#9c84ec2dcf68187ff00bc64e1274b442176e1c84" diff --git a/package.json b/package.json index b00f8b56..f6d60739 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "flow-bin": "^0.51.0", "husky": "^0.14.3", "jest": "^20.0.4", - "jest-fixtures": "^0.3.2", + "jest-fixtures": "^0.5.0", "lint-staged": "^4.1.3", "mode-to-permissions": "^0.0.2", "prettier": "^1.6.1" diff --git a/src/__fixtures__/nested-workspaces-with-dependencies-installed/package.json b/src/__fixtures__/nested-workspaces-with-dependencies-installed/package.json new file mode 100644 index 00000000..ec4a46f4 --- /dev/null +++ b/src/__fixtures__/nested-workspaces-with-dependencies-installed/package.json @@ -0,0 +1,18 @@ +{ + "private": true, + "version": "1.0.0", + "name": "fixture-project-nested-workspaces-with-dependencies-installed", + "description": "A project with all the node_modules and symlinks already set up", + "description2": "We deliberately choose small packages that have binary files for testing symlinking", + "pworkspaces": [ + "packages/*" + ], + "dependencies": { + "left-pad": "^1.1.3", + "semver": "^5.4.1", + "uuid": "^3.1.0" + }, + "devDependencies": { + "right-pad": "^1.0.1" + } +} diff --git a/src/__fixtures__/nested-workspaces-with-dependencies-installed/packages/bar/package.json b/src/__fixtures__/nested-workspaces-with-dependencies-installed/packages/bar/package.json new file mode 100644 index 00000000..d012d905 --- /dev/null +++ b/src/__fixtures__/nested-workspaces-with-dependencies-installed/packages/bar/package.json @@ -0,0 +1,8 @@ +{ + "name": "bar", + "version": "1.0.0", + "dependencies": { + "left-pad": "^1.1.3", + "uuid": "^3.1.0" + } +} diff --git a/src/__fixtures__/nested-workspaces-with-dependencies-installed/packages/foo/package.json b/src/__fixtures__/nested-workspaces-with-dependencies-installed/packages/foo/package.json new file mode 100644 index 00000000..53547e40 --- /dev/null +++ b/src/__fixtures__/nested-workspaces-with-dependencies-installed/packages/foo/package.json @@ -0,0 +1,9 @@ +{ + "name": "foo", + "version": "1.0.0", + "pworkspaces": ["packages/*"], + "dependencies": { + "bar": "^1.0.0", + "semver": "^5.4.1" + } +} diff --git a/src/__fixtures__/nested-workspaces-with-dependencies-installed/packages/foo/packages/baz/package.json b/src/__fixtures__/nested-workspaces-with-dependencies-installed/packages/foo/packages/baz/package.json new file mode 100644 index 00000000..038fa598 --- /dev/null +++ b/src/__fixtures__/nested-workspaces-with-dependencies-installed/packages/foo/packages/baz/package.json @@ -0,0 +1,9 @@ +{ + "name": "baz", + "version": "1.0.0", + "dependencies": { + "bar": "^1.0.0", + "uuid": "^3.1.0", + "right-pad": "^1.0.1" + } +} diff --git a/src/__fixtures__/nested-workspaces-with-dependencies-installed/yarn.lock b/src/__fixtures__/nested-workspaces-with-dependencies-installed/yarn.lock new file mode 100644 index 00000000..af2d1c59 --- /dev/null +++ b/src/__fixtures__/nested-workspaces-with-dependencies-installed/yarn.lock @@ -0,0 +1,19 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +left-pad@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/left-pad/-/left-pad-1.1.3.tgz#612f61c033f3a9e08e939f1caebeea41b6f3199a" + +right-pad@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/right-pad/-/right-pad-1.0.1.tgz#8ca08c2cbb5b55e74dafa96bf7fd1a27d568c8d0" + +semver@^5.4.1: + version "5.4.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e" + +uuid@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" diff --git a/src/__fixtures__/nested-workspaces/package.json b/src/__fixtures__/nested-workspaces/package.json index e4200e02..889b2dc6 100644 --- a/src/__fixtures__/nested-workspaces/package.json +++ b/src/__fixtures__/nested-workspaces/package.json @@ -4,6 +4,7 @@ "name": "fixture-project-nested-workspaces", "pworkspaces": ["packages/*"], "dependencies": { - "react": "^15.6.1" + "react": "^15.6.1", + "left-pad": "^1.1.3" } } diff --git a/src/__fixtures__/simple-project/packages/bar/package.json b/src/__fixtures__/simple-project/packages/bar/package.json index da86787a..06a8b8dc 100644 --- a/src/__fixtures__/simple-project/packages/bar/package.json +++ b/src/__fixtures__/simple-project/packages/bar/package.json @@ -1,4 +1,4 @@ { - "name": "foo", + "name": "bar", "version": "1.0.0" } diff --git a/src/__fixtures__/simple-project/packages/foo/package.json b/src/__fixtures__/simple-project/packages/foo/package.json index 06a8b8dc..da86787a 100644 --- a/src/__fixtures__/simple-project/packages/foo/package.json +++ b/src/__fixtures__/simple-project/packages/foo/package.json @@ -1,4 +1,4 @@ { - "name": "bar", + "name": "foo", "version": "1.0.0" } diff --git a/src/commands/install.js b/src/commands/install.js index 30332410..b84a52d3 100644 --- a/src/commands/install.js +++ b/src/commands/install.js @@ -41,6 +41,7 @@ export async function install(opts: InstallOptions) { let workspacesToDependencies = {}; + /** Calculate all the external dependencies that need to be symlinked */ for (let workspace of workspaces) { let dependencies = workspace.pkg.getAllDependencies(); @@ -84,6 +85,7 @@ export async function install(opts: InstallOptions) { } } + /** Calculate all the internal dependencies that need to be symlinked */ for (let [name, node] of dependencyGraph) { let nodeModules = path.join(node.pkg.dir, 'node_modules'); @@ -117,6 +119,8 @@ export async function install(opts: InstallOptions) { logger.log('[2/2] Linking workspace dependencies...'); + /** Calculate all the bin files that need to be symlinked */ + for (let binFile of await fs.readdirSafe(project.pkg.nodeModulesBin)) { let binPath = path.join(project.pkg.nodeModulesBin, binFile); let binName = path.basename(binPath); diff --git a/src/types.js b/src/types.js index c7f86e1e..4915d463 100644 --- a/src/types.js +++ b/src/types.js @@ -27,3 +27,14 @@ export type FilterOpts = { onlyFs?: string, ignoreFs?: string }; + +export type Dependency = { + name: string, + version?: string +}; + +export type configDependencyType = + | 'dependencies' + | 'devDependencies' + | 'peerDependencies' + | 'optionalDependencies'; diff --git a/src/utils/__tests__/addDependenciesToPackages.test.js b/src/utils/__tests__/addDependenciesToPackages.test.js new file mode 100644 index 00000000..ddaed243 --- /dev/null +++ b/src/utils/__tests__/addDependenciesToPackages.test.js @@ -0,0 +1,77 @@ +// @flow + +import { getFixturePath } from 'jest-fixtures'; + +import addDependenciesToPackage from '../addDependenciesToPackages'; +import * as yarn from '../yarn'; +import Project from '../../Project'; + +jest.mock('../yarn'); + +const unsafeYarn: any & typeof yarn = yarn; + +function assertSingleYarnAddCall(expectedPkg, expectedDeps) { + const yarnAddCalls = unsafeYarn.add.mock.calls; + + expect(yarnAddCalls.length).toEqual(1); + expect(yarnAddCalls[0][0]).toEqual(expectedPkg); + expect(yarnAddCalls[0][1]).toEqual(expectedDeps); +} + +describe('utils/addDependenciesToPackages', () => { + describe('addDependenciesToPackage()', () => { + let cwd; + let project; + let projectPkg; + let workspaces; + + beforeEach(async () => { + cwd = await getFixturePath(__dirname, 'nested-workspaces'); + project = await Project.init(cwd); + projectPkg = project.pkg; + workspaces = await project.getWorkspaces(); + }); + + describe('when called from the root of a project', () => { + it('should just run yarn add in the project dir', async () => { + const depsToAdd = [{ name: 'chalk' }]; + await addDependenciesToPackage(project, projectPkg, depsToAdd); + + assertSingleYarnAddCall(projectPkg, depsToAdd); + }); + }); + + describe('when called from a workspace', () => { + let pkgToRunIn; + + beforeEach(() => { + const workspaceToRunIn = + project.getWorkspaceByName(workspaces, 'foo') || {}; + pkgToRunIn = workspaceToRunIn.pkg; + }); + + describe('if all packages are already in the root', () => {}); + + describe('if some packages are not in the root', () => { + it('should only run yarn add for missing deps', async () => { + // our project already has left-pad installed + const depsToAdd = [{ name: 'chalk' }, { name: 'left-pad' }]; + + await addDependenciesToPackage(project, pkgToRunIn, depsToAdd); + // should only add chalk to root + assertSingleYarnAddCall(projectPkg, [{ name: 'chalk' }]); + }); + }); + + describe('if all packages are not in the root', () => { + it('should run yarn add in the root', async () => { + const depsToAdd = [{ name: 'chalk', name: 'does-not-exist' }]; + + await addDependenciesToPackage(project, pkgToRunIn, depsToAdd); + + assertSingleYarnAddCall(projectPkg, depsToAdd); + }); + }); + }); + }); +}); diff --git a/src/utils/__tests__/symlinkPackageDependencies.test.js b/src/utils/__tests__/symlinkPackageDependencies.test.js new file mode 100644 index 00000000..d67b6392 --- /dev/null +++ b/src/utils/__tests__/symlinkPackageDependencies.test.js @@ -0,0 +1,111 @@ +// @flow +import path from 'path'; +import { copyFixtureIntoTempDir } from 'jest-fixtures'; + +import symlinkPackageDependencies from '../symlinkPackageDependencies'; +import * as fs from '../fs'; +import * as yarn from '../yarn'; +import Project from '../../Project'; + +jest.mock('../yarn'); + +const unsafeFs: any & typeof fs = fs; +const unsafeYarn: any & typeof yarn = yarn; + +async function dirExists(dir: string) { + try { + const stat = await fs.stat(dir); + return stat.isDirectory(); + } catch (err) { + return false; + } +} + +async function symlinkExists(dir: string, symlink: string) { + try { + const stat = await fs.lstat(path.join(dir, symlink)); + return stat.isSymbolicLink(); + } catch (err) { + return false; + } +} + +describe('utils/symlinkPackageDependencies', () => { + describe('symlinkPackageDependencies() in valid project', () => { + let project; + let workspaces; + let mkdir; + let pkgToSymlink; + + beforeEach(async () => { + const tempDir = await copyFixtureIntoTempDir( + __dirname, + 'nested-workspaces-with-dependencies-installed' + ); + project = await Project.init(tempDir); + workspaces = await project.getWorkspaces(); + const wsToSymlink = project.getWorkspaceByName(workspaces, 'foo') || {}; + const pkgToSymlink = wsToSymlink.pkg; + }); + + it('should create node modules and node_modules/.bin if not existing', async () => { + expect(await dirExists(pkgToSymlink.nodeModules)).toEqual(false); + expect(await dirExists(pkgToSymlink.nodeModulesBin)).toEqual(false); + + await symlinkPackageDependencies(project, pkgToSymlink, ['semver']); + + expect(await dirExists(pkgToSymlink.nodeModules)).toEqual(true); + expect(await dirExists(pkgToSymlink.nodeModulesBin)).toEqual(true); + }); + + it('should symlink external dependencies (only ones passed in)', async () => { + const wsToSymlink = project.getWorkspaceByName(workspaces, 'foo') || {}; + const pkgToSymlink = wsToSymlink.pkg; + const { nodeModules } = pkgToSymlink; + + expect(await dirExists(path.join(nodeModules, 'semver'))).toEqual(false); + + await symlinkPackageDependencies(project, pkgToSymlink, ['semver']); + + expect(await dirExists(path.join(nodeModules, 'semver'))).toEqual(true); + }); + + it('should symlink external dependencies bin files', async () => { + const wsToSymlink = project.getWorkspaceByName(workspaces, 'foo') || {}; + const pkgToSymlink = wsToSymlink.pkg; + const { nodeModulesBin } = pkgToSymlink; + + expect(await symlinkExists(nodeModulesBin, 'semver')).toEqual(false); + + await symlinkPackageDependencies(project, pkgToSymlink, ['semver']); + + expect(await symlinkExists(nodeModulesBin, 'semver')).toEqual(true); + }); + + it('should symlink internal dependencies', async () => { + const wsToSymlink = project.getWorkspaceByName(workspaces, 'foo') || {}; + const pkgToSymlink = wsToSymlink.pkg; + const { nodeModules, nodeModulesBin } = pkgToSymlink; + + expect(await dirExists(path.join(nodeModules, 'bar'))).toEqual(false); + + await symlinkPackageDependencies(project, pkgToSymlink, ['bar']); + + expect(await dirExists(path.join(nodeModules, 'bar'))).toEqual(true); + expect(await symlinkExists(nodeModules, 'bar')).toEqual(true); + }); + + it('should run preinstall, postinstall and prepublish scripts', async () => { + const wsToSymlink = project.getWorkspaceByName(workspaces, 'foo') || {}; + const pkgToSymlink = wsToSymlink.pkg; + + await symlinkPackageDependencies(project, pkgToSymlink, ['bar']); + + expect(yarn.run).toHaveBeenCalledTimes(3); + const yarnCalls = unsafeYarn.run.mock.calls; + expect(yarnCalls[0][1]).toEqual('preinstall'); + expect(yarnCalls[1][1]).toEqual('postinstall'); + expect(yarnCalls[2][1]).toEqual('prepublish'); + }); + }); +}); diff --git a/src/utils/addDependenciesToPackages.js b/src/utils/addDependenciesToPackages.js new file mode 100644 index 00000000..39e4f0d2 --- /dev/null +++ b/src/utils/addDependenciesToPackages.js @@ -0,0 +1,31 @@ +// @flow + +import Project from '../Project'; +import type Workspace from '../Workspace'; +import type Package from '../Package'; +import type { Dependency, configDependencyType } from '../types'; +import * as logger from './logger'; +import * as yarn from './yarn'; + +export default async function addDependenciesToPackage( + project: Project, + pkg: Package, + dependencies: Array, + type?: configDependencyType +) { + const isProjectPackage = project.pkg.dir === pkg.dir; + if (isProjectPackage) { + await yarn.add(project.pkg, dependencies); + return true; + } + + const projectDependencies = project.pkg.getAllDependencies(); + const depsToInstallInProject = dependencies.filter( + dep => !projectDependencies.has(dep.name) + ); + + await yarn.add(project.pkg, depsToInstallInProject); + + const depsToSymlink = []; + dependencies.forEach(dep => {}); +} diff --git a/src/utils/symlinkPackageDependencies.js b/src/utils/symlinkPackageDependencies.js new file mode 100644 index 00000000..9eae6f89 --- /dev/null +++ b/src/utils/symlinkPackageDependencies.js @@ -0,0 +1,156 @@ +// @flow +import path from 'path'; +import pathIsInside from 'path-is-inside'; +import includes from 'array-includes'; + +import Project from '../Project'; +import type Workspace from '../Workspace'; +import type Package from '../Package'; +import { PError } from './errors'; +import * as fs from './fs'; +import * as logger from './logger'; +import * as messages from './messages'; +import * as yarn from './yarn'; + +export default async function symlinkPackageDependencies( + project: Project, + pkg: Package, + dependencies: Array +) { + const projectDeps = project.pkg.getAllDependencies(); + const pkgDependencies = project.pkg.getAllDependencies(); + const workspaces = await project.getWorkspaces(); + const { + graph: dependencyGraph, + valid: dependencyGraphValid + } = await project.getDependencyGraph(workspaces); + + const directoriesToCreate = []; + const symlinksToCreate = []; + + let valid = true; + + /********************************************************************* + * Calculate all the external dependencies that need to be symlinked * + **********************************************************************/ + + directoriesToCreate.push(pkg.nodeModules, pkg.nodeModulesBin); + + for (let [name, version] of pkgDependencies) { + const matched = projectDeps.get(name); + + // If dependency is internal we can ignore it + if (dependencyGraph.has(name)) { + continue; + } + + // If dependency is not in the Project deps, warn user (but don't throw yet) + if (!matched) { + valid = false; + logger.error(messages.depMustBeAddedToProject(pkg.config.name, name)); + continue; + } + + if (version !== matched) { + valid = false; + logger.error( + messages.depMustMatchProject(pkg.config.name, name, matched, version) + ); + continue; + } + + let src = path.join(project.pkg.nodeModules, name); + let dest = path.join(pkg.nodeModules, name); + + symlinksToCreate.push({ src, dest, type: 'junction' }); + } + + /********************************************************************* + * Calculate all the internal dependencies that need to be symlinked * + **********************************************************************/ + + for (let [name, node] of dependencyGraph) { + const nodeModules = path.join(node.pkg.dir, 'node_modules'); + + for (let dependency of node.dependencies) { + const depWorkspace = dependencyGraph.get(dependency); + + if (!depWorkspace) { + throw new PError(`Missing workspace: "${dependency}"`); + } + + let src = depWorkspace.pkg.dir; + let dest = path.join(nodeModules, dependency); + + symlinksToCreate.push({ src, dest, type: 'junction' }); + } + } + + if (!dependencyGraphValid || !valid) { + throw new PError('Cannot symlink invalid set of dependencies.'); + } + + /******************************************************** + * Calculate all the bin files that need to be symlinked * + *********************************************************/ + + for (let binFile of await fs.readdirSafe(project.pkg.nodeModulesBin)) { + const binPath = path.join(project.pkg.nodeModulesBin, binFile); + const binName = path.basename(binPath); + + const linkFile = await fs.readlink(binPath); + + if (!linkFile) { + throw new PError(`${binName} is not a symlink`); + } + + const linkPath = path.join(project.pkg.nodeModulesBin, linkFile); + + if (!pathIsInside(linkPath, project.pkg.nodeModules)) { + throw new PError( + `${binName} is linked to a location outside of project node_modules: ${linkPath}` + ); + } + + const relativeLinkPath = path.relative(project.pkg.nodeModules, linkPath); + const pathParts = relativeLinkPath.split(path.sep); + let pkgName = pathParts[0]; + + if (pkgName.startsWith('@')) { + pkgName += '/' + pathParts[1]; + } + + if (!includes(dependencies, pkgName)) { + continue; + } + + let workspaceBinPath = path.join(pkg.nodeModulesBin, binName); + + symlinksToCreate.push({ + src: binPath, + dest: workspaceBinPath, + type: 'exec' + }); + } + + /********************************** + * Create directories and symlinks * + ***********************************/ + + await yarn.run(pkg, 'preinstall'); + + await Promise.all( + directoriesToCreate.map(dirName => { + return fs.mkdirp(dirName); + }) + ); + + await Promise.all( + symlinksToCreate.map(async ({ src, dest, type }) => { + await fs.symlink(src, dest, type); + }) + ); + + await yarn.run(pkg, 'postinstall'); + await yarn.run(pkg, 'prepublish'); +} diff --git a/src/utils/yarn.js b/src/utils/yarn.js index f39bf467..5f6449ba 100644 --- a/src/utils/yarn.js +++ b/src/utils/yarn.js @@ -1,23 +1,15 @@ // @flow import includes from 'array-includes'; +import type { Dependency, configDependencyType } from '../types'; import type Package from '../Package'; import * as processes from './processes'; import * as fs from '../utils/fs'; -type Dependency = { - name: string, - version?: string -}; - export async function add( pkg: Package, dependencies: Array, - type?: - | 'dependencies' - | 'devDependencies' - | 'peerDependencies' - | 'optionalDependencies' + type?: configDependencyType ) { const spawnArgs = ['add']; if (!dependencies.length) return; diff --git a/yarn.lock b/yarn.lock index b1082b70..ead948dd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1328,6 +1328,14 @@ form-data@~2.1.1: combined-stream "^1.0.5" mime-types "^2.1.12" +fs-extra@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.2.tgz#f91704c53d1b461f893452b0c307d9997647ab6b" + dependencies: + graceful-fs "^4.1.2" + jsonfile "^4.0.0" + universalify "^0.1.0" + fs-readdir-recursive@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs-readdir-recursive/-/fs-readdir-recursive-1.0.0.tgz#8cd1745c8b4f8a29c8caec392476921ba195f560" @@ -1441,7 +1449,7 @@ globby@^6.1.0: pify "^2.0.0" pinkie-promise "^2.0.0" -graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.4: +graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.4, graceful-fs@^4.1.6: version "4.1.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" @@ -1886,11 +1894,12 @@ jest-environment-node@^20.0.3: jest-mock "^20.0.3" jest-util "^20.0.3" -jest-fixtures@^0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/jest-fixtures/-/jest-fixtures-0.3.2.tgz#204694429c1ac103fd35b6605c56e24c68d418de" +jest-fixtures@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/jest-fixtures/-/jest-fixtures-0.5.0.tgz#1c337cca8850ad64cb16e638e4bb57ee11bfff48" dependencies: find-up "^2.1.0" + fs-extra "^4.0.2" typeable-promisify "^2.0.1" jest-haste-map@^20.0.4: @@ -2087,6 +2096,12 @@ json5@^0.5.0: version "0.5.1" resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" +jsonfile@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + optionalDependencies: + graceful-fs "^4.1.6" + jsonify@~0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" @@ -3278,6 +3293,10 @@ union-value@^0.2.3: is-extendable "^0.1.1" set-value "^0.4.3" +universalify@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.1.tgz#fa71badd4437af4c148841e3b3b165f9e9e590b7" + user-home@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/user-home/-/user-home-1.1.1.tgz#2b5be23a32b63a7c9deb8d0f28d485724a3df190" From ec212ad32cd9ae421bdcc42dc76e32aab4bab9e5 Mon Sep 17 00:00:00 2001 From: Luke Batchelor Date: Thu, 21 Sep 2017 21:48:24 +1000 Subject: [PATCH 04/14] Better implementation of symlinkPackages --- .../package.json | 18 --- .../packages/foo/package.json | 9 -- .../packages/foo/packages/baz/package.json | 9 -- .../yarn.lock | 19 --- .../node_modules/.bin/external-dep-two-bins-1 | 1 + .../node_modules/.bin/external-dep-two-bins-2 | 1 + .../node_modules/.bin/external-dep-with-bin | 1 + .../external-dep-with-bin/external-bin.js | 0 .../external-dep-with-bin/package.json | 5 + .../external-bin-1.js | 0 .../external-bin-2.js | 0 .../external-dep-with-two-bins/package.json | 8 + .../node_modules/external-dep/package.json | 4 + .../package.json | 17 +++ .../packages/bar/bar.js | 0 .../packages/bar/package.json | 6 +- .../packages/foo/package.json | 10 ++ .../packages/foo/packages/baz/baz-bin-1.js | 0 .../packages/foo/packages/baz/baz-bin-2.js | 0 .../packages/foo/packages/baz/package.json | 11 ++ .../packages/foo/packages/zee/package.json | 13 ++ src/commands/__tests__/remove.test.js | 10 +- src/commands/project/__tests__/remove.test.js | 2 +- .../workspace/__tests__/remove.test.js | 6 +- .../workspaces/__tests__/remove.test.js | 8 +- src/types.js | 5 + .../symlinkPackageDependencies.test.js | 138 ++++++++++++------ src/utils/messages.js | 8 + src/utils/symlinkPackageDependencies.js | 131 ++++++++++++----- 29 files changed, 289 insertions(+), 151 deletions(-) delete mode 100644 src/__fixtures__/nested-workspaces-with-dependencies-installed/package.json delete mode 100644 src/__fixtures__/nested-workspaces-with-dependencies-installed/packages/foo/package.json delete mode 100644 src/__fixtures__/nested-workspaces-with-dependencies-installed/packages/foo/packages/baz/package.json delete mode 100644 src/__fixtures__/nested-workspaces-with-dependencies-installed/yarn.lock create mode 120000 src/__fixtures__/nested-workspaces-with-root-dependencies-installed/node_modules/.bin/external-dep-two-bins-1 create mode 120000 src/__fixtures__/nested-workspaces-with-root-dependencies-installed/node_modules/.bin/external-dep-two-bins-2 create mode 120000 src/__fixtures__/nested-workspaces-with-root-dependencies-installed/node_modules/.bin/external-dep-with-bin create mode 100644 src/__fixtures__/nested-workspaces-with-root-dependencies-installed/node_modules/external-dep-with-bin/external-bin.js create mode 100644 src/__fixtures__/nested-workspaces-with-root-dependencies-installed/node_modules/external-dep-with-bin/package.json create mode 100644 src/__fixtures__/nested-workspaces-with-root-dependencies-installed/node_modules/external-dep-with-two-bins/external-bin-1.js create mode 100644 src/__fixtures__/nested-workspaces-with-root-dependencies-installed/node_modules/external-dep-with-two-bins/external-bin-2.js create mode 100644 src/__fixtures__/nested-workspaces-with-root-dependencies-installed/node_modules/external-dep-with-two-bins/package.json create mode 100644 src/__fixtures__/nested-workspaces-with-root-dependencies-installed/node_modules/external-dep/package.json create mode 100644 src/__fixtures__/nested-workspaces-with-root-dependencies-installed/package.json create mode 100644 src/__fixtures__/nested-workspaces-with-root-dependencies-installed/packages/bar/bar.js rename src/__fixtures__/{nested-workspaces-with-dependencies-installed => nested-workspaces-with-root-dependencies-installed}/packages/bar/package.json (53%) create mode 100644 src/__fixtures__/nested-workspaces-with-root-dependencies-installed/packages/foo/package.json create mode 100644 src/__fixtures__/nested-workspaces-with-root-dependencies-installed/packages/foo/packages/baz/baz-bin-1.js create mode 100644 src/__fixtures__/nested-workspaces-with-root-dependencies-installed/packages/foo/packages/baz/baz-bin-2.js create mode 100644 src/__fixtures__/nested-workspaces-with-root-dependencies-installed/packages/foo/packages/baz/package.json create mode 100644 src/__fixtures__/nested-workspaces-with-root-dependencies-installed/packages/foo/packages/zee/package.json diff --git a/src/__fixtures__/nested-workspaces-with-dependencies-installed/package.json b/src/__fixtures__/nested-workspaces-with-dependencies-installed/package.json deleted file mode 100644 index ec4a46f4..00000000 --- a/src/__fixtures__/nested-workspaces-with-dependencies-installed/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "private": true, - "version": "1.0.0", - "name": "fixture-project-nested-workspaces-with-dependencies-installed", - "description": "A project with all the node_modules and symlinks already set up", - "description2": "We deliberately choose small packages that have binary files for testing symlinking", - "pworkspaces": [ - "packages/*" - ], - "dependencies": { - "left-pad": "^1.1.3", - "semver": "^5.4.1", - "uuid": "^3.1.0" - }, - "devDependencies": { - "right-pad": "^1.0.1" - } -} diff --git a/src/__fixtures__/nested-workspaces-with-dependencies-installed/packages/foo/package.json b/src/__fixtures__/nested-workspaces-with-dependencies-installed/packages/foo/package.json deleted file mode 100644 index 53547e40..00000000 --- a/src/__fixtures__/nested-workspaces-with-dependencies-installed/packages/foo/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "foo", - "version": "1.0.0", - "pworkspaces": ["packages/*"], - "dependencies": { - "bar": "^1.0.0", - "semver": "^5.4.1" - } -} diff --git a/src/__fixtures__/nested-workspaces-with-dependencies-installed/packages/foo/packages/baz/package.json b/src/__fixtures__/nested-workspaces-with-dependencies-installed/packages/foo/packages/baz/package.json deleted file mode 100644 index 038fa598..00000000 --- a/src/__fixtures__/nested-workspaces-with-dependencies-installed/packages/foo/packages/baz/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "baz", - "version": "1.0.0", - "dependencies": { - "bar": "^1.0.0", - "uuid": "^3.1.0", - "right-pad": "^1.0.1" - } -} diff --git a/src/__fixtures__/nested-workspaces-with-dependencies-installed/yarn.lock b/src/__fixtures__/nested-workspaces-with-dependencies-installed/yarn.lock deleted file mode 100644 index af2d1c59..00000000 --- a/src/__fixtures__/nested-workspaces-with-dependencies-installed/yarn.lock +++ /dev/null @@ -1,19 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -left-pad@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/left-pad/-/left-pad-1.1.3.tgz#612f61c033f3a9e08e939f1caebeea41b6f3199a" - -right-pad@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/right-pad/-/right-pad-1.0.1.tgz#8ca08c2cbb5b55e74dafa96bf7fd1a27d568c8d0" - -semver@^5.4.1: - version "5.4.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e" - -uuid@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" diff --git a/src/__fixtures__/nested-workspaces-with-root-dependencies-installed/node_modules/.bin/external-dep-two-bins-1 b/src/__fixtures__/nested-workspaces-with-root-dependencies-installed/node_modules/.bin/external-dep-two-bins-1 new file mode 120000 index 00000000..95605340 --- /dev/null +++ b/src/__fixtures__/nested-workspaces-with-root-dependencies-installed/node_modules/.bin/external-dep-two-bins-1 @@ -0,0 +1 @@ +../external-dep-with-two-bins/external-bin-1.js \ No newline at end of file diff --git a/src/__fixtures__/nested-workspaces-with-root-dependencies-installed/node_modules/.bin/external-dep-two-bins-2 b/src/__fixtures__/nested-workspaces-with-root-dependencies-installed/node_modules/.bin/external-dep-two-bins-2 new file mode 120000 index 00000000..84a50cd6 --- /dev/null +++ b/src/__fixtures__/nested-workspaces-with-root-dependencies-installed/node_modules/.bin/external-dep-two-bins-2 @@ -0,0 +1 @@ +../external-dep-with-two-bins/external-bin-2.js \ No newline at end of file diff --git a/src/__fixtures__/nested-workspaces-with-root-dependencies-installed/node_modules/.bin/external-dep-with-bin b/src/__fixtures__/nested-workspaces-with-root-dependencies-installed/node_modules/.bin/external-dep-with-bin new file mode 120000 index 00000000..92b520a9 --- /dev/null +++ b/src/__fixtures__/nested-workspaces-with-root-dependencies-installed/node_modules/.bin/external-dep-with-bin @@ -0,0 +1 @@ +../external-dep-with-bin/external-bin.js \ No newline at end of file diff --git a/src/__fixtures__/nested-workspaces-with-root-dependencies-installed/node_modules/external-dep-with-bin/external-bin.js b/src/__fixtures__/nested-workspaces-with-root-dependencies-installed/node_modules/external-dep-with-bin/external-bin.js new file mode 100644 index 00000000..e69de29b diff --git a/src/__fixtures__/nested-workspaces-with-root-dependencies-installed/node_modules/external-dep-with-bin/package.json b/src/__fixtures__/nested-workspaces-with-root-dependencies-installed/node_modules/external-dep-with-bin/package.json new file mode 100644 index 00000000..d9e0e5bf --- /dev/null +++ b/src/__fixtures__/nested-workspaces-with-root-dependencies-installed/node_modules/external-dep-with-bin/package.json @@ -0,0 +1,5 @@ +{ + "name": "external-dep-with-bin", + "version": "1.0.0", + "bin": "./external-bin.js" +} diff --git a/src/__fixtures__/nested-workspaces-with-root-dependencies-installed/node_modules/external-dep-with-two-bins/external-bin-1.js b/src/__fixtures__/nested-workspaces-with-root-dependencies-installed/node_modules/external-dep-with-two-bins/external-bin-1.js new file mode 100644 index 00000000..e69de29b diff --git a/src/__fixtures__/nested-workspaces-with-root-dependencies-installed/node_modules/external-dep-with-two-bins/external-bin-2.js b/src/__fixtures__/nested-workspaces-with-root-dependencies-installed/node_modules/external-dep-with-two-bins/external-bin-2.js new file mode 100644 index 00000000..e69de29b diff --git a/src/__fixtures__/nested-workspaces-with-root-dependencies-installed/node_modules/external-dep-with-two-bins/package.json b/src/__fixtures__/nested-workspaces-with-root-dependencies-installed/node_modules/external-dep-with-two-bins/package.json new file mode 100644 index 00000000..d2e89bfa --- /dev/null +++ b/src/__fixtures__/nested-workspaces-with-root-dependencies-installed/node_modules/external-dep-with-two-bins/package.json @@ -0,0 +1,8 @@ +{ + "name": "external-dep-with-two-bins", + "version": "1.0.0", + "bin": { + "external-dep-two-bins-1": "./external-bin-1.js", + "external-dep-two-bins-2": "./external-bin-2.js" + } +} diff --git a/src/__fixtures__/nested-workspaces-with-root-dependencies-installed/node_modules/external-dep/package.json b/src/__fixtures__/nested-workspaces-with-root-dependencies-installed/node_modules/external-dep/package.json new file mode 100644 index 00000000..cbfaeb03 --- /dev/null +++ b/src/__fixtures__/nested-workspaces-with-root-dependencies-installed/node_modules/external-dep/package.json @@ -0,0 +1,4 @@ +{ + "name": "external-dep", + "version": "1.0.0" +} diff --git a/src/__fixtures__/nested-workspaces-with-root-dependencies-installed/package.json b/src/__fixtures__/nested-workspaces-with-root-dependencies-installed/package.json new file mode 100644 index 00000000..ccc85e09 --- /dev/null +++ b/src/__fixtures__/nested-workspaces-with-root-dependencies-installed/package.json @@ -0,0 +1,17 @@ +{ + "private": true, + "version": "1.0.0", + "name": "fixture-project-nested-workspaces-with-root-dependencies-installed", + "description": "A project with only the root node_modules and symlinks already set up", + "description2": "Packages: foo, bar, baz, zee", + "description3": "bar has a single bin file, baz has two", + "description4": "foo and baz depend on bar, zee depends on baz", + "pworkspaces": [ + "packages/*" + ], + "dependencies": { + "external-dep": "^1.0.0", + "external-dep-with-bin": "^1.0.0", + "external-dep-with-two-bins": "^1.0.0" + } +} diff --git a/src/__fixtures__/nested-workspaces-with-root-dependencies-installed/packages/bar/bar.js b/src/__fixtures__/nested-workspaces-with-root-dependencies-installed/packages/bar/bar.js new file mode 100644 index 00000000..e69de29b diff --git a/src/__fixtures__/nested-workspaces-with-dependencies-installed/packages/bar/package.json b/src/__fixtures__/nested-workspaces-with-root-dependencies-installed/packages/bar/package.json similarity index 53% rename from src/__fixtures__/nested-workspaces-with-dependencies-installed/packages/bar/package.json rename to src/__fixtures__/nested-workspaces-with-root-dependencies-installed/packages/bar/package.json index d012d905..965aef0c 100644 --- a/src/__fixtures__/nested-workspaces-with-dependencies-installed/packages/bar/package.json +++ b/src/__fixtures__/nested-workspaces-with-root-dependencies-installed/packages/bar/package.json @@ -2,7 +2,7 @@ "name": "bar", "version": "1.0.0", "dependencies": { - "left-pad": "^1.1.3", - "uuid": "^3.1.0" - } + "external-dep": "^1.0.0" + }, + "bin": "./bar.js" } diff --git a/src/__fixtures__/nested-workspaces-with-root-dependencies-installed/packages/foo/package.json b/src/__fixtures__/nested-workspaces-with-root-dependencies-installed/packages/foo/package.json new file mode 100644 index 00000000..f64c0542 --- /dev/null +++ b/src/__fixtures__/nested-workspaces-with-root-dependencies-installed/packages/foo/package.json @@ -0,0 +1,10 @@ +{ + "name": "foo", + "version": "1.0.0", + "dependencies": { + "bar": "^1.0.0", + "external-dep": "^1.0.0", + "external-dep-wth-bin": "^1.0.0" + }, + "pworkspaces": ["packages/*"] +} diff --git a/src/__fixtures__/nested-workspaces-with-root-dependencies-installed/packages/foo/packages/baz/baz-bin-1.js b/src/__fixtures__/nested-workspaces-with-root-dependencies-installed/packages/foo/packages/baz/baz-bin-1.js new file mode 100644 index 00000000..e69de29b diff --git a/src/__fixtures__/nested-workspaces-with-root-dependencies-installed/packages/foo/packages/baz/baz-bin-2.js b/src/__fixtures__/nested-workspaces-with-root-dependencies-installed/packages/foo/packages/baz/baz-bin-2.js new file mode 100644 index 00000000..e69de29b diff --git a/src/__fixtures__/nested-workspaces-with-root-dependencies-installed/packages/foo/packages/baz/package.json b/src/__fixtures__/nested-workspaces-with-root-dependencies-installed/packages/foo/packages/baz/package.json new file mode 100644 index 00000000..d1339e9b --- /dev/null +++ b/src/__fixtures__/nested-workspaces-with-root-dependencies-installed/packages/foo/packages/baz/package.json @@ -0,0 +1,11 @@ +{ + "name": "baz", + "version": "1.0.0", + "dependencies": { + "bar": "^1.0.0" + }, + "bin": { + "baz-1": "./baz-bin-1.js", + "baz-2": "./baz-bin-2.js" + } +} diff --git a/src/__fixtures__/nested-workspaces-with-root-dependencies-installed/packages/foo/packages/zee/package.json b/src/__fixtures__/nested-workspaces-with-root-dependencies-installed/packages/foo/packages/zee/package.json new file mode 100644 index 00000000..e1cd1c60 --- /dev/null +++ b/src/__fixtures__/nested-workspaces-with-root-dependencies-installed/packages/foo/packages/zee/package.json @@ -0,0 +1,13 @@ +{ + "name": "zee", + "version": "1.0.0", + "description": "We use the zee package to test all bin symlinking", + "description2": "bar and external-dep-with-bin both have one bin", + "description3": "baz and external-dep... both have two", + "dependencies": { + "bar": "^1.0.0", + "baz": "^1.0.0", + "external-dep-with-bin": "^1.0.0", + "external-dep-with-two-bins": "^1.0.0" + } +} diff --git a/src/commands/__tests__/remove.test.js b/src/commands/__tests__/remove.test.js index 0aa6df20..7590c5db 100644 --- a/src/commands/__tests__/remove.test.js +++ b/src/commands/__tests__/remove.test.js @@ -11,7 +11,7 @@ jest.mock('../../utils/yarn'); describe('pyarn remove', () => { test('removing a project dependency only used by the project', async () => { - let { tempDir } = await copyFixtureIntoTempDir( + let tempDir = await copyFixtureIntoTempDir( __dirname, 'package-with-external-deps-installed' ); @@ -23,7 +23,7 @@ describe('pyarn remove', () => { }); test('removing a workspace dependency', async () => { - let { tempDir } = await copyFixtureIntoTempDir( + let tempDir = await copyFixtureIntoTempDir( __dirname, 'package-with-external-deps-installed' ); @@ -45,7 +45,7 @@ describe('pyarn remove', () => { }); test('removing a dependency that does not exist', async () => { - let { tempDir } = await copyFixtureIntoTempDir( + let tempDir = await copyFixtureIntoTempDir( __dirname, 'package-with-external-deps-installed' ); @@ -55,8 +55,8 @@ describe('pyarn remove', () => { ).rejects.toBeInstanceOf(Error); }); - test('removing a dependency that is used by a workspace', async () => { - let { tempDir } = await copyFixtureIntoTempDir( + test.only('removing a dependency that is used by a workspace', async () => { + let tempDir = await copyFixtureIntoTempDir( __dirname, 'package-with-external-deps-installed' ); diff --git a/src/commands/project/__tests__/remove.test.js b/src/commands/project/__tests__/remove.test.js index 07a6cf40..e658f521 100644 --- a/src/commands/project/__tests__/remove.test.js +++ b/src/commands/project/__tests__/remove.test.js @@ -10,7 +10,7 @@ jest.mock('../../../utils/yarn'); describe('pyarn project remove', () => { test('removing a project dependency only used by the project', async () => { - let { tempDir } = await copyFixtureIntoTempDir( + let tempDir = await copyFixtureIntoTempDir( __dirname, 'package-with-external-deps-installed' ); diff --git a/src/commands/workspace/__tests__/remove.test.js b/src/commands/workspace/__tests__/remove.test.js index 4dc05444..1ce4a57b 100644 --- a/src/commands/workspace/__tests__/remove.test.js +++ b/src/commands/workspace/__tests__/remove.test.js @@ -11,7 +11,7 @@ jest.mock('../../../utils/yarn'); describe('pyarn workspace remove', () => { test('removing a workspace dependency that exists', async () => { - let { tempDir } = await copyFixtureIntoTempDir( + let tempDir = await copyFixtureIntoTempDir( __dirname, 'package-with-external-deps-installed' ); @@ -35,7 +35,7 @@ describe('pyarn workspace remove', () => { }); test('removing a workspace dependency that doesnt exist in that package', async () => { - let { tempDir } = await copyFixtureIntoTempDir( + let tempDir = await copyFixtureIntoTempDir( __dirname, 'package-with-external-deps-installed' ); @@ -48,7 +48,7 @@ describe('pyarn workspace remove', () => { }); test('removing a workspace dependency from inside another directory', async () => { - let { tempDir } = await copyFixtureIntoTempDir( + let tempDir = await copyFixtureIntoTempDir( __dirname, 'package-with-external-deps-installed' ); diff --git a/src/commands/workspaces/__tests__/remove.test.js b/src/commands/workspaces/__tests__/remove.test.js index 2b03d2c0..d674717f 100644 --- a/src/commands/workspaces/__tests__/remove.test.js +++ b/src/commands/workspaces/__tests__/remove.test.js @@ -11,7 +11,7 @@ jest.mock('../../../utils/yarn'); describe('pyarn workspaces remove', () => { test('removing a dependency from all workspaces', async () => { - let { tempDir } = await copyFixtureIntoTempDir( + let tempDir = await copyFixtureIntoTempDir( __dirname, 'package-with-external-deps-installed' ); @@ -42,7 +42,7 @@ describe('pyarn workspaces remove', () => { }); test('removing a dependency that only exists in some of the workspaces', async () => { - let { tempDir } = await copyFixtureIntoTempDir( + let tempDir = await copyFixtureIntoTempDir( __dirname, 'package-with-external-deps-installed' ); @@ -66,7 +66,7 @@ describe('pyarn workspaces remove', () => { }); test('removing a dependency that doesnt exist in any of the workspaces', async () => { - let { tempDir } = await copyFixtureIntoTempDir( + let tempDir = await copyFixtureIntoTempDir( __dirname, 'package-with-external-deps-installed' ); @@ -79,7 +79,7 @@ describe('pyarn workspaces remove', () => { }); test('removing a dependency that doesnt exist in any of the filtered workspaces', async () => { - let { tempDir } = await copyFixtureIntoTempDir( + let tempDir = await copyFixtureIntoTempDir( __dirname, 'package-with-external-deps-installed' ); diff --git a/src/types.js b/src/types.js index 4915d463..ff4dcca1 100644 --- a/src/types.js +++ b/src/types.js @@ -12,6 +12,11 @@ export type Config = { name: string, version: string, private?: boolean, + bin?: + | string + | { + [key: string]: string + }, dependencies?: DependencySet, devDependencies?: DependencySet, peerDependencies?: DependencySet, diff --git a/src/utils/__tests__/symlinkPackageDependencies.test.js b/src/utils/__tests__/symlinkPackageDependencies.test.js index d67b6392..e66da250 100644 --- a/src/utils/__tests__/symlinkPackageDependencies.test.js +++ b/src/utils/__tests__/symlinkPackageDependencies.test.js @@ -30,63 +30,60 @@ async function symlinkExists(dir: string, symlink: string) { } } -describe('utils/symlinkPackageDependencies', () => { - describe('symlinkPackageDependencies() in valid project', () => { - let project; - let workspaces; - let mkdir; - let pkgToSymlink; - - beforeEach(async () => { - const tempDir = await copyFixtureIntoTempDir( - __dirname, - 'nested-workspaces-with-dependencies-installed' - ); - project = await Project.init(tempDir); - workspaces = await project.getWorkspaces(); - const wsToSymlink = project.getWorkspaceByName(workspaces, 'foo') || {}; - const pkgToSymlink = wsToSymlink.pkg; +describe('utils/symlinkPackageDependencies()', () => { + let project; + let workspaces; + let mkdir; + let pkgToSymlink; + let nodeModules; + let nodeModulesBin; + + beforeEach(async () => { + const tempDir = await copyFixtureIntoTempDir( + __dirname, + 'nested-workspaces-with-root-dependencies-installed' + ); + project = await Project.init(tempDir); + workspaces = await project.getWorkspaces(); + }); + + /******************** + * Linking packages * + ********************/ + + describe('linking packages', () => { + beforeEach(() => { + // We use the foo package as it has internal and external dependencies + const workspaceToSymlink = + project.getWorkspaceByName(workspaces, 'foo') || {}; + pkgToSymlink = workspaceToSymlink.pkg; + nodeModules = pkgToSymlink.nodeModules; + nodeModulesBin = pkgToSymlink.nodeModulesBin; }); it('should create node modules and node_modules/.bin if not existing', async () => { expect(await dirExists(pkgToSymlink.nodeModules)).toEqual(false); expect(await dirExists(pkgToSymlink.nodeModulesBin)).toEqual(false); - await symlinkPackageDependencies(project, pkgToSymlink, ['semver']); + await symlinkPackageDependencies(project, pkgToSymlink, ['bar']); expect(await dirExists(pkgToSymlink.nodeModules)).toEqual(true); expect(await dirExists(pkgToSymlink.nodeModulesBin)).toEqual(true); }); it('should symlink external dependencies (only ones passed in)', async () => { - const wsToSymlink = project.getWorkspaceByName(workspaces, 'foo') || {}; - const pkgToSymlink = wsToSymlink.pkg; - const { nodeModules } = pkgToSymlink; - - expect(await dirExists(path.join(nodeModules, 'semver'))).toEqual(false); - - await symlinkPackageDependencies(project, pkgToSymlink, ['semver']); - - expect(await dirExists(path.join(nodeModules, 'semver'))).toEqual(true); - }); - - it('should symlink external dependencies bin files', async () => { - const wsToSymlink = project.getWorkspaceByName(workspaces, 'foo') || {}; - const pkgToSymlink = wsToSymlink.pkg; - const { nodeModulesBin } = pkgToSymlink; - - expect(await symlinkExists(nodeModulesBin, 'semver')).toEqual(false); + expect(await dirExists(path.join(nodeModules, 'external-dep'))).toEqual( + false + ); - await symlinkPackageDependencies(project, pkgToSymlink, ['semver']); + await symlinkPackageDependencies(project, pkgToSymlink, ['external-dep']); - expect(await symlinkExists(nodeModulesBin, 'semver')).toEqual(true); + expect(await dirExists(path.join(nodeModules, 'external-dep'))).toEqual( + true + ); }); it('should symlink internal dependencies', async () => { - const wsToSymlink = project.getWorkspaceByName(workspaces, 'foo') || {}; - const pkgToSymlink = wsToSymlink.pkg; - const { nodeModules, nodeModulesBin } = pkgToSymlink; - expect(await dirExists(path.join(nodeModules, 'bar'))).toEqual(false); await symlinkPackageDependencies(project, pkgToSymlink, ['bar']); @@ -96,9 +93,6 @@ describe('utils/symlinkPackageDependencies', () => { }); it('should run preinstall, postinstall and prepublish scripts', async () => { - const wsToSymlink = project.getWorkspaceByName(workspaces, 'foo') || {}; - const pkgToSymlink = wsToSymlink.pkg; - await symlinkPackageDependencies(project, pkgToSymlink, ['bar']); expect(yarn.run).toHaveBeenCalledTimes(3); @@ -108,4 +102,62 @@ describe('utils/symlinkPackageDependencies', () => { expect(yarnCalls[2][1]).toEqual('prepublish'); }); }); + + describe('linking binaries', () => { + beforeEach(() => { + // We use the zee package as it has internal and external dependencies that have various + // kinds of bin set ups + const workspaceToSymlink = + project.getWorkspaceByName(workspaces, 'zee') || {}; + pkgToSymlink = workspaceToSymlink.pkg; + nodeModules = pkgToSymlink.nodeModules; + nodeModulesBin = pkgToSymlink.nodeModulesBin; + }); + + it('should symlink external dependencies bin files (when declared using strings)', async () => { + expect( + await symlinkExists(nodeModulesBin, 'external-dep-with-bins') + ).toEqual(false); + + await symlinkPackageDependencies(project, pkgToSymlink, [ + 'external-dep-with-bin' + ]); + + expect( + await symlinkExists(nodeModulesBin, 'external-dep-with-bin') + ).toEqual(true); + }); + + it('should symlink external dependencies bin files (when declared using object)', async () => { + expect( + await symlinkExists(nodeModulesBin, 'external-dep-two-bins-1') + ).toEqual(false); + + await symlinkPackageDependencies(project, pkgToSymlink, [ + 'external-dep-with-two-bins' + ]); + + expect( + await symlinkExists(nodeModulesBin, 'external-dep-two-bins-1') + ).toEqual(true); + }); + }); + + it('should symlink internal dependencies bin files (when declared using string)', async () => { + expect(await symlinkExists(nodeModulesBin, 'bar')).toEqual(false); + + await symlinkPackageDependencies(project, pkgToSymlink, ['bar']); + + expect(await symlinkExists(nodeModulesBin, 'bar')).toEqual(true); + }); + + it('should symlink internal dependencies bin files (when declared using object)', async () => { + expect(await symlinkExists(nodeModulesBin, 'baz-1')).toEqual(false); + expect(await symlinkExists(nodeModulesBin, 'baz-2')).toEqual(false); + + await symlinkPackageDependencies(project, pkgToSymlink, ['baz']); + + expect(await symlinkExists(nodeModulesBin, 'baz-1')).toEqual(true); + expect(await symlinkExists(nodeModulesBin, 'baz-2')).toEqual(true); + }); }); diff --git a/src/utils/messages.js b/src/utils/messages.js index 723ee909..618fb91e 100644 --- a/src/utils/messages.js +++ b/src/utils/messages.js @@ -1,6 +1,7 @@ // @flow import chalk from 'chalk'; import type Workspace from '../Workspace'; +import type Package from '../Package'; function normalPkg(str) { return chalk.cyan(`"${str}"`); @@ -122,3 +123,10 @@ export function couldntRemoveDependencies(deps: Array) { .map(depName => ` - ${normalPkg(depName)}`) .join('\n')}`; } + +export function couldntSymlinkDependencyNotExists( + pkgName: string, + depName: string +) { + return `Could not symlink ${depName} in ${pkgName} as no dependency exists`; +} diff --git a/src/utils/symlinkPackageDependencies.js b/src/utils/symlinkPackageDependencies.js index 9eae6f89..85c65edb 100644 --- a/src/utils/symlinkPackageDependencies.js +++ b/src/utils/symlinkPackageDependencies.js @@ -24,6 +24,8 @@ export default async function symlinkPackageDependencies( graph: dependencyGraph, valid: dependencyGraphValid } = await project.getDependencyGraph(workspaces); + const internalDeps = + (dependencyGraph.get(pkg.config.name) || {}).dependencies || []; const directoriesToCreate = []; const symlinksToCreate = []; @@ -36,31 +38,44 @@ export default async function symlinkPackageDependencies( directoriesToCreate.push(pkg.nodeModules, pkg.nodeModulesBin); - for (let [name, version] of pkgDependencies) { - const matched = projectDeps.get(name); + for (let depName of dependencies) { + const versionInProject = projectDeps.get(depName); + const versionInPkg = pkgDependencies.get(depName); - // If dependency is internal we can ignore it - if (dependencyGraph.has(name)) { + // If dependency is internal we can ignore it (we symlink below) + if (dependencyGraph.has(depName)) { continue; } - // If dependency is not in the Project deps, warn user (but don't throw yet) - if (!matched) { + if (!versionInProject) { valid = false; - logger.error(messages.depMustBeAddedToProject(pkg.config.name, name)); + logger.error(messages.depMustBeAddedToProject(pkg.config.name, depName)); continue; } - if (version !== matched) { + if (!versionInPkg) { valid = false; logger.error( - messages.depMustMatchProject(pkg.config.name, name, matched, version) + messages.couldntSymlinkDependencyNotExists(pkg.config.name, depName) ); continue; } - let src = path.join(project.pkg.nodeModules, name); - let dest = path.join(pkg.nodeModules, name); + if (versionInProject !== versionInPkg) { + valid = false; + logger.error( + messages.depMustMatchProject( + pkg.config.name, + depName, + versionInProject, + versionInPkg + ) + ); + continue; + } + + let src = path.join(project.pkg.nodeModules, depName); + let dest = path.join(pkg.nodeModules, depName); symlinksToCreate.push({ src, dest, type: 'junction' }); } @@ -69,21 +84,12 @@ export default async function symlinkPackageDependencies( * Calculate all the internal dependencies that need to be symlinked * **********************************************************************/ - for (let [name, node] of dependencyGraph) { - const nodeModules = path.join(node.pkg.dir, 'node_modules'); - - for (let dependency of node.dependencies) { - const depWorkspace = dependencyGraph.get(dependency); - - if (!depWorkspace) { - throw new PError(`Missing workspace: "${dependency}"`); - } + for (let dependency of internalDeps) { + const depWorkspace = dependencyGraph.get(dependency) || {}; + const src = depWorkspace.pkg.dir; + const dest = path.join(pkg.nodeModules, dependency); - let src = depWorkspace.pkg.dir; - let dest = path.join(nodeModules, dependency); - - symlinksToCreate.push({ src, dest, type: 'junction' }); - } + symlinksToCreate.push({ src, dest, type: 'junction' }); } if (!dependencyGraphValid || !valid) { @@ -93,37 +99,48 @@ export default async function symlinkPackageDependencies( /******************************************************** * Calculate all the bin files that need to be symlinked * *********************************************************/ + const projectBinFiles = await fs.readdirSafe(project.pkg.nodeModulesBin); + + // TODO: For now, we'll search through each of the bin files in the Project and find which ones are + // dependencies we are symlinking. In the future, we should really be going through each dependency + // and all of its dependencies and checking which ones expose bins so that all the transitive ones + // are included too - for (let binFile of await fs.readdirSafe(project.pkg.nodeModulesBin)) { + for (let binFile of projectBinFiles) { const binPath = path.join(project.pkg.nodeModulesBin, binFile); const binName = path.basename(binPath); - const linkFile = await fs.readlink(binPath); + // read the symlink to find the actual bin file (path will be relative to the symlink) + const actualBinFileRelative = await fs.readlink(binPath); - if (!linkFile) { + if (!actualBinFileRelative) { throw new PError(`${binName} is not a symlink`); } - const linkPath = path.join(project.pkg.nodeModulesBin, linkFile); + const actualBinFile = path.join( + project.pkg.nodeModulesBin, + actualBinFileRelative + ); - if (!pathIsInside(linkPath, project.pkg.nodeModules)) { + if (!pathIsInside(actualBinFile, project.pkg.nodeModules)) { throw new PError( - `${binName} is linked to a location outside of project node_modules: ${linkPath}` + `${binName} is linked to a location outside of project node_modules: ${actualBinFileRelative}` ); } - const relativeLinkPath = path.relative(project.pkg.nodeModules, linkPath); - const pathParts = relativeLinkPath.split(path.sep); + // To find the name of the dep that created the bin we'll get its path from node_modules and + // use the first one or two parts (two if the package is scoped) + const binFileRelativeToNodeModules = path.relative( + project.pkg.nodeModules, + actualBinFile + ); + const pathParts = binFileRelativeToNodeModules.split(path.sep); let pkgName = pathParts[0]; if (pkgName.startsWith('@')) { pkgName += '/' + pathParts[1]; } - if (!includes(dependencies, pkgName)) { - continue; - } - let workspaceBinPath = path.join(pkg.nodeModulesBin, binName); symlinksToCreate.push({ @@ -133,6 +150,46 @@ export default async function symlinkPackageDependencies( }); } + /***************************************************************** + * Calculate all the internal bin files that need to be symlinked * + ******************************************************************/ + + // TODO: Same as above, we should really be making sure we get all the transitive bins as well + + for (let dependency of internalDeps) { + const depWorkspace = dependencyGraph.get(dependency) || {}; + const depBinFiles = + depWorkspace.pkg && + depWorkspace.pkg.config && + depWorkspace.pkg.config.bin; + + if (!depBinFiles) { + continue; + } + + if (!includes(dependencies, dependency)) { + // dependency is not one we are supposed to symlink right now + continue; + } + + if (typeof depBinFiles === 'string') { + // package may be scoped, name will only be the second part + const binName = dependency.split('/').pop(); + const src = path.join(depWorkspace.pkg.dir, depBinFiles); + const dest = path.join(pkg.nodeModulesBin, binName); + + symlinksToCreate.push({ src, dest, type: 'exec' }); + continue; + } + + for (let [binName, binPath] of Object.entries(depBinFiles)) { + const src = path.join(depWorkspace.pkg.dir, String(binPath)); + const dest = path.join(pkg.nodeModulesBin, binName); + + symlinksToCreate.push({ src, dest, type: 'exec' }); + } + } + /********************************** * Create directories and symlinks * ***********************************/ From 0c2873cf95449fff977312c9d7e4bcb83bf012cc Mon Sep 17 00:00:00 2001 From: Luke Batchelor Date: Thu, 21 Sep 2017 21:51:57 +1000 Subject: [PATCH 05/14] Removes my testing dir --- example-2/package.json | 14 -- example-2/packages/bar/README.md | 3 - example-2/packages/bar/package.json | 13 -- example-2/packages/foo/README.md | 3 - example-2/packages/foo/package.json | 19 -- example-2/packages/foo/packages/baz/README.md | 3 - .../packages/foo/packages/baz/package.json | 14 -- example-2/packages/foo/yarn.lock | 200 ------------------ example-2/yarn.lock | 146 ------------- 9 files changed, 415 deletions(-) delete mode 100644 example-2/package.json delete mode 100644 example-2/packages/bar/README.md delete mode 100644 example-2/packages/bar/package.json delete mode 100644 example-2/packages/foo/README.md delete mode 100644 example-2/packages/foo/package.json delete mode 100644 example-2/packages/foo/packages/baz/README.md delete mode 100644 example-2/packages/foo/packages/baz/package.json delete mode 100644 example-2/packages/foo/yarn.lock delete mode 100644 example-2/yarn.lock diff --git a/example-2/package.json b/example-2/package.json deleted file mode 100644 index d2a5e05d..00000000 --- a/example-2/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "private": true, - "name": "fixture-project", - "scripts": { - "clean": "rm -rf packages/**/node_modules && rm -rf node_modules" - }, - "pworkspaces": [ - "packages/*" - ], - "dependencies": { - "chalk": "^2.1.0", - "react": "^15.6.1" - } -} diff --git a/example-2/packages/bar/README.md b/example-2/packages/bar/README.md deleted file mode 100644 index e2dce07a..00000000 --- a/example-2/packages/bar/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# @lbatchelor/bar - -A placeholder package used for testing \ No newline at end of file diff --git a/example-2/packages/bar/package.json b/example-2/packages/bar/package.json deleted file mode 100644 index 403007d7..00000000 --- a/example-2/packages/bar/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "@lbatchelor/bar", - "version": "1.0.0", - "license": "MIT", - "scripts": { - "preinstall": "echo preinstall", - "postinstall": "echo postinstall", - "prepublish": "echo prepublish" - }, - "dependencies": { - "react": "^15.6.1" - } -} diff --git a/example-2/packages/foo/README.md b/example-2/packages/foo/README.md deleted file mode 100644 index 5ea56f98..00000000 --- a/example-2/packages/foo/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# @lbatchelor/foo - -A placeholder package used for testing \ No newline at end of file diff --git a/example-2/packages/foo/package.json b/example-2/packages/foo/package.json deleted file mode 100644 index 5d98e40f..00000000 --- a/example-2/packages/foo/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "@lbatchelor/foo", - "version": "1.0.11", - "license": "MIT", - "scripts": { - "preinstall": "echo preinstall from foo", - "postinstall": "echo postinstall from foo", - "prepublish": "echo prepublish from foo" - }, - "pworkspaces": [ - "packages/*" - ], - "dependencies": { - "@lbatchelor/bar": "^1.0.0", - "chalk": "^2.1.0", - "cowsay": "^1.2.1", - "react": "^15.6.1" - } -} diff --git a/example-2/packages/foo/packages/baz/README.md b/example-2/packages/foo/packages/baz/README.md deleted file mode 100644 index 0cfc3d79..00000000 --- a/example-2/packages/foo/packages/baz/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# @lbatchelor/baz - -A placeholder package used for testing \ No newline at end of file diff --git a/example-2/packages/foo/packages/baz/package.json b/example-2/packages/foo/packages/baz/package.json deleted file mode 100644 index 24b9b422..00000000 --- a/example-2/packages/foo/packages/baz/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "@lbatchelor/baz", - "version": "1.0.0", - "license": "MIT", - "scripts": { - "preinstall": "echo preinstall", - "postinstall": "echo postinstall", - "prepublish": "echo prepublish" - }, - "dependencies": { - "react": "^15.6.1", - "@lbatchelor/bar": "^1.0.0" - } -} diff --git a/example-2/packages/foo/yarn.lock b/example-2/packages/foo/yarn.lock deleted file mode 100644 index a4643602..00000000 --- a/example-2/packages/foo/yarn.lock +++ /dev/null @@ -1,200 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@lbatchelor/bar@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@lbatchelor/bar/-/bar-1.0.0.tgz#941ff5942f1386d03707752fc80d71d37efdb972" - dependencies: - react "^15.6.1" - -ansi-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" - -ansi-styles@^3.1.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.0.tgz#c159b8d5be0f9e5a6f346dab94f16ce022161b88" - dependencies: - color-convert "^1.9.0" - -asap@~2.0.3: - version "2.0.6" - resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" - -chalk@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.1.0.tgz#ac5becf14fa21b99c6c92ca7a7d7cfd5b17e743e" - dependencies: - ansi-styles "^3.1.0" - escape-string-regexp "^1.0.5" - supports-color "^4.0.0" - -color-convert@^1.9.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.0.tgz#1accf97dd739b983bf994d56fec8f95853641b7a" - dependencies: - color-name "^1.1.1" - -color-name@^1.1.1: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - -core-js@^1.0.0: - version "1.2.7" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" - -cowsay@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/cowsay/-/cowsay-1.2.1.tgz#3bde9f17ba64049bd359ff57b8916ec81d0332fe" - dependencies: - get-stdin "^5.0.1" - optimist "~0.6.1" - string-width "~2.1.1" - -create-react-class@^15.6.0: - version "15.6.0" - resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.6.0.tgz#ab448497c26566e1e29413e883207d57cfe7bed4" - dependencies: - fbjs "^0.8.9" - loose-envify "^1.3.1" - object-assign "^4.1.1" - -encoding@^0.1.11: - version "0.1.12" - resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb" - dependencies: - iconv-lite "~0.4.13" - -escape-string-regexp@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - -fbjs@^0.8.9: - version "0.8.15" - resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.15.tgz#4f0695fdfcc16c37c0b07facec8cb4c4091685b9" - dependencies: - core-js "^1.0.0" - isomorphic-fetch "^2.1.1" - loose-envify "^1.0.0" - object-assign "^4.1.0" - promise "^7.1.1" - setimmediate "^1.0.5" - ua-parser-js "^0.7.9" - -get-stdin@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-5.0.1.tgz#122e161591e21ff4c52530305693f20e6393a398" - -has-flag@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" - -iconv-lite@~0.4.13: - version "0.4.19" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" - -is-fullwidth-code-point@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" - -is-stream@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" - -isomorphic-fetch@^2.1.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9" - dependencies: - node-fetch "^1.0.1" - whatwg-fetch ">=0.10.0" - -js-tokens@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" - -loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848" - dependencies: - js-tokens "^3.0.0" - -minimist@~0.0.1: - version "0.0.10" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" - -node-fetch@^1.0.1: - version "1.7.3" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" - dependencies: - encoding "^0.1.11" - is-stream "^1.0.1" - -object-assign@^4.1.0, object-assign@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - -optimist@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" - dependencies: - minimist "~0.0.1" - wordwrap "~0.0.2" - -promise@^7.1.1: - version "7.3.1" - resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" - dependencies: - asap "~2.0.3" - -prop-types@^15.5.10: - version "15.5.10" - resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.10.tgz#2797dfc3126182e3a95e3dfbb2e893ddd7456154" - dependencies: - fbjs "^0.8.9" - loose-envify "^1.3.1" - -react@^15.6.1: - version "15.6.1" - resolved "https://registry.yarnpkg.com/react/-/react-15.6.1.tgz#baa8434ec6780bde997cdc380b79cd33b96393df" - dependencies: - create-react-class "^15.6.0" - fbjs "^0.8.9" - loose-envify "^1.1.0" - object-assign "^4.1.0" - prop-types "^15.5.10" - -setimmediate@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" - -string-width@~2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" - dependencies: - is-fullwidth-code-point "^2.0.0" - strip-ansi "^4.0.0" - -strip-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" - dependencies: - ansi-regex "^3.0.0" - -supports-color@^4.0.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.4.0.tgz#883f7ddabc165142b2a61427f3352ded195d1a3e" - dependencies: - has-flag "^2.0.0" - -ua-parser-js@^0.7.9: - version "0.7.14" - resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.14.tgz#110d53fa4c3f326c121292bbeac904d2e03387ca" - -whatwg-fetch@>=0.10.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz#9c84ec2dcf68187ff00bc64e1274b442176e1c84" - -wordwrap@~0.0.2: - version "0.0.3" - resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" diff --git a/example-2/yarn.lock b/example-2/yarn.lock deleted file mode 100644 index 9ecf8c83..00000000 --- a/example-2/yarn.lock +++ /dev/null @@ -1,146 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -ansi-styles@^3.1.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.0.tgz#c159b8d5be0f9e5a6f346dab94f16ce022161b88" - dependencies: - color-convert "^1.9.0" - -asap@~2.0.3: - version "2.0.6" - resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" - -chalk@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.1.0.tgz#ac5becf14fa21b99c6c92ca7a7d7cfd5b17e743e" - dependencies: - ansi-styles "^3.1.0" - escape-string-regexp "^1.0.5" - supports-color "^4.0.0" - -color-convert@^1.9.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.0.tgz#1accf97dd739b983bf994d56fec8f95853641b7a" - dependencies: - color-name "^1.1.1" - -color-name@^1.1.1: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - -core-js@^1.0.0: - version "1.2.7" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" - -create-react-class@^15.6.0: - version "15.6.0" - resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.6.0.tgz#ab448497c26566e1e29413e883207d57cfe7bed4" - dependencies: - fbjs "^0.8.9" - loose-envify "^1.3.1" - object-assign "^4.1.1" - -encoding@^0.1.11: - version "0.1.12" - resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb" - dependencies: - iconv-lite "~0.4.13" - -escape-string-regexp@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - -fbjs@^0.8.9: - version "0.8.14" - resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.14.tgz#d1dbe2be254c35a91e09f31f9cd50a40b2a0ed1c" - dependencies: - core-js "^1.0.0" - isomorphic-fetch "^2.1.1" - loose-envify "^1.0.0" - object-assign "^4.1.0" - promise "^7.1.1" - setimmediate "^1.0.5" - ua-parser-js "^0.7.9" - -has-flag@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" - -iconv-lite@~0.4.13: - version "0.4.18" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.18.tgz#23d8656b16aae6742ac29732ea8f0336a4789cf2" - -is-stream@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" - -isomorphic-fetch@^2.1.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9" - dependencies: - node-fetch "^1.0.1" - whatwg-fetch ">=0.10.0" - -js-tokens@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" - -loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848" - dependencies: - js-tokens "^3.0.0" - -node-fetch@^1.0.1: - version "1.7.1" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.1.tgz#899cb3d0a3c92f952c47f1b876f4c8aeabd400d5" - dependencies: - encoding "^0.1.11" - is-stream "^1.0.1" - -object-assign@^4.1.0, object-assign@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - -promise@^7.1.1: - version "7.3.1" - resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" - dependencies: - asap "~2.0.3" - -prop-types@^15.5.10: - version "15.5.10" - resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.10.tgz#2797dfc3126182e3a95e3dfbb2e893ddd7456154" - dependencies: - fbjs "^0.8.9" - loose-envify "^1.3.1" - -react@^15.6.1: - version "15.6.1" - resolved "https://registry.yarnpkg.com/react/-/react-15.6.1.tgz#baa8434ec6780bde997cdc380b79cd33b96393df" - dependencies: - create-react-class "^15.6.0" - fbjs "^0.8.9" - loose-envify "^1.1.0" - object-assign "^4.1.0" - prop-types "^15.5.10" - -setimmediate@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" - -supports-color@^4.0.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.4.0.tgz#883f7ddabc165142b2a61427f3352ded195d1a3e" - dependencies: - has-flag "^2.0.0" - -ua-parser-js@^0.7.9: - version "0.7.14" - resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.14.tgz#110d53fa4c3f326c121292bbeac904d2e03387ca" - -whatwg-fetch@>=0.10.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz#9c84ec2dcf68187ff00bc64e1274b442176e1c84" From e4da359b1cbf949029c12d0585f3aab16e07a295 Mon Sep 17 00:00:00 2001 From: Luke Batchelor Date: Fri, 22 Sep 2017 12:31:52 +1000 Subject: [PATCH 06/14] Refactors install to use symlinkPackageDependencies and adds tests --- example/packages/bar/bar.js | 0 .../packages/foo/package.json | 2 +- .../packages/bar/node_modules/react | 1 + .../packages/foo/node_modules/@scoped/bar | 1 + .../packages/foo/node_modules/react | 1 + .../packages/foo/package.json | 2 +- .../foo/packages/baz/node_modules/@scoped/bar | 1 + .../foo/packages/baz/node_modules/react | 1 + .../packages/foo/packages/baz/package.json | 2 +- src/commands/__tests__/install.test.js | 92 +++++++++-- src/commands/install.js | 149 +----------------- 11 files changed, 94 insertions(+), 158 deletions(-) create mode 100644 example/packages/bar/bar.js create mode 120000 src/__fixtures__/nested-workspaces-with-scoped-package-names/packages/bar/node_modules/react create mode 120000 src/__fixtures__/nested-workspaces-with-scoped-package-names/packages/foo/node_modules/@scoped/bar create mode 120000 src/__fixtures__/nested-workspaces-with-scoped-package-names/packages/foo/node_modules/react create mode 120000 src/__fixtures__/nested-workspaces-with-scoped-package-names/packages/foo/packages/baz/node_modules/@scoped/bar create mode 120000 src/__fixtures__/nested-workspaces-with-scoped-package-names/packages/foo/packages/baz/node_modules/react diff --git a/example/packages/bar/bar.js b/example/packages/bar/bar.js new file mode 100644 index 00000000..e69de29b diff --git a/src/__fixtures__/nested-workspaces-with-root-dependencies-installed/packages/foo/package.json b/src/__fixtures__/nested-workspaces-with-root-dependencies-installed/packages/foo/package.json index f64c0542..b9bd01b1 100644 --- a/src/__fixtures__/nested-workspaces-with-root-dependencies-installed/packages/foo/package.json +++ b/src/__fixtures__/nested-workspaces-with-root-dependencies-installed/packages/foo/package.json @@ -4,7 +4,7 @@ "dependencies": { "bar": "^1.0.0", "external-dep": "^1.0.0", - "external-dep-wth-bin": "^1.0.0" + "external-dep-with-bin": "^1.0.0" }, "pworkspaces": ["packages/*"] } diff --git a/src/__fixtures__/nested-workspaces-with-scoped-package-names/packages/bar/node_modules/react b/src/__fixtures__/nested-workspaces-with-scoped-package-names/packages/bar/node_modules/react new file mode 120000 index 00000000..b29c4786 --- /dev/null +++ b/src/__fixtures__/nested-workspaces-with-scoped-package-names/packages/bar/node_modules/react @@ -0,0 +1 @@ +../../../node_modules/react \ No newline at end of file diff --git a/src/__fixtures__/nested-workspaces-with-scoped-package-names/packages/foo/node_modules/@scoped/bar b/src/__fixtures__/nested-workspaces-with-scoped-package-names/packages/foo/node_modules/@scoped/bar new file mode 120000 index 00000000..cb4cd09f --- /dev/null +++ b/src/__fixtures__/nested-workspaces-with-scoped-package-names/packages/foo/node_modules/@scoped/bar @@ -0,0 +1 @@ +../../../bar \ No newline at end of file diff --git a/src/__fixtures__/nested-workspaces-with-scoped-package-names/packages/foo/node_modules/react b/src/__fixtures__/nested-workspaces-with-scoped-package-names/packages/foo/node_modules/react new file mode 120000 index 00000000..b29c4786 --- /dev/null +++ b/src/__fixtures__/nested-workspaces-with-scoped-package-names/packages/foo/node_modules/react @@ -0,0 +1 @@ +../../../node_modules/react \ No newline at end of file diff --git a/src/__fixtures__/nested-workspaces-with-scoped-package-names/packages/foo/package.json b/src/__fixtures__/nested-workspaces-with-scoped-package-names/packages/foo/package.json index 5ea4a64b..80361993 100644 --- a/src/__fixtures__/nested-workspaces-with-scoped-package-names/packages/foo/package.json +++ b/src/__fixtures__/nested-workspaces-with-scoped-package-names/packages/foo/package.json @@ -4,6 +4,6 @@ "pworkspaces": ["packages/*"], "dependencies": { "react": "^15.6.1", - "bar": "^1.0.0" + "@scoped/bar": "^1.0.0" } } diff --git a/src/__fixtures__/nested-workspaces-with-scoped-package-names/packages/foo/packages/baz/node_modules/@scoped/bar b/src/__fixtures__/nested-workspaces-with-scoped-package-names/packages/foo/packages/baz/node_modules/@scoped/bar new file mode 120000 index 00000000..d77d27eb --- /dev/null +++ b/src/__fixtures__/nested-workspaces-with-scoped-package-names/packages/foo/packages/baz/node_modules/@scoped/bar @@ -0,0 +1 @@ +../../../../../bar \ No newline at end of file diff --git a/src/__fixtures__/nested-workspaces-with-scoped-package-names/packages/foo/packages/baz/node_modules/react b/src/__fixtures__/nested-workspaces-with-scoped-package-names/packages/foo/packages/baz/node_modules/react new file mode 120000 index 00000000..0808930c --- /dev/null +++ b/src/__fixtures__/nested-workspaces-with-scoped-package-names/packages/foo/packages/baz/node_modules/react @@ -0,0 +1 @@ +../../../../../node_modules/react \ No newline at end of file diff --git a/src/__fixtures__/nested-workspaces-with-scoped-package-names/packages/foo/packages/baz/package.json b/src/__fixtures__/nested-workspaces-with-scoped-package-names/packages/foo/packages/baz/package.json index 18c6e967..acf055ae 100644 --- a/src/__fixtures__/nested-workspaces-with-scoped-package-names/packages/foo/packages/baz/package.json +++ b/src/__fixtures__/nested-workspaces-with-scoped-package-names/packages/foo/packages/baz/package.json @@ -3,6 +3,6 @@ "version": "1.0.0", "dependencies": { "react": "^15.6.1", - "bar": "^1.0.0" + "@scoped/bar": "^1.0.0" } } diff --git a/src/commands/__tests__/install.test.js b/src/commands/__tests__/install.test.js index 9b23aad7..c63f0e27 100644 --- a/src/commands/__tests__/install.test.js +++ b/src/commands/__tests__/install.test.js @@ -1,34 +1,104 @@ // @flow -import {install, toInstallOptions} from '../install'; +import { install, toInstallOptions } from '../install'; import * as processes from '../../utils/processes'; +import * as yarn from '../../utils/yarn'; import * as path from 'path'; import * as fs from '../../utils/fs'; -import {getFixturePath} from 'jest-fixtures'; +import Project from '../../Project'; +import Workspace from '../../Workspace'; +import { getFixturePath, copyFixtureIntoTempDir } from 'jest-fixtures'; jest.mock('../../utils/processes'); +jest.mock('../../utils/yarn'); jest.mock('../../utils/logger'); jest.unmock('../install'); const unsafeProcesses: any & typeof processes = processes; +const unsafeYarn: any & typeof yarn = yarn; -describe('install', () => { - let readdirSpy; +async function assertNodeModulesExists(workspace: Workspace) { + const nodeModulesStat = await fs.stat(workspace.pkg.nodeModules); + const nodeModulesBinStat = await fs.stat(workspace.pkg.nodeModulesBin); + expect(nodeModulesStat.isDirectory()).toEqual(true); + expect(nodeModulesBinStat.isDirectory()).toEqual(true); +} - beforeEach(() => { - readdirSpy = jest.spyOn(fs, 'readdir').mockImplementation(() => Promise.resolve([])); - }); +async function assertDependenciesSymlinked(workspace: Workspace) { + const deps = workspace.pkg.getAllDependencies(); - afterEach(() => { - readdirSpy.mockRestore(); - }); + for (let dep of deps.keys()) { + const depStat = await fs.lstat(path.join(workspace.pkg.nodeModules, dep)); + expect(depStat.isSymbolicLink()).toBe(true); + } +} - test('simple-package', async () => { +describe('install', () => { + test('simple-package, should run yarn install at the root', async () => { let cwd = await getFixturePath(__dirname, 'simple-package'); await install(toInstallOptions([], { cwd })); + expect(unsafeProcesses.spawn).toHaveBeenCalledTimes(1); expect(unsafeProcesses.spawn).toHaveBeenCalledWith( 'yarn', ['install', '--non-interactive', '-s'], { cwd } ); }); + + test('should still run yarn install at the root when called from ws', async () => { + let rootDir = await getFixturePath(__dirname, 'simple-project'); + let cwd = path.join(path.join(rootDir, 'packages', 'foo')); + await install(toInstallOptions([], { cwd })); + expect(unsafeProcesses.spawn).toHaveBeenCalledTimes(1); + expect(unsafeProcesses.spawn).toHaveBeenCalledWith( + 'yarn', + ['install', '--non-interactive', '-s'], + { cwd: rootDir } + ); + }); + + test('should work in project with scoped packages', async () => { + let cwd = await getFixturePath( + __dirname, + 'nested-workspaces-with-scoped-package-names' + ); + const project = await Project.init(cwd); + const workspaces = await project.getWorkspaces(); + + await install(toInstallOptions([], { cwd })); + + for (let workspace of workspaces) { + assertNodeModulesExists(workspace); + assertDependenciesSymlinked(workspace); + } + }); + + test('should run preinstall, postinstall and prepublish in each ws', async () => { + let cwd = await getFixturePath(__dirname, 'simple-project'); + const project = await Project.init(cwd); + const workspaces = await project.getWorkspaces(); + + await install(toInstallOptions([], { cwd })); + + for (let workspace of workspaces) { + expect(unsafeYarn.run).toHaveBeenCalledWith(workspace.pkg, 'preinstall'); + expect(unsafeYarn.run).toHaveBeenCalledWith(workspace.pkg, 'postinstall'); + expect(unsafeYarn.run).toHaveBeenCalledWith(workspace.pkg, 'prepublish'); + } + }); + + test('should install (symlink) all deps in workspaces', async () => { + const cwd = await copyFixtureIntoTempDir( + __dirname, + 'nested-workspaces-with-root-dependencies-installed' + ); + const project = await Project.init(cwd); + const workspaces = await project.getWorkspaces(); + + await install(toInstallOptions([], { cwd })); + + for (let workspace of workspaces) { + assertNodeModulesExists(workspace); + assertDependenciesSymlinked(workspace); + } + }); }); diff --git a/src/commands/install.js b/src/commands/install.js index b84a52d3..9165cd12 100644 --- a/src/commands/install.js +++ b/src/commands/install.js @@ -6,6 +6,7 @@ import * as fs from '../utils/fs'; import * as path from 'path'; import * as logger from '../utils/logger'; import * as messages from '../utils/messages'; +import symlinkPackageDependencies from '../utils/symlinkPackageDependencies'; import * as yarn from '../utils/yarn'; import pathIsInside from 'path-is-inside'; import { PError } from '../utils/errors'; @@ -28,158 +29,18 @@ export async function install(opts: InstallOptions) { let project = await Project.init(cwd); let workspaces = await project.getWorkspaces(); - let { - graph: dependencyGraph, - valid: dependencyGraphValid - } = await project.getDependencyGraph(workspaces); - - let projectDependencies = project.pkg.getAllDependencies(); - - let directoriesToCreate = []; - let symlinksToCreate = []; - let valid = true; - - let workspacesToDependencies = {}; - - /** Calculate all the external dependencies that need to be symlinked */ - for (let workspace of workspaces) { - let dependencies = workspace.pkg.getAllDependencies(); - - workspacesToDependencies[workspace.pkg.config.name] = dependencies; - - directoriesToCreate.push(workspace.pkg.nodeModules); - directoriesToCreate.push(workspace.pkg.nodeModulesBin); - - for (let [name, version] of dependencies) { - let matched = projectDependencies.get(name); - - if (dependencyGraph.has(name)) { - continue; - } - - if (!matched) { - valid = false; - logger.error( - messages.depMustBeAddedToProject(workspace.pkg.config.name, name) - ); - continue; - } - - if (version !== matched) { - valid = false; - logger.error( - messages.depMustMatchProject( - workspace.pkg.config.name, - name, - matched, - version - ) - ); - continue; - } - - let src = path.join(project.pkg.nodeModules, name); - let dest = path.join(workspace.pkg.nodeModules, name); - - symlinksToCreate.push({ src, dest, type: 'junction' }); - } - } - - /** Calculate all the internal dependencies that need to be symlinked */ - for (let [name, node] of dependencyGraph) { - let nodeModules = path.join(node.pkg.dir, 'node_modules'); - - for (let dependency of node.dependencies) { - let depWorkspace = dependencyGraph.get(dependency); - - if (!depWorkspace) { - throw new PError(`Missing workspace: "${dependency}"`); - } - - let src = depWorkspace.pkg.dir; - let dest = path.join(nodeModules, dependency); - - symlinksToCreate.push({ src, dest, type: 'junction' }); - } - } - - if (!dependencyGraphValid || !valid) { - throw new PError('Cannot symlink invalid set of dependencies.'); - } - - await Project.runWorkspaceTasks(workspaces, async workspace => { - await yarn.run(workspace.pkg, 'preinstall'); - }); - logger.log('[1/2] Installing project dependencies...'); await processes.spawn('yarn', ['install', '--non-interactive', '-s'], { cwd: project.pkg.dir }); - logger.log('[2/2] Linking workspace dependencies...'); - - /** Calculate all the bin files that need to be symlinked */ - - for (let binFile of await fs.readdirSafe(project.pkg.nodeModulesBin)) { - let binPath = path.join(project.pkg.nodeModulesBin, binFile); - let binName = path.basename(binPath); - - let linkFile = await fs.readlink(binPath); - - if (!linkFile) { - throw new PError(`${binName} is not a symlink`); - } - - let linkPath = path.join(project.pkg.nodeModulesBin, linkFile); - - if (!pathIsInside(linkPath, project.pkg.nodeModules)) { - throw new PError( - `${binName} is linked to a location outside of project node_modules: ${linkPath}` - ); - } + logger.log(`[2/2] Linking ${workspaces.length} workspace dependencies...`); - let relativeLinkPath = path.relative(project.pkg.nodeModules, linkPath); - let pathParts = relativeLinkPath.split(path.sep); - let pkgName = pathParts[0]; - - if (pkgName.startsWith('@')) { - pkgName += '/' + pathParts[1]; - } - - for (let workspace of workspaces) { - let dependencies = workspacesToDependencies[workspace.pkg.config.name]; - - if (!dependencies.has(pkgName)) { - continue; - } - - let workspaceBinPath = path.join(workspace.pkg.nodeModulesBin, binName); - - symlinksToCreate.push({ - src: binPath, - dest: workspaceBinPath, - type: 'exec' - }); - } + for (let workspace of workspaces) { + const dependencies = workspace.pkg.getAllDependencies().keys(); + await symlinkPackageDependencies(project, workspace.pkg, dependencies); } - await Promise.all( - directoriesToCreate.map(dirName => { - return fs.mkdirp(dirName); - }) - ); - - await Promise.all( - symlinksToCreate.map(async ({ src, dest, type }) => { - await fs.symlink(src, dest, type); - }) - ); - - await Project.runWorkspaceTasks(workspaces, async workspace => { - await yarn.run(workspace.pkg, 'postinstall'); - await yarn.run(workspace.pkg, 'prepublish'); - }); - logger.success('Installed and linked workspaces.'); } From c1ac623e04f26c455fd8c44fdb778917de155f0d Mon Sep 17 00:00:00 2001 From: Luke Batchelor Date: Fri, 22 Sep 2017 12:33:17 +1000 Subject: [PATCH 07/14] Adds comments to tests --- src/commands/__tests__/install.test.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/commands/__tests__/install.test.js b/src/commands/__tests__/install.test.js index c63f0e27..5ea72c8c 100644 --- a/src/commands/__tests__/install.test.js +++ b/src/commands/__tests__/install.test.js @@ -86,6 +86,7 @@ describe('install', () => { } }); + // This is re-testing symlinkPackageDependencies, but we'd rather be explicit here test('should install (symlink) all deps in workspaces', async () => { const cwd = await copyFixtureIntoTempDir( __dirname, @@ -99,6 +100,7 @@ describe('install', () => { for (let workspace of workspaces) { assertNodeModulesExists(workspace); assertDependenciesSymlinked(workspace); + // TODO: assertBinfileSymlinked (currently tested partially in symlinkPackageDependencies) } }); }); From 3f1ad5b1f087c1c5e6e12d53aebf512750da7d6b Mon Sep 17 00:00:00 2001 From: Luke Batchelor Date: Mon, 25 Sep 2017 15:19:37 +1000 Subject: [PATCH 08/14] addDependenciesToPackages now updates versions in configs --- src/Package.js | 9 + .../packages/foo/packages/baz/package.json | 2 +- src/commands/__tests__/remove.test.js | 2 +- .../addDependenciesToPackages.test.js | 180 +++++++++++++----- src/utils/addDependenciesToPackages.js | 67 +++++-- src/utils/symlinkPackageDependencies.js | 4 +- 6 files changed, 205 insertions(+), 59 deletions(-) diff --git a/src/Package.js b/src/Package.js index e8e6312d..8336cabe 100644 --- a/src/Package.js +++ b/src/Package.js @@ -132,6 +132,15 @@ export default class Package { return null; } + getDependencyVersionRange(depName: string) { + for (let depType of DEPENDENCY_TYPES) { + if (this.config[depType] && this.config[depType][depName]) { + return this.config[depType][depName]; + } + } + return null; + } + // async maybeUpdateDependencyVersionRange(depName: string, current: string, version: string) { // let versionRange = '^' + version; // let updated = false; diff --git a/src/__fixtures__/nested-workspaces/packages/foo/packages/baz/package.json b/src/__fixtures__/nested-workspaces/packages/foo/packages/baz/package.json index b941c80f..0a69ca46 100644 --- a/src/__fixtures__/nested-workspaces/packages/foo/packages/baz/package.json +++ b/src/__fixtures__/nested-workspaces/packages/foo/packages/baz/package.json @@ -1,6 +1,6 @@ { "name": "baz", - "version": "1.0.0", + "version": "1.0.1", "dependencies": { "react": "^15.6.1", "bar": "^1.0.0" diff --git a/src/commands/__tests__/remove.test.js b/src/commands/__tests__/remove.test.js index 7590c5db..aefb086c 100644 --- a/src/commands/__tests__/remove.test.js +++ b/src/commands/__tests__/remove.test.js @@ -55,7 +55,7 @@ describe('pyarn remove', () => { ).rejects.toBeInstanceOf(Error); }); - test.only('removing a dependency that is used by a workspace', async () => { + test('removing a dependency that is used by a workspace', async () => { let tempDir = await copyFixtureIntoTempDir( __dirname, 'package-with-external-deps-installed' diff --git a/src/utils/__tests__/addDependenciesToPackages.test.js b/src/utils/__tests__/addDependenciesToPackages.test.js index ddaed243..aecc1e45 100644 --- a/src/utils/__tests__/addDependenciesToPackages.test.js +++ b/src/utils/__tests__/addDependenciesToPackages.test.js @@ -1,15 +1,28 @@ // @flow -import { getFixturePath } from 'jest-fixtures'; +import { copyFixtureIntoTempDir } from 'jest-fixtures'; +import path from 'path'; import addDependenciesToPackage from '../addDependenciesToPackages'; +import * as symlinkPackageDependencies from '../symlinkPackageDependencies'; import * as yarn from '../yarn'; import Project from '../../Project'; +import Package from '../../Package'; jest.mock('../yarn'); +jest.mock('../logger'); const unsafeYarn: any & typeof yarn = yarn; +// Mock yarn.add to make it update the packages config when called +function fakeYarnAdd(pkg, dependencies, type = 'dependencies') { + pkg.config[type] = pkg.config[type] || {}; + + dependencies.forEach(dep => { + pkg.config[type][dep.name] = dep.version || '^1.0.0'; + }); +} + function assertSingleYarnAddCall(expectedPkg, expectedDeps) { const yarnAddCalls = unsafeYarn.add.mock.calls; @@ -19,59 +32,138 @@ function assertSingleYarnAddCall(expectedPkg, expectedDeps) { } describe('utils/addDependenciesToPackages', () => { - describe('addDependenciesToPackage()', () => { - let cwd; - let project; - let projectPkg; - let workspaces; - - beforeEach(async () => { - cwd = await getFixturePath(__dirname, 'nested-workspaces'); - project = await Project.init(cwd); - projectPkg = project.pkg; - workspaces = await project.getWorkspaces(); - }); + let symlinkSpy; - describe('when called from the root of a project', () => { - it('should just run yarn add in the project dir', async () => { - const depsToAdd = [{ name: 'chalk' }]; - await addDependenciesToPackage(project, projectPkg, depsToAdd); + beforeEach(() => { + unsafeYarn.add.mockImplementation(fakeYarnAdd); + symlinkSpy = jest.spyOn(symlinkPackageDependencies, 'default'); + }); - assertSingleYarnAddCall(projectPkg, depsToAdd); - }); - }); + test('it should just yarn add at the root when run from the root of a project', async () => { + const cwd = await copyFixtureIntoTempDir(__dirname, 'nested-workspaces'); + const project = await Project.init(cwd); - describe('when called from a workspace', () => { - let pkgToRunIn; + await addDependenciesToPackage(project, project.pkg, [{ name: 'chalk' }]); - beforeEach(() => { - const workspaceToRunIn = - project.getWorkspaceByName(workspaces, 'foo') || {}; - pkgToRunIn = workspaceToRunIn.pkg; - }); + expect(unsafeYarn.add).toHaveBeenCalledTimes(1); + expect(unsafeYarn.add).toHaveBeenCalledWith( + project.pkg, + [{ name: 'chalk' }], + 'dependencies' + ); + }); + + test('it should still run yarn add at the root when run from another package', async () => { + const rootDir = await copyFixtureIntoTempDir( + __dirname, + 'nested-workspaces' + ); + const project = await Project.init(rootDir); + const workspaces = await project.getWorkspaces(); + const wsToRunIn = project.getWorkspaceByName(workspaces, 'foo') || {}; + + await addDependenciesToPackage(project, wsToRunIn.pkg, [{ name: 'chalk' }]); + + expect(unsafeYarn.add).toHaveBeenCalledTimes(1); + expect(unsafeYarn.add).toHaveBeenCalledWith( + project.pkg, + [{ name: 'chalk' }], + 'dependencies' + ); + }); - describe('if all packages are already in the root', () => {}); + test('it should not yarn install packages that are already installed', async () => { + const cwd = await copyFixtureIntoTempDir(__dirname, 'nested-workspaces'); + const project = await Project.init(cwd); - describe('if some packages are not in the root', () => { - it('should only run yarn add for missing deps', async () => { - // our project already has left-pad installed - const depsToAdd = [{ name: 'chalk' }, { name: 'left-pad' }]; + await addDependenciesToPackage(project, project.pkg, [{ name: 'react' }]); - await addDependenciesToPackage(project, pkgToRunIn, depsToAdd); - // should only add chalk to root - assertSingleYarnAddCall(projectPkg, [{ name: 'chalk' }]); - }); - }); + expect(unsafeYarn.add).toHaveBeenCalledWith( + project.pkg, + [], + 'dependencies' + ); + }); + + test('it should throw if version does not match version in project config', async () => { + const cwd = await copyFixtureIntoTempDir(__dirname, 'nested-workspaces'); + const project = await Project.init(cwd); + const workspaces = await project.getWorkspaces(); + const wsToAddTo = project.getWorkspaceByName(workspaces, 'foo') || {}; + + await expect( + addDependenciesToPackage(project, wsToAddTo.pkg, [ + { name: 'left-pad', version: '2.0.0' } + ]) + ).rejects.toBeInstanceOf(Error); + }); - describe('if all packages are not in the root', () => { - it('should run yarn add in the root', async () => { - const depsToAdd = [{ name: 'chalk', name: 'does-not-exist' }]; + test('should call symlinkPackageDependencies to symlink dependencies in workspace', async () => { + const cwd = await copyFixtureIntoTempDir(__dirname, 'nested-workspaces'); + const project = await Project.init(cwd); + const workspaces = await project.getWorkspaces(); + const wsToAddTo = project.getWorkspaceByName(workspaces, 'foo') || {}; - await addDependenciesToPackage(project, pkgToRunIn, depsToAdd); + await addDependenciesToPackage(project, wsToAddTo.pkg, [{ name: 'chalk' }]); + + expect(symlinkSpy).toHaveBeenCalledWith(project, wsToAddTo.pkg, ['chalk']); + }); + + test('should update packages dependencies in package config', async () => { + const cwd = await copyFixtureIntoTempDir(__dirname, 'nested-workspaces'); + const project = await Project.init(cwd); + const workspaces = await project.getWorkspaces(); + const wsToAddTo = project.getWorkspaceByName(workspaces, 'foo') || {}; + + expect(wsToAddTo.pkg.getDependencyVersionRange('chalk')).toEqual(null); + await addDependenciesToPackage(project, wsToAddTo.pkg, [{ name: 'chalk' }]); + + expect(symlinkSpy).toHaveBeenCalledWith(project, wsToAddTo.pkg, ['chalk']); + expect(wsToAddTo.pkg.getDependencyVersionRange('chalk')).toEqual('^1.0.0'); + }); + + describe('when adding internal package', () => { + test('should set version as current version of internal package', async () => { + // i.e if we have bar 1.0.0 installed locally addDependenciesToPackages should add ^1.0.0 + const cwd = await copyFixtureIntoTempDir(__dirname, 'nested-workspaces'); + const project = await Project.init(cwd); + const workspaces = await project.getWorkspaces(); + const wsToAddTo = project.getWorkspaceByName(workspaces, 'bar') || {}; + + expect(wsToAddTo.pkg.getDependencyVersionRange('baz')).toEqual(null); + await addDependenciesToPackage(project, wsToAddTo.pkg, [{ name: 'baz' }]); + + expect(symlinkSpy).toHaveBeenCalledWith(project, wsToAddTo.pkg, ['baz']); + expect(wsToAddTo.pkg.getDependencyVersionRange('baz')).toEqual('^1.0.1'); + }); + + test('should throw if attempting to set to version that doesnt match local', async () => { + const cwd = await copyFixtureIntoTempDir(__dirname, 'nested-workspaces'); + const project = await Project.init(cwd); + const workspaces = await project.getWorkspaces(); + const wsToAddTo = project.getWorkspaceByName(workspaces, 'bar') || {}; + + await expect( + addDependenciesToPackage(project, wsToAddTo.pkg, [ + { name: 'baz', version: '^1.0.0' } + ]) + ).rejects.toBeInstanceOf(Error); + }); - assertSingleYarnAddCall(projectPkg, depsToAdd); - }); - }); + test('should not add internal to root package', async () => { + const cwd = await copyFixtureIntoTempDir(__dirname, 'nested-workspaces'); + const project = await Project.init(cwd); + const workspaces = await project.getWorkspaces(); + const wsToAddTo = project.getWorkspaceByName(workspaces, 'bar') || {}; + + expect(project.pkg.getDependencyVersionRange('baz')).toEqual(null); + await addDependenciesToPackage(project, wsToAddTo.pkg, [{ name: 'baz' }]); + expect(project.pkg.getDependencyVersionRange('baz')).toEqual(null); + expect(unsafeYarn.add).toHaveBeenCalledWith( + project.pkg, + [], + 'dependencies' + ); }); }); }); diff --git a/src/utils/addDependenciesToPackages.js b/src/utils/addDependenciesToPackages.js index 39e4f0d2..754bd0a7 100644 --- a/src/utils/addDependenciesToPackages.js +++ b/src/utils/addDependenciesToPackages.js @@ -4,28 +4,73 @@ import Project from '../Project'; import type Workspace from '../Workspace'; import type Package from '../Package'; import type { Dependency, configDependencyType } from '../types'; +import * as messages from './messages'; import * as logger from './logger'; import * as yarn from './yarn'; +import symlinkPackageDependencies from './symlinkPackageDependencies'; export default async function addDependenciesToPackage( project: Project, pkg: Package, dependencies: Array, - type?: configDependencyType + type?: configDependencyType = 'dependencies' ) { - const isProjectPackage = project.pkg.dir === pkg.dir; - if (isProjectPackage) { - await yarn.add(project.pkg, dependencies); - return true; - } - + const workspaces = await project.getWorkspaces(); const projectDependencies = project.pkg.getAllDependencies(); - const depsToInstallInProject = dependencies.filter( + const pkgDependencies = pkg.getAllDependencies(); + const { graph: depGraph } = await project.getDependencyGraph(workspaces); + + const dependencyNames = dependencies.map(dep => dep.name); + const externalDeps = dependencies.filter(dep => !depGraph.has(dep.name)); + const internalDeps = dependencies.filter(dep => depGraph.has(dep.name)); + + const externalDepsToInstallForProject = externalDeps.filter( dep => !projectDependencies.has(dep.name) ); + await yarn.add(project.pkg, externalDepsToInstallForProject, type); + + if (pkg.isSamePackage(project.pkg)) { + return true; + } + + const installedVersions = {}; + + for (let dep of externalDeps) { + const installed = project.pkg.getDependencyVersionRange(dep.name); + if (dep.version && dep.version !== installed) { + logger.warn( + messages.depMustMatchProject( + pkg.config.name, + dep.name, + installed, + dep.version + ) + ); + throw new Error(); + } + installedVersions[dep.name] = String(installed); + } - await yarn.add(project.pkg, depsToInstallInProject); + for (let dep of internalDeps) { + const dependencyPkg = (depGraph.get(dep.name) || {}).pkg; + const curVersion = dependencyPkg.config.version; + if (dep.version) { + logger.warn( + messages.packageMustDependOnCurrentVersion( + pkg.config.name, + dep.name, + curVersion, + dep.version + ) + ); + throw new Error(); + } + installedVersions[dep.name] = `^${curVersion}`; + } + + for (let [depName, depVersion] of Object.entries(installedVersions)) { + await pkg.setDependencyVersionRange(depName, type, depVersion); + } - const depsToSymlink = []; - dependencies.forEach(dep => {}); + await symlinkPackageDependencies(project, pkg, dependencyNames); } diff --git a/src/utils/symlinkPackageDependencies.js b/src/utils/symlinkPackageDependencies.js index 85c65edb..5b870bdc 100644 --- a/src/utils/symlinkPackageDependencies.js +++ b/src/utils/symlinkPackageDependencies.js @@ -39,8 +39,8 @@ export default async function symlinkPackageDependencies( directoriesToCreate.push(pkg.nodeModules, pkg.nodeModulesBin); for (let depName of dependencies) { - const versionInProject = projectDeps.get(depName); - const versionInPkg = pkgDependencies.get(depName); + const versionInProject = project.pkg.getDependencyVersionRange(depName); + const versionInPkg = pkg.getDependencyVersionRange(depName); // If dependency is internal we can ignore it (we symlink below) if (dependencyGraph.has(depName)) { From d4118478ea0cd0ff5a89688029800f5a4d9d0615 Mon Sep 17 00:00:00 2001 From: Luke Batchelor Date: Tue, 3 Oct 2017 19:01:47 +1100 Subject: [PATCH 09/14] Fixes rest of minor changes after refactor --- src/Config.js | 12 +++++++++ src/Package.js | 8 +++--- .../nested-workspaces-2/package.json | 12 +++++++++ .../packages/bar/package.json | 7 +++++ .../packages/foo/package.json | 11 ++++++++ .../packages/foo/packages/baz/package.json | 8 ++++++ .../package.json | 8 +++--- .../packages/foo/package.json | 4 ++- .../addDependenciesToPackages.test.js | 5 ++-- .../symlinkPackageDependencies.test.js | 1 - src/utils/addDependenciesToPackages.js | 12 ++++----- src/utils/messages.js | 2 +- src/utils/symlinkPackageDependencies.js | 26 ++++++++++++------- 13 files changed, 88 insertions(+), 28 deletions(-) create mode 100644 src/__fixtures__/nested-workspaces-2/package.json create mode 100644 src/__fixtures__/nested-workspaces-2/packages/bar/package.json create mode 100644 src/__fixtures__/nested-workspaces-2/packages/foo/package.json create mode 100644 src/__fixtures__/nested-workspaces-2/packages/foo/packages/baz/package.json diff --git a/src/Config.js b/src/Config.js index ada0bd86..736a52af 100644 --- a/src/Config.js +++ b/src/Config.js @@ -224,4 +224,16 @@ export default class Config { .filePath}"` ); } + + getBin() { + let config = this.getConfig(); + let bin = config.bin; + if (typeof bin === 'undefined') return; + if (typeof bin === 'string') return bin; + return toObjectOfStrings( + bin, + `package.json#bin must be an object of strings or a string. See "${this + .filePath}"` + ); + } } diff --git a/src/Package.js b/src/Package.js index 09683515..a8c60ad1 100644 --- a/src/Package.js +++ b/src/Package.js @@ -108,8 +108,7 @@ export default class Package { cleaned[depName] = versionRange; } } - - this.config.write({ + await this.config.write({ ...this.config.getConfig(), [depType]: sortObject(cleaned) }); @@ -127,8 +126,9 @@ export default class Package { getDependencyVersionRange(depName: string) { for (let depType of DEPENDENCY_TYPES) { - if (this.config[depType] && this.config[depType][depName]) { - return this.config[depType][depName]; + const deps = this.config.getDeps(depType); + if (deps && deps[depName]) { + return deps[depName]; } } return null; diff --git a/src/__fixtures__/nested-workspaces-2/package.json b/src/__fixtures__/nested-workspaces-2/package.json new file mode 100644 index 00000000..cbd2cab3 --- /dev/null +++ b/src/__fixtures__/nested-workspaces-2/package.json @@ -0,0 +1,12 @@ +{ + "private": true, + "version": "1.0.0", + "name": "fixture-project-nested-workspaces", + "bolt": { + "workspaces": ["packages/*"] + }, + "dependencies": { + "react": "^15.6.1", + "left-pad": "^1.1.3" + } +} diff --git a/src/__fixtures__/nested-workspaces-2/packages/bar/package.json b/src/__fixtures__/nested-workspaces-2/packages/bar/package.json new file mode 100644 index 00000000..18e841eb --- /dev/null +++ b/src/__fixtures__/nested-workspaces-2/packages/bar/package.json @@ -0,0 +1,7 @@ +{ + "name": "bar", + "version": "1.0.0", + "dependencies": { + "react": "^15.6.1" + } +} diff --git a/src/__fixtures__/nested-workspaces-2/packages/foo/package.json b/src/__fixtures__/nested-workspaces-2/packages/foo/package.json new file mode 100644 index 00000000..ef1f6028 --- /dev/null +++ b/src/__fixtures__/nested-workspaces-2/packages/foo/package.json @@ -0,0 +1,11 @@ +{ + "name": "foo", + "version": "1.0.0", + "bolt": { + "workspaces": ["packages/*"] + }, + "dependencies": { + "react": "^15.6.1", + "bar": "^1.0.0" + } +} diff --git a/src/__fixtures__/nested-workspaces-2/packages/foo/packages/baz/package.json b/src/__fixtures__/nested-workspaces-2/packages/foo/packages/baz/package.json new file mode 100644 index 00000000..0a69ca46 --- /dev/null +++ b/src/__fixtures__/nested-workspaces-2/packages/foo/packages/baz/package.json @@ -0,0 +1,8 @@ +{ + "name": "baz", + "version": "1.0.1", + "dependencies": { + "react": "^15.6.1", + "bar": "^1.0.0" + } +} diff --git a/src/__fixtures__/nested-workspaces-with-root-dependencies-installed/package.json b/src/__fixtures__/nested-workspaces-with-root-dependencies-installed/package.json index ccc85e09..ab8062a2 100644 --- a/src/__fixtures__/nested-workspaces-with-root-dependencies-installed/package.json +++ b/src/__fixtures__/nested-workspaces-with-root-dependencies-installed/package.json @@ -6,9 +6,11 @@ "description2": "Packages: foo, bar, baz, zee", "description3": "bar has a single bin file, baz has two", "description4": "foo and baz depend on bar, zee depends on baz", - "pworkspaces": [ - "packages/*" - ], + "bolt": { + "workspaces": [ + "packages/*" + ] + }, "dependencies": { "external-dep": "^1.0.0", "external-dep-with-bin": "^1.0.0", diff --git a/src/__fixtures__/nested-workspaces-with-root-dependencies-installed/packages/foo/package.json b/src/__fixtures__/nested-workspaces-with-root-dependencies-installed/packages/foo/package.json index b9bd01b1..e6fc7858 100644 --- a/src/__fixtures__/nested-workspaces-with-root-dependencies-installed/packages/foo/package.json +++ b/src/__fixtures__/nested-workspaces-with-root-dependencies-installed/packages/foo/package.json @@ -6,5 +6,7 @@ "external-dep": "^1.0.0", "external-dep-with-bin": "^1.0.0" }, - "pworkspaces": ["packages/*"] + "bolt": { + "workspaces": ["packages/*"] + } } diff --git a/src/utils/__tests__/addDependenciesToPackages.test.js b/src/utils/__tests__/addDependenciesToPackages.test.js index aecc1e45..48be4ddd 100644 --- a/src/utils/__tests__/addDependenciesToPackages.test.js +++ b/src/utils/__tests__/addDependenciesToPackages.test.js @@ -8,6 +8,7 @@ import * as symlinkPackageDependencies from '../symlinkPackageDependencies'; import * as yarn from '../yarn'; import Project from '../../Project'; import Package from '../../Package'; +import Config from '../../Config'; jest.mock('../yarn'); jest.mock('../logger'); @@ -16,10 +17,10 @@ const unsafeYarn: any & typeof yarn = yarn; // Mock yarn.add to make it update the packages config when called function fakeYarnAdd(pkg, dependencies, type = 'dependencies') { - pkg.config[type] = pkg.config[type] || {}; + pkg.config.json[type] = pkg.config.json[type] || {}; dependencies.forEach(dep => { - pkg.config[type][dep.name] = dep.version || '^1.0.0'; + pkg.config.json[type][dep.name] = dep.version || '^1.0.0'; }); } diff --git a/src/utils/__tests__/symlinkPackageDependencies.test.js b/src/utils/__tests__/symlinkPackageDependencies.test.js index e66da250..c4c3f0e3 100644 --- a/src/utils/__tests__/symlinkPackageDependencies.test.js +++ b/src/utils/__tests__/symlinkPackageDependencies.test.js @@ -33,7 +33,6 @@ async function symlinkExists(dir: string, symlink: string) { describe('utils/symlinkPackageDependencies()', () => { let project; let workspaces; - let mkdir; let pkgToSymlink; let nodeModules; let nodeModulesBin; diff --git a/src/utils/addDependenciesToPackages.js b/src/utils/addDependenciesToPackages.js index 754bd0a7..206094fd 100644 --- a/src/utils/addDependenciesToPackages.js +++ b/src/utils/addDependenciesToPackages.js @@ -40,10 +40,10 @@ export default async function addDependenciesToPackage( if (dep.version && dep.version !== installed) { logger.warn( messages.depMustMatchProject( - pkg.config.name, + pkg.config.getName(), dep.name, installed, - dep.version + String(dep.version) ) ); throw new Error(); @@ -53,14 +53,14 @@ export default async function addDependenciesToPackage( for (let dep of internalDeps) { const dependencyPkg = (depGraph.get(dep.name) || {}).pkg; - const curVersion = dependencyPkg.config.version; + const curVersion = dependencyPkg.config.getVersion(); if (dep.version) { logger.warn( messages.packageMustDependOnCurrentVersion( - pkg.config.name, + pkg.config.getName(), dep.name, curVersion, - dep.version + String(dep.version) ) ); throw new Error(); @@ -69,7 +69,7 @@ export default async function addDependenciesToPackage( } for (let [depName, depVersion] of Object.entries(installedVersions)) { - await pkg.setDependencyVersionRange(depName, type, depVersion); + await pkg.setDependencyVersionRange(depName, type, String(depVersion)); } await symlinkPackageDependencies(project, pkg, dependencyNames); diff --git a/src/utils/messages.js b/src/utils/messages.js index 2ef0210e..09462046 100644 --- a/src/utils/messages.js +++ b/src/utils/messages.js @@ -145,7 +145,7 @@ export function couldntRemoveDependencies(deps: Array): Message { export function couldntSymlinkDependencyNotExists( pkgName: string, depName: string -) { +): Message { return `Could not symlink ${depName} in ${pkgName} as no dependency exists`; } diff --git a/src/utils/symlinkPackageDependencies.js b/src/utils/symlinkPackageDependencies.js index 5b870bdc..55c38f91 100644 --- a/src/utils/symlinkPackageDependencies.js +++ b/src/utils/symlinkPackageDependencies.js @@ -6,7 +6,7 @@ import includes from 'array-includes'; import Project from '../Project'; import type Workspace from '../Workspace'; import type Package from '../Package'; -import { PError } from './errors'; +import { BoltError } from './errors'; import * as fs from './fs'; import * as logger from './logger'; import * as messages from './messages'; @@ -24,8 +24,9 @@ export default async function symlinkPackageDependencies( graph: dependencyGraph, valid: dependencyGraphValid } = await project.getDependencyGraph(workspaces); - const internalDeps = - (dependencyGraph.get(pkg.config.name) || {}).dependencies || []; + const pkgName = pkg.config.getName(); + // get all the dependencies that are internal workspaces in this project + const internalDeps = (dependencyGraph.get(pkgName) || {}).dependencies || []; const directoriesToCreate = []; const symlinksToCreate = []; @@ -49,14 +50,19 @@ export default async function symlinkPackageDependencies( if (!versionInProject) { valid = false; - logger.error(messages.depMustBeAddedToProject(pkg.config.name, depName)); + logger.error( + messages.depMustBeAddedToProject(pkg.config.getName(), depName) + ); continue; } if (!versionInPkg) { valid = false; logger.error( - messages.couldntSymlinkDependencyNotExists(pkg.config.name, depName) + messages.couldntSymlinkDependencyNotExists( + pkg.config.getName(), + depName + ) ); continue; } @@ -65,7 +71,7 @@ export default async function symlinkPackageDependencies( valid = false; logger.error( messages.depMustMatchProject( - pkg.config.name, + pkg.config.getName(), depName, versionInProject, versionInPkg @@ -93,7 +99,7 @@ export default async function symlinkPackageDependencies( } if (!dependencyGraphValid || !valid) { - throw new PError('Cannot symlink invalid set of dependencies.'); + throw new BoltError('Cannot symlink invalid set of dependencies.'); } /******************************************************** @@ -114,7 +120,7 @@ export default async function symlinkPackageDependencies( const actualBinFileRelative = await fs.readlink(binPath); if (!actualBinFileRelative) { - throw new PError(`${binName} is not a symlink`); + throw new BoltError(`${binName} is not a symlink`); } const actualBinFile = path.join( @@ -123,7 +129,7 @@ export default async function symlinkPackageDependencies( ); if (!pathIsInside(actualBinFile, project.pkg.nodeModules)) { - throw new PError( + throw new BoltError( `${binName} is linked to a location outside of project node_modules: ${actualBinFileRelative}` ); } @@ -161,7 +167,7 @@ export default async function symlinkPackageDependencies( const depBinFiles = depWorkspace.pkg && depWorkspace.pkg.config && - depWorkspace.pkg.config.bin; + depWorkspace.pkg.config.getBin(); if (!depBinFiles) { continue; From da357d8a5c9215841da430096b1eeaf05a5b99e9 Mon Sep 17 00:00:00 2001 From: Luke Batchelor Date: Fri, 6 Oct 2017 12:52:19 +1100 Subject: [PATCH 10/14] Adds config invalidation and workspace add tests --- src/Config.js | 10 ++ src/commands/workspace/__tests__/add.test.js | 102 +++++++++++++++++- src/commands/workspace/add.js | 55 +++++++++- .../addDependenciesToPackages.test.js | 48 +++++---- src/utils/addDependenciesToPackages.js | 6 +- 5 files changed, 197 insertions(+), 24 deletions(-) diff --git a/src/Config.js b/src/Config.js index 736a52af..683696b8 100644 --- a/src/Config.js +++ b/src/Config.js @@ -69,6 +69,7 @@ export default class Config { fileContents: string; json: JSONValue; indent: string; + invalid: boolean; constructor(filePath: string, fileContents: string) { this.filePath = filePath; @@ -125,6 +126,11 @@ export default class Config { } getConfig(): { [key: string]: JSONValue } { + if (this.invalid) { + throw new BoltError( + `You need to refresh the Config object for ${this.filePath}` + ); + } let config = this.json; if ( @@ -140,6 +146,10 @@ export default class Config { return config; } + invalidate() { + this.invalid = true; + } + getDescriptor(): string { if (this.json && typeof this.json.name === 'string') { return this.json.name; diff --git a/src/commands/workspace/__tests__/add.test.js b/src/commands/workspace/__tests__/add.test.js index d7034ee1..91af5cbc 100644 --- a/src/commands/workspace/__tests__/add.test.js +++ b/src/commands/workspace/__tests__/add.test.js @@ -1,4 +1,104 @@ // @flow import { workspaceAdd, toWorkspaceAddOptions } from '../add'; +import { copyFixtureIntoTempDir } from 'jest-fixtures'; +import * as path from 'path'; +import * as processes from '../../../utils/processes'; +import * as yarn from '../../../utils/yarn'; +import * as fs from '../../../utils/fs'; +import Package from '../../../Package'; +import pathExists from 'path-exists'; -test('bolt workspace add'); +jest.mock('../../../utils/yarn'); +jest.mock('../../../utils/logger'); + +const unsafeProcessses: any & typeof processes = processes; +const unsafeYarn: any & typeof yarn = yarn; + +async function depIsInstalled(workspaceDir: string, depName: string) { + const pkg = await Package.init(path.join(workspaceDir, 'package.json')); + const dirExists = await pathExists( + path.join(workspaceDir, 'node_modules', depName) + ); + const depInPkgJson = pkg.getDependencyType(depName) !== null; + + return dirExists && depInPkgJson; +} + +describe('bolt workspace add', () => { + let projectDir; + let fooWorkspaceDir; + let barWorkspaceDir; + + beforeEach(async () => { + projectDir = await copyFixtureIntoTempDir( + __dirname, + 'package-with-external-deps-installed' + ); + fooWorkspaceDir = path.join(projectDir, 'packages', 'foo'); + barWorkspaceDir = path.join(projectDir, 'packages', 'bar'); + }); + + test('running from project, installing dependency that is in project', async () => { + expect(await depIsInstalled(fooWorkspaceDir, 'project-only-dep')).toEqual( + false + ); + await workspaceAdd( + toWorkspaceAddOptions(['foo', 'project-only-dep'], { + cwd: projectDir + }) + ); + expect(yarn.add).toHaveBeenCalledTimes(0); + expect(await depIsInstalled(fooWorkspaceDir, 'project-only-dep')).toEqual( + true + ); + }); + + test('running from a different workspace, installing dependency that is in project', async () => { + expect(await depIsInstalled(fooWorkspaceDir, 'project-only-dep')).toEqual( + false + ); + await workspaceAdd( + toWorkspaceAddOptions(['foo', 'project-only-dep'], { + cwd: barWorkspaceDir + }) + ); + expect(yarn.add).toHaveBeenCalledTimes(0); + expect(await depIsInstalled(fooWorkspaceDir, 'project-only-dep')).toEqual( + true + ); + }); + + test('running from project, installing dep not in project', async () => { + // yarn add is mocked, so we need to update the package json and create a dir in node_modules + // ourselves (the tasks `yarn add` would normally do) + unsafeYarn.add.mockImplementationOnce(async (pkg: Package) => { + await pkg.setDependencyVersionRange('new-dep', 'dependencies', '^1.0.0'); + await fs.mkdirp(path.join(pkg.nodeModules, 'new-dep')); + }); + expect(await depIsInstalled(fooWorkspaceDir, 'new-dep')).toEqual(false); + await workspaceAdd( + toWorkspaceAddOptions(['foo', 'new-dep'], { + cwd: projectDir + }) + ); + expect(yarn.add).toHaveBeenCalledTimes(1); + expect(await depIsInstalled(fooWorkspaceDir, 'new-dep')).toEqual(true); + }); + + test('running from a different workspace, installing dep not in project', async () => { + // yarn add is mocked, so we need to update the package json and create a dir in node_modules + // ourselves (the tasks `yarn add` would normally do) + unsafeYarn.add.mockImplementationOnce(async (pkg: Package) => { + await pkg.setDependencyVersionRange('new-dep', 'dependencies', '^1.0.0'); + await fs.mkdirp(path.join(pkg.nodeModules, 'new-dep')); + }); + expect(await depIsInstalled(fooWorkspaceDir, 'new-dep')).toEqual(false); + await workspaceAdd( + toWorkspaceAddOptions(['foo', 'new-dep'], { + cwd: barWorkspaceDir + }) + ); + expect(yarn.add).toHaveBeenCalledTimes(1); + expect(await depIsInstalled(fooWorkspaceDir, 'new-dep')).toEqual(true); + }); +}); diff --git a/src/commands/workspace/add.js b/src/commands/workspace/add.js index 10c84d49..d9fd6257 100644 --- a/src/commands/workspace/add.js +++ b/src/commands/workspace/add.js @@ -1,20 +1,69 @@ // @flow +import Project from '../../Project'; +import Package from '../../Package'; import * as options from '../../utils/options'; +import * as logger from '../../utils/logger'; +import addDependenciesToPackage from '../../utils/addDependenciesToPackages'; import { BoltError } from '../../utils/errors'; +import type { Dependency, configDependencyType } from '../../types'; export type WorkspaceAddOptions = { - cwd?: string + cwd?: string, + workspaceName: string, + deps: Array, + type: configDependencyType +}; + +const depTypeFlags = { + '--dev': 'devDependencies', + '-D': 'devDependencies', + '--peer': 'peerDependencies', + '-P': 'peerDependencies', + '--optional': 'optionalDependencies', + '-O': '--optional' }; export function toWorkspaceAddOptions( args: options.Args, flags: options.Flags ): WorkspaceAddOptions { + let [workspaceName, ...deps] = args; + const depsArgs = []; + let type = 'dependencies'; + + deps.forEach(dep => { + const [name, version] = dep.split('@'); + depsArgs.push(version ? { name, version } : { name }); + }); + + Object.keys(depTypeFlags).forEach(depTypeFlag => { + if (flags[depTypeFlag]) { + type = depTypeFlags[depTypeFlag]; + } + }); + return { - cwd: options.string(flags.cwd, 'cwd') + cwd: options.string(flags.cwd, 'cwd'), + workspaceName, + deps: depsArgs, + type }; } export async function workspaceAdd(opts: WorkspaceAddOptions) { - throw new BoltError('Unimplemented command "workspace add"'); + let cwd = opts.cwd || process.cwd(); + let project = await Project.init(cwd); + let workspaces = await project.getWorkspaces(); + let workspace = await project.getWorkspaceByName( + workspaces, + opts.workspaceName + ); + + if (!workspace) { + throw new BoltError( + `Could not find a workspace named "${opts.workspaceName}" from "${cwd}"` + ); + } + + await addDependenciesToPackage(project, workspace.pkg, opts.deps, opts.type); } diff --git a/src/utils/__tests__/addDependenciesToPackages.test.js b/src/utils/__tests__/addDependenciesToPackages.test.js index 48be4ddd..c6199773 100644 --- a/src/utils/__tests__/addDependenciesToPackages.test.js +++ b/src/utils/__tests__/addDependenciesToPackages.test.js @@ -79,11 +79,7 @@ describe('utils/addDependenciesToPackages', () => { await addDependenciesToPackage(project, project.pkg, [{ name: 'react' }]); - expect(unsafeYarn.add).toHaveBeenCalledWith( - project.pkg, - [], - 'dependencies' - ); + expect(unsafeYarn.add).toHaveBeenCalledTimes(0); }); test('it should throw if version does not match version in project config', async () => { @@ -100,27 +96,45 @@ describe('utils/addDependenciesToPackages', () => { }); test('should call symlinkPackageDependencies to symlink dependencies in workspace', async () => { - const cwd = await copyFixtureIntoTempDir(__dirname, 'nested-workspaces'); + const cwd = await copyFixtureIntoTempDir( + __dirname, + 'package-with-external-deps-installed' + ); const project = await Project.init(cwd); const workspaces = await project.getWorkspaces(); const wsToAddTo = project.getWorkspaceByName(workspaces, 'foo') || {}; - await addDependenciesToPackage(project, wsToAddTo.pkg, [{ name: 'chalk' }]); + await addDependenciesToPackage(project, wsToAddTo.pkg, [ + { name: 'project-only-dep' } + ]); - expect(symlinkSpy).toHaveBeenCalledWith(project, wsToAddTo.pkg, ['chalk']); + expect(symlinkSpy).toHaveBeenCalledWith(project, wsToAddTo.pkg, [ + 'project-only-dep' + ]); }); test('should update packages dependencies in package config', async () => { - const cwd = await copyFixtureIntoTempDir(__dirname, 'nested-workspaces'); + const cwd = await copyFixtureIntoTempDir( + __dirname, + 'package-with-external-deps-installed' + ); const project = await Project.init(cwd); const workspaces = await project.getWorkspaces(); const wsToAddTo = project.getWorkspaceByName(workspaces, 'foo') || {}; - expect(wsToAddTo.pkg.getDependencyVersionRange('chalk')).toEqual(null); - await addDependenciesToPackage(project, wsToAddTo.pkg, [{ name: 'chalk' }]); - - expect(symlinkSpy).toHaveBeenCalledWith(project, wsToAddTo.pkg, ['chalk']); - expect(wsToAddTo.pkg.getDependencyVersionRange('chalk')).toEqual('^1.0.0'); + expect(wsToAddTo.pkg.getDependencyVersionRange('project-only-dep')).toEqual( + null + ); + await addDependenciesToPackage(project, wsToAddTo.pkg, [ + { name: 'project-only-dep' } + ]); + + expect(symlinkSpy).toHaveBeenCalledWith(project, wsToAddTo.pkg, [ + 'project-only-dep' + ]); + expect(wsToAddTo.pkg.getDependencyVersionRange('project-only-dep')).toEqual( + '^1.0.0' + ); }); describe('when adding internal package', () => { @@ -160,11 +174,7 @@ describe('utils/addDependenciesToPackages', () => { expect(project.pkg.getDependencyVersionRange('baz')).toEqual(null); await addDependenciesToPackage(project, wsToAddTo.pkg, [{ name: 'baz' }]); expect(project.pkg.getDependencyVersionRange('baz')).toEqual(null); - expect(unsafeYarn.add).toHaveBeenCalledWith( - project.pkg, - [], - 'dependencies' - ); + expect(unsafeYarn.add).toHaveBeenCalledTimes(0); }); }); }); diff --git a/src/utils/addDependenciesToPackages.js b/src/utils/addDependenciesToPackages.js index 206094fd..e1acaca8 100644 --- a/src/utils/addDependenciesToPackages.js +++ b/src/utils/addDependenciesToPackages.js @@ -27,7 +27,11 @@ export default async function addDependenciesToPackage( const externalDepsToInstallForProject = externalDeps.filter( dep => !projectDependencies.has(dep.name) ); - await yarn.add(project.pkg, externalDepsToInstallForProject, type); + if (externalDepsToInstallForProject.length !== 0) { + await yarn.add(project.pkg, externalDepsToInstallForProject, type); + // we reinitialise the project config because it will be modified externally by yarn + project = await Project.init(project.pkg.dir); + } if (pkg.isSamePackage(project.pkg)) { return true; From 2e3ad9cfa0f0c384101f9150ef68c25178c161dc Mon Sep 17 00:00:00 2001 From: Luke Batchelor Date: Fri, 6 Oct 2017 16:09:49 +1100 Subject: [PATCH 11/14] Adds bolt add command and tests --- src/commands/__tests__/add.test.js | 177 ++++++++++++++++++++++++++++- src/commands/add.js | 48 +++++++- 2 files changed, 220 insertions(+), 5 deletions(-) diff --git a/src/commands/__tests__/add.test.js b/src/commands/__tests__/add.test.js index 845b23d5..97fa1da7 100644 --- a/src/commands/__tests__/add.test.js +++ b/src/commands/__tests__/add.test.js @@ -1,4 +1,179 @@ // @flow import { add, toAddOptions } from '../add'; +import { copyFixtureIntoTempDir } from 'jest-fixtures'; +import * as path from 'path'; +import * as processes from '../../utils/processes'; +import * as yarn from '../../utils/yarn'; +import * as fs from '../../utils/fs'; +import Package from '../../Package'; +import pathExists from 'path-exists'; -test('bolt add'); +jest.mock('../../utils/yarn'); +jest.mock('../../utils/logger'); + +const unsafeProcessses: any & typeof processes = processes; +const unsafeYarn: any & typeof yarn = yarn; + +// Helper method to check if a dependency is installed, both in the package.json and on the fs +async function depIsInstalled(workspaceDir: string, depName: string) { + const pkg = await Package.init(path.join(workspaceDir, 'package.json')); + const dirExists = await pathExists( + path.join(workspaceDir, 'node_modules', depName) + ); + const depInPkgJson = pkg.getDependencyType(depName) !== null; + + return dirExists && depInPkgJson; +} + +// a mock yarn add function to update the pakcages's config and also node_modules dir +async function fakeYarnAdd(pkg: Package, dependencies, type = 'dependencies') { + for (let dep of dependencies) { + await pkg.setDependencyVersionRange( + dep.name, + type, + dep.version || '^1.0.0' + ); + await fs.mkdirp(path.join(pkg.nodeModules, dep.name)); + } +} + +describe('bolt add', () => { + let projectDir; + let fooWorkspaceDir; + let barWorkspaceDir; + + beforeEach(async () => { + projectDir = await copyFixtureIntoTempDir( + __dirname, + 'package-with-external-deps-installed' + ); + fooWorkspaceDir = path.join(projectDir, 'packages', 'foo'); + barWorkspaceDir = path.join(projectDir, 'packages', 'bar'); + yarn.add.mockImplementation(fakeYarnAdd); + }); + + describe('from the project pkg', () => { + test('adding new package', async () => { + expect(await depIsInstalled(projectDir, 'new-dep')).toEqual(false); + await add( + toAddOptions(['new-dep'], { + cwd: projectDir + }) + ); + expect(yarn.add).toHaveBeenCalledTimes(1); + expect(await depIsInstalled(projectDir, 'new-dep')).toEqual(true); + }); + + test('adding existing package', async () => { + expect(await depIsInstalled(projectDir, 'project-only-dep')).toEqual( + true + ); + await add( + toAddOptions(['project-only-dep'], { + cwd: projectDir + }) + ); + expect(yarn.add).toHaveBeenCalledTimes(0); + }); + + test('adding new dev dependency', async () => { + expect(await depIsInstalled(projectDir, 'new-dep')).toEqual(false); + await add( + toAddOptions(['new-dep'], { + dev: true, // equivalent of passing --dev flag + cwd: projectDir + }) + ); + expect(yarn.add).toHaveBeenCalledTimes(1); + expect(yarn.add).toHaveBeenCalledWith( + expect.any(Package), + [{ name: 'new-dep' }], + 'devDependencies' + ); + expect(await depIsInstalled(projectDir, 'new-dep')).toEqual(true); + }); + + test('adding multiple new dependencies', async () => { + expect(await depIsInstalled(projectDir, 'new-dep')).toEqual(false); + expect(await depIsInstalled(projectDir, 'new-dep-2')).toEqual(false); + await add( + toAddOptions(['new-dep', 'new-dep-2'], { + cwd: projectDir + }) + ); + expect(yarn.add).toHaveBeenCalledTimes(1); + expect(yarn.add).toHaveBeenCalledWith( + expect.any(Package), + [{ name: 'new-dep' }, { name: 'new-dep-2' }], + 'dependencies' + ); + expect(await depIsInstalled(projectDir, 'new-dep')).toEqual(true); + expect(await depIsInstalled(projectDir, 'new-dep-2')).toEqual(true); + }); + }); + + describe('from a workspace', () => { + test('adding new package', async () => { + expect(await depIsInstalled(projectDir, 'new-dep')).toEqual(false); + expect(await depIsInstalled(fooWorkspaceDir, 'new-dep')).toEqual(false); + await add( + toAddOptions(['new-dep'], { + cwd: fooWorkspaceDir + }) + ); + expect(yarn.add).toHaveBeenCalledTimes(1); + expect(await depIsInstalled(projectDir, 'new-dep')).toEqual(true); + expect(await depIsInstalled(fooWorkspaceDir, 'new-dep')).toEqual(true); + }); + + test('adding existing package', async () => { + expect(await depIsInstalled(fooWorkspaceDir, 'project-only-dep')).toEqual( + false + ); + await add( + toAddOptions(['project-only-dep'], { + cwd: fooWorkspaceDir + }) + ); + expect(yarn.add).toHaveBeenCalledTimes(0); + expect(await depIsInstalled(fooWorkspaceDir, 'project-only-dep')).toEqual( + true + ); + }); + + test('adding new dev dependency', async () => { + expect(await depIsInstalled(fooWorkspaceDir, 'new-dep')).toEqual(false); + await add( + toAddOptions(['new-dep'], { + dev: true, // equivalent of passing --dev flag + cwd: fooWorkspaceDir + }) + ); + expect(yarn.add).toHaveBeenCalledTimes(1); + expect(yarn.add).toHaveBeenCalledWith( + expect.any(Package), + [{ name: 'new-dep' }], + 'devDependencies' + ); + expect(await depIsInstalled(fooWorkspaceDir, 'new-dep')).toEqual(true); + }); + + test('adding multiple new dependencies', async () => { + expect(await depIsInstalled(fooWorkspaceDir, 'new-dep')).toEqual(false); + expect(await depIsInstalled(fooWorkspaceDir, 'new-dep-2')).toEqual(false); + await add( + toAddOptions(['new-dep', 'new-dep-2'], { + cwd: fooWorkspaceDir + }) + ); + expect(yarn.add).toHaveBeenCalledTimes(1); + expect(yarn.add).toHaveBeenCalledWith( + expect.any(Package), + [{ name: 'new-dep' }, { name: 'new-dep-2' }], + 'dependencies' + ); + expect(await depIsInstalled(fooWorkspaceDir, 'new-dep')).toEqual(true); + expect(await depIsInstalled(fooWorkspaceDir, 'new-dep-2')).toEqual(true); + }); + }); +}); diff --git a/src/commands/add.js b/src/commands/add.js index c56aacf5..a3a8c3db 100644 --- a/src/commands/add.js +++ b/src/commands/add.js @@ -1,16 +1,56 @@ // @flow +import Project from '../Project'; +import Package from '../Package'; import * as options from '../utils/options'; -import { BoltError } from '../utils/errors'; +import * as logger from '../utils/logger'; +import addDependenciesToPackage from '../utils/addDependenciesToPackages'; +import type { Dependency, configDependencyType } from '../types'; -export type AddOptions = {}; +export type AddOptions = { + cwd?: string, + deps: Array, + type: configDependencyType +}; + +const depTypeFlags = { + dev: 'devDependencies', + D: 'devDependencies', + peer: 'peerDependencies', + P: 'peerDependencies', + optional: 'optionalDependencies', + O: '--optional' +}; export function toAddOptions( args: options.Args, flags: options.Flags ): AddOptions { - return {}; + const depsArgs = []; + let type = 'dependencies'; + + // args is each of our dependencies we are adding + args.forEach(dep => { + const [name, version] = dep.split('@'); + depsArgs.push(version ? { name, version } : { name }); + }); + + Object.keys(depTypeFlags).forEach(depTypeFlag => { + if (flags[depTypeFlag]) { + type = depTypeFlags[depTypeFlag]; + } + }); + + return { + cwd: options.string(flags.cwd, 'cwd'), + deps: depsArgs, + type + }; } export async function add(opts: AddOptions) { - throw new BoltError('Unimplemented command "add"'); + let cwd = opts.cwd || process.cwd(); + let project = await Project.init(cwd); + let pkg = await Package.closest(cwd); + + await addDependenciesToPackage(project, pkg, opts.deps, opts.type); } From 2c4ab8b1e59853fadc6c006bc84796b67ff3797f Mon Sep 17 00:00:00 2001 From: Luke Batchelor Date: Fri, 6 Oct 2017 16:19:02 +1100 Subject: [PATCH 12/14] Fixes flow error with unsafeYarn --- src/commands/__tests__/add.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/__tests__/add.test.js b/src/commands/__tests__/add.test.js index 97fa1da7..df29b6d8 100644 --- a/src/commands/__tests__/add.test.js +++ b/src/commands/__tests__/add.test.js @@ -49,7 +49,7 @@ describe('bolt add', () => { ); fooWorkspaceDir = path.join(projectDir, 'packages', 'foo'); barWorkspaceDir = path.join(projectDir, 'packages', 'bar'); - yarn.add.mockImplementation(fakeYarnAdd); + unsafeYarn.add.mockImplementation(fakeYarnAdd); }); describe('from the project pkg', () => { From ade08df39725f96302d04cbb576a358876820f76 Mon Sep 17 00:00:00 2001 From: Luke Batchelor Date: Fri, 6 Oct 2017 16:22:12 +1100 Subject: [PATCH 13/14] Removes unused test fixture --- src/__fixtures__/nested-workspaces-2/package.json | 12 ------------ .../nested-workspaces-2/packages/bar/package.json | 7 ------- .../nested-workspaces-2/packages/foo/package.json | 11 ----------- .../packages/foo/packages/baz/package.json | 8 -------- 4 files changed, 38 deletions(-) delete mode 100644 src/__fixtures__/nested-workspaces-2/package.json delete mode 100644 src/__fixtures__/nested-workspaces-2/packages/bar/package.json delete mode 100644 src/__fixtures__/nested-workspaces-2/packages/foo/package.json delete mode 100644 src/__fixtures__/nested-workspaces-2/packages/foo/packages/baz/package.json diff --git a/src/__fixtures__/nested-workspaces-2/package.json b/src/__fixtures__/nested-workspaces-2/package.json deleted file mode 100644 index cbd2cab3..00000000 --- a/src/__fixtures__/nested-workspaces-2/package.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "private": true, - "version": "1.0.0", - "name": "fixture-project-nested-workspaces", - "bolt": { - "workspaces": ["packages/*"] - }, - "dependencies": { - "react": "^15.6.1", - "left-pad": "^1.1.3" - } -} diff --git a/src/__fixtures__/nested-workspaces-2/packages/bar/package.json b/src/__fixtures__/nested-workspaces-2/packages/bar/package.json deleted file mode 100644 index 18e841eb..00000000 --- a/src/__fixtures__/nested-workspaces-2/packages/bar/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "bar", - "version": "1.0.0", - "dependencies": { - "react": "^15.6.1" - } -} diff --git a/src/__fixtures__/nested-workspaces-2/packages/foo/package.json b/src/__fixtures__/nested-workspaces-2/packages/foo/package.json deleted file mode 100644 index ef1f6028..00000000 --- a/src/__fixtures__/nested-workspaces-2/packages/foo/package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "foo", - "version": "1.0.0", - "bolt": { - "workspaces": ["packages/*"] - }, - "dependencies": { - "react": "^15.6.1", - "bar": "^1.0.0" - } -} diff --git a/src/__fixtures__/nested-workspaces-2/packages/foo/packages/baz/package.json b/src/__fixtures__/nested-workspaces-2/packages/foo/packages/baz/package.json deleted file mode 100644 index 0a69ca46..00000000 --- a/src/__fixtures__/nested-workspaces-2/packages/foo/packages/baz/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "baz", - "version": "1.0.1", - "dependencies": { - "react": "^15.6.1", - "bar": "^1.0.0" - } -} From a19a9db783c2d109ecce7a1f88b7be879f505165 Mon Sep 17 00:00:00 2001 From: Luke Batchelor Date: Mon, 9 Oct 2017 13:29:56 +1100 Subject: [PATCH 14/14] Prevents adding workspace as a project dep and adds more tests to Add command --- src/__tests__/Config.test.js | 23 +++---- src/commands/__tests__/add.test.js | 66 +++++++++++++++++-- src/commands/add.js | 14 +--- src/commands/workspace/add.js | 14 +--- src/constants.js | 9 +++ .../symlinkPackageDependencies.test.js | 5 +- src/utils/addDependenciesToPackages.js | 28 ++++---- src/utils/messages.js | 4 ++ src/utils/symlinkPackageDependencies.js | 1 + src/utils/yarn.js | 17 +++-- 10 files changed, 122 insertions(+), 59 deletions(-) diff --git a/src/__tests__/Config.test.js b/src/__tests__/Config.test.js index b8df92f1..cdb1c4e5 100644 --- a/src/__tests__/Config.test.js +++ b/src/__tests__/Config.test.js @@ -113,17 +113,14 @@ describe('getProjectConfig()', () => { expect(found).toBe(null); }); - it.only( - 'should get the root if in nested package not included in a parent project', - async () => { - let fixturePath = await getFixturePath( - __dirname, - 'simple-project-with-excluded-package', - 'packages', - 'bar' - ); - let found = await Config.getProjectConfig(fixturePath); - expect(found).toBe(path.join(fixturePath, 'package.json')); - } - ); + it('should get the root if in nested package not included in a parent project', async () => { + let fixturePath = await getFixturePath( + __dirname, + 'simple-project-with-excluded-package', + 'packages', + 'bar' + ); + let found = await Config.getProjectConfig(fixturePath); + expect(found).toBe(path.join(fixturePath, 'package.json')); + }); }); diff --git a/src/commands/__tests__/add.test.js b/src/commands/__tests__/add.test.js index df29b6d8..f1984426 100644 --- a/src/commands/__tests__/add.test.js +++ b/src/commands/__tests__/add.test.js @@ -15,14 +15,20 @@ const unsafeProcessses: any & typeof processes = processes; const unsafeYarn: any & typeof yarn = yarn; // Helper method to check if a dependency is installed, both in the package.json and on the fs -async function depIsInstalled(workspaceDir: string, depName: string) { +async function depIsInstalled( + workspaceDir: string, + depName: string, + version?: string +) { const pkg = await Package.init(path.join(workspaceDir, 'package.json')); const dirExists = await pathExists( path.join(workspaceDir, 'node_modules', depName) ); const depInPkgJson = pkg.getDependencyType(depName) !== null; + const correctVersion = + !version || pkg.getDependencyVersionRange(depName) === version; - return dirExists && depInPkgJson; + return dirExists && depInPkgJson && correctVersion; } // a mock yarn add function to update the pakcages's config and also node_modules dir @@ -76,7 +82,7 @@ describe('bolt add', () => { expect(yarn.add).toHaveBeenCalledTimes(0); }); - test('adding new dev dependency', async () => { + test('adding new dependency with --dev flag', async () => { expect(await depIsInstalled(projectDir, 'new-dep')).toEqual(false); await add( toAddOptions(['new-dep'], { @@ -110,6 +116,32 @@ describe('bolt add', () => { expect(await depIsInstalled(projectDir, 'new-dep')).toEqual(true); expect(await depIsInstalled(projectDir, 'new-dep-2')).toEqual(true); }); + + test('adding internal dep (should error)', async () => { + await expect( + add( + toAddOptions(['bar'], { + cwd: projectDir + }) + ) + ).rejects.toBeInstanceOf(Error); + + expect(yarn.add).toHaveBeenCalledTimes(0); + expect(await depIsInstalled(projectDir, 'bar')).toEqual(false); + }); + + test('adding new package at version', async () => { + expect(await depIsInstalled(projectDir, 'new-dep')).toEqual(false); + await add( + toAddOptions(['new-dep@^2.0.0'], { + cwd: projectDir + }) + ); + expect(yarn.add).toHaveBeenCalledTimes(1); + expect(await depIsInstalled(projectDir, 'new-dep', '^2.0.0')).toEqual( + true + ); + }); }); describe('from a workspace', () => { @@ -141,7 +173,7 @@ describe('bolt add', () => { ); }); - test('adding new dev dependency', async () => { + test('adding new dependency with --dev flag', async () => { expect(await depIsInstalled(fooWorkspaceDir, 'new-dep')).toEqual(false); await add( toAddOptions(['new-dep'], { @@ -175,5 +207,31 @@ describe('bolt add', () => { expect(await depIsInstalled(fooWorkspaceDir, 'new-dep')).toEqual(true); expect(await depIsInstalled(fooWorkspaceDir, 'new-dep-2')).toEqual(true); }); + + test('adding internal dep', async () => { + expect(await depIsInstalled(fooWorkspaceDir, 'bar')).toEqual(false); + + await add( + toAddOptions(['bar'], { + cwd: fooWorkspaceDir + }) + ); + + expect(yarn.add).toHaveBeenCalledTimes(0); + expect(await depIsInstalled(fooWorkspaceDir, 'bar')).toEqual(true); + }); + + test('adding new package at version', async () => { + expect(await depIsInstalled(fooWorkspaceDir, 'new-dep')).toEqual(false); + await add( + toAddOptions(['new-dep@^2.0.0'], { + cwd: fooWorkspaceDir + }) + ); + expect(yarn.add).toHaveBeenCalledTimes(1); + expect( + await depIsInstalled(fooWorkspaceDir, 'new-dep', '^2.0.0') + ).toEqual(true); + }); }); }); diff --git a/src/commands/add.js b/src/commands/add.js index a3a8c3db..8975eeee 100644 --- a/src/commands/add.js +++ b/src/commands/add.js @@ -5,6 +5,7 @@ import * as options from '../utils/options'; import * as logger from '../utils/logger'; import addDependenciesToPackage from '../utils/addDependenciesToPackages'; import type { Dependency, configDependencyType } from '../types'; +import { DEPENDENCY_TYPE_FLAGS_MAP } from '../constants'; export type AddOptions = { cwd?: string, @@ -12,15 +13,6 @@ export type AddOptions = { type: configDependencyType }; -const depTypeFlags = { - dev: 'devDependencies', - D: 'devDependencies', - peer: 'peerDependencies', - P: 'peerDependencies', - optional: 'optionalDependencies', - O: '--optional' -}; - export function toAddOptions( args: options.Args, flags: options.Flags @@ -34,9 +26,9 @@ export function toAddOptions( depsArgs.push(version ? { name, version } : { name }); }); - Object.keys(depTypeFlags).forEach(depTypeFlag => { + Object.keys(DEPENDENCY_TYPE_FLAGS_MAP).forEach(depTypeFlag => { if (flags[depTypeFlag]) { - type = depTypeFlags[depTypeFlag]; + type = DEPENDENCY_TYPE_FLAGS_MAP[depTypeFlag]; } }); diff --git a/src/commands/workspace/add.js b/src/commands/workspace/add.js index d9fd6257..2a37fffc 100644 --- a/src/commands/workspace/add.js +++ b/src/commands/workspace/add.js @@ -6,6 +6,7 @@ import * as logger from '../../utils/logger'; import addDependenciesToPackage from '../../utils/addDependenciesToPackages'; import { BoltError } from '../../utils/errors'; import type { Dependency, configDependencyType } from '../../types'; +import { DEPENDENCY_TYPE_FLAGS_MAP } from '../../constants'; export type WorkspaceAddOptions = { cwd?: string, @@ -14,15 +15,6 @@ export type WorkspaceAddOptions = { type: configDependencyType }; -const depTypeFlags = { - '--dev': 'devDependencies', - '-D': 'devDependencies', - '--peer': 'peerDependencies', - '-P': 'peerDependencies', - '--optional': 'optionalDependencies', - '-O': '--optional' -}; - export function toWorkspaceAddOptions( args: options.Args, flags: options.Flags @@ -36,9 +28,9 @@ export function toWorkspaceAddOptions( depsArgs.push(version ? { name, version } : { name }); }); - Object.keys(depTypeFlags).forEach(depTypeFlag => { + Object.keys(DEPENDENCY_TYPE_FLAGS_MAP).forEach(depTypeFlag => { if (flags[depTypeFlag]) { - type = depTypeFlags[depTypeFlag]; + type = DEPENDENCY_TYPE_FLAGS_MAP[depTypeFlag]; } }); diff --git a/src/constants.js b/src/constants.js index 7ce4f652..9aae3b5a 100644 --- a/src/constants.js +++ b/src/constants.js @@ -7,3 +7,12 @@ export const DEPENDENCY_TYPES = [ 'bundledDependencies', 'optionalDependencies' ]; + +export const DEPENDENCY_TYPE_FLAGS_MAP = { + dev: 'devDependencies', + peer: 'peerDependencies', + optional: 'optionalDependencies', + D: 'devDependencies', + P: 'peerDependencies', + O: 'optionalDependencies' +}; diff --git a/src/utils/__tests__/symlinkPackageDependencies.test.js b/src/utils/__tests__/symlinkPackageDependencies.test.js index c4c3f0e3..facd1c2f 100644 --- a/src/utils/__tests__/symlinkPackageDependencies.test.js +++ b/src/utils/__tests__/symlinkPackageDependencies.test.js @@ -91,14 +91,15 @@ describe('utils/symlinkPackageDependencies()', () => { expect(await symlinkExists(nodeModules, 'bar')).toEqual(true); }); - it('should run preinstall, postinstall and prepublish scripts', async () => { + it('should run correct lifecycle scripts', async () => { await symlinkPackageDependencies(project, pkgToSymlink, ['bar']); - expect(yarn.run).toHaveBeenCalledTimes(3); + expect(yarn.run).toHaveBeenCalledTimes(4); const yarnCalls = unsafeYarn.run.mock.calls; expect(yarnCalls[0][1]).toEqual('preinstall'); expect(yarnCalls[1][1]).toEqual('postinstall'); expect(yarnCalls[2][1]).toEqual('prepublish'); + expect(yarnCalls[3][1]).toEqual('prepare'); }); }); diff --git a/src/utils/addDependenciesToPackages.js b/src/utils/addDependenciesToPackages.js index e1acaca8..285a0e76 100644 --- a/src/utils/addDependenciesToPackages.js +++ b/src/utils/addDependenciesToPackages.js @@ -5,6 +5,7 @@ import type Workspace from '../Workspace'; import type Package from '../Package'; import type { Dependency, configDependencyType } from '../types'; import * as messages from './messages'; +import { BoltError } from './errors'; import * as logger from './logger'; import * as yarn from './yarn'; import symlinkPackageDependencies from './symlinkPackageDependencies'; @@ -34,6 +35,11 @@ export default async function addDependenciesToPackage( } if (pkg.isSamePackage(project.pkg)) { + if (internalDeps.length > 0) { + throw new BoltError( + messages.cannotInstallWorkspaceInProject(internalDeps[0].name) + ); + } return true; } @@ -41,35 +47,35 @@ export default async function addDependenciesToPackage( for (let dep of externalDeps) { const installed = project.pkg.getDependencyVersionRange(dep.name); - if (dep.version && dep.version !== installed) { - logger.warn( + const depVersion = dep.version; + if (depVersion && depVersion !== installed) { + throw new BoltError( messages.depMustMatchProject( pkg.config.getName(), dep.name, installed, - String(dep.version) + depVersion ) ); - throw new Error(); } installedVersions[dep.name] = String(installed); } for (let dep of internalDeps) { const dependencyPkg = (depGraph.get(dep.name) || {}).pkg; - const curVersion = dependencyPkg.config.getVersion(); - if (dep.version) { - logger.warn( + const requestedVersion = dep.version; + const internalVersion = dependencyPkg.config.getVersion(); + if (requestedVersion && requestedVersion !== internalVersion) { + throw new BoltError( messages.packageMustDependOnCurrentVersion( pkg.config.getName(), dep.name, - curVersion, - String(dep.version) + internalVersion, + requestedVersion ) ); - throw new Error(); } - installedVersions[dep.name] = `^${curVersion}`; + installedVersions[dep.name] = `^${internalVersion}`; } for (let [depName, depVersion] of Object.entries(installedVersions)) { diff --git a/src/utils/messages.js b/src/utils/messages.js index 09462046..c578505f 100644 --- a/src/utils/messages.js +++ b/src/utils/messages.js @@ -255,3 +255,7 @@ export function couldNotBeNormalized(): Message { export function installedAndLinkedWorkspaces(): Message { return 'Installed and linked workspaces.'; } + +export function cannotInstallWorkspaceInProject(pkgName: string): Message { + return `Cannot install workspace "${pkgName}" as a dependency of a project`; +} diff --git a/src/utils/symlinkPackageDependencies.js b/src/utils/symlinkPackageDependencies.js index 55c38f91..39ac829f 100644 --- a/src/utils/symlinkPackageDependencies.js +++ b/src/utils/symlinkPackageDependencies.js @@ -216,4 +216,5 @@ export default async function symlinkPackageDependencies( await yarn.run(pkg, 'postinstall'); await yarn.run(pkg, 'prepublish'); + await yarn.run(pkg, 'prepare'); } diff --git a/src/utils/yarn.js b/src/utils/yarn.js index e6e9de11..93df4849 100644 --- a/src/utils/yarn.js +++ b/src/utils/yarn.js @@ -6,6 +6,15 @@ import type Package from '../Package'; import * as processes from './processes'; import * as fs from '../utils/fs'; import * as logger from '../utils/fs'; +import { DEPENDENCY_TYPE_FLAGS_MAP } from '../constants'; + +function depTypeToFlag(depType) { + const flag = Object.keys(DEPENDENCY_TYPE_FLAGS_MAP).find( + key => DEPENDENCY_TYPE_FLAGS_MAP[key] === depType + ); + + return flag ? `--${flag}` : flag; +} export async function add( pkg: Package, @@ -24,13 +33,7 @@ export async function add( }); if (type) { - const typeToFlagMap = { - dependencies: '', - devDependencies: '--dev', - peerDependencies: '--peer', - optionalDependencies: '--optional' - }; - const flag = typeToFlagMap[type]; + const flag = depTypeToFlag(type); if (flag) spawnArgs.push(flag); }