From 399ff8cbccd5198f637518ccafa86c43bab47a4a Mon Sep 17 00:00:00 2001 From: isaacs Date: Wed, 26 May 2021 14:16:36 -0700 Subject: [PATCH] feat(link): add workspace support PR-URL: https://github.com/npm/cli/pull/3312 Credit: @isaacs Close: #3312 Reviewed-by: @wraithgar --- docs/content/commands/npm-link.md | 42 +++++ lib/base-command.js | 1 + lib/link.js | 11 +- lib/workspaces/arborist-cmd.js | 1 + tap-snapshots/test/lib/link.js.test.cjs | 15 ++ .../test/lib/load-all-commands.js.test.cjs | 2 + .../test/lib/utils/npm-usage.js.test.cjs | 2 + test/lib/link.js | 172 ++++++++++++++++++ 8 files changed, 243 insertions(+), 3 deletions(-) diff --git a/docs/content/commands/npm-link.md b/docs/content/commands/npm-link.md index e48be396ade1b..b1c6066768a99 100644 --- a/docs/content/commands/npm-link.md +++ b/docs/content/commands/npm-link.md @@ -99,6 +99,16 @@ relevant metadata by running `npm install --package-lock-only`. If you _want_ to save the `file:` reference in your `package.json` and `package-lock.json` files, you can use `npm link --save` to do so. +### Workspace Usage + +`npm link --workspace ` will link the relevant package as a +dependency of the specified workspace(s). Note that It may actually be +linked into the parent project's `node_modules` folder, if there are no +conflicting dependencies. + +`npm link --workspace ` will create a global link to the specified +workspace(s). + ### Configuration @@ -261,6 +271,38 @@ commands that modify your local installation, eg, `install`, `update`, Note: This is NOT honored by other network related commands, eg `dist-tags`, `owner`, etc. +#### `workspace` + +* Default: +* Type: String (can be set multiple times) + +Enable running a command in the context of the configured workspaces of the +current project while filtering by running only the workspaces defined by +this configuration option. + +Valid values for the `workspace` config are either: + +* Workspace names +* Path to a workspace directory +* Path to a parent workspace directory (will result to selecting all of the + nested workspaces) + +When set for the `npm init` command, this may be set to the folder of a +workspace which does not yet exist, to create the folder and set it up as a +brand new workspace within the project. + +This value is not exported to the environment for child processes. + +#### `workspaces` + +* Default: false +* Type: Boolean + +Enable running a command in the context of **all** the configured +workspaces. + +This value is not exported to the environment for child processes. + ### See Also diff --git a/lib/base-command.js b/lib/base-command.js index e1efcff5832b3..843fb2d4b1358 100644 --- a/lib/base-command.js +++ b/lib/base-command.js @@ -7,6 +7,7 @@ class BaseCommand { this.wrapWidth = 80 this.npm = npm this.workspaces = null + this.workspacePaths = null } get name () { diff --git a/lib/link.js b/lib/link.js index 60665a9964fab..47fe4b17a272b 100644 --- a/lib/link.js +++ b/lib/link.js @@ -10,8 +10,8 @@ const semver = require('semver') const reifyFinish = require('./utils/reify-finish.js') -const BaseCommand = require('./base-command.js') -class Link extends BaseCommand { +const ArboristWorkspaceCmd = require('./workspaces/arborist-cmd.js') +class Link extends ArboristWorkspaceCmd { /* istanbul ignore next - see test/lib/load-all-commands.js */ static get description () { return 'Symlink a package folder' @@ -46,6 +46,7 @@ class Link extends BaseCommand { 'bin-links', 'fund', 'dry-run', + ...super.params, ] } @@ -143,12 +144,16 @@ class Link extends BaseCommand { log: this.npm.log, add: names.map(l => `file:${resolve(globalTop, 'node_modules', l)}`), save, + workspaces: this.workspaces, }) await reifyFinish(this.npm, localArb) } async linkPkg () { + const wsp = this.workspacePaths + const paths = wsp && wsp.length ? wsp : [this.npm.prefix] + const add = paths.map(path => `file:${path}`) const globalTop = resolve(this.npm.globalDir, '..') const arb = new Arborist({ ...this.npm.flatOptions, @@ -157,7 +162,7 @@ class Link extends BaseCommand { global: true, }) await arb.reify({ - add: [`file:${this.npm.prefix}`], + add, log: this.npm.log, }) await reifyFinish(this.npm, arb) diff --git a/lib/workspaces/arborist-cmd.js b/lib/workspaces/arborist-cmd.js index b12c0ee47b19f..337e7f9d8f932 100644 --- a/lib/workspaces/arborist-cmd.js +++ b/lib/workspaces/arborist-cmd.js @@ -17,6 +17,7 @@ class ArboristCmd extends BaseCommand { getWorkspaces(filters, { path: this.npm.localPrefix }) .then(workspaces => { this.workspaces = [...workspaces.keys()] + this.workspacePaths = [...workspaces.values()] this.exec(args, cb) }) .catch(er => cb(er)) diff --git a/tap-snapshots/test/lib/link.js.test.cjs b/tap-snapshots/test/lib/link.js.test.cjs index d6dd376593b4d..0e20bcd994e3a 100644 --- a/tap-snapshots/test/lib/link.js.test.cjs +++ b/tap-snapshots/test/lib/link.js.test.cjs @@ -14,6 +14,16 @@ exports[`test/lib/link.js TAP link global linked pkg to local nm when using args ` +exports[`test/lib/link.js TAP link global linked pkg to local workspace using args > should create a local symlink to global pkg 1`] = ` +{CWD}/test/lib/tap-testdir-link-link-global-linked-pkg-to-local-workspace-using-args/my-project/node_modules/@myscope/bar -> {CWD}/test/lib/tap-testdir-link-link-global-linked-pkg-to-local-workspace-using-args/global-prefix/lib/node_modules/@myscope/bar +{CWD}/test/lib/tap-testdir-link-link-global-linked-pkg-to-local-workspace-using-args/my-project/node_modules/@myscope/linked -> {CWD}/test/lib/tap-testdir-link-link-global-linked-pkg-to-local-workspace-using-args/scoped-linked +{CWD}/test/lib/tap-testdir-link-link-global-linked-pkg-to-local-workspace-using-args/my-project/node_modules/a -> {CWD}/test/lib/tap-testdir-link-link-global-linked-pkg-to-local-workspace-using-args/global-prefix/lib/node_modules/a +{CWD}/test/lib/tap-testdir-link-link-global-linked-pkg-to-local-workspace-using-args/my-project/node_modules/link-me-too -> {CWD}/test/lib/tap-testdir-link-link-global-linked-pkg-to-local-workspace-using-args/link-me-too +{CWD}/test/lib/tap-testdir-link-link-global-linked-pkg-to-local-workspace-using-args/my-project/node_modules/test-pkg-link -> {CWD}/test/lib/tap-testdir-link-link-global-linked-pkg-to-local-workspace-using-args/test-pkg-link +{CWD}/test/lib/tap-testdir-link-link-global-linked-pkg-to-local-workspace-using-args/my-project/node_modules/x -> {CWD}/test/lib/tap-testdir-link-link-global-linked-pkg-to-local-workspace-using-args/my-project/packages/x + +` + exports[`test/lib/link.js TAP link pkg already in global space > should create a local symlink to global pkg 1`] = ` {CWD}/test/lib/tap-testdir-link-link-pkg-already-in-global-space/my-project/node_modules/@myscope/linked -> {CWD}/test/lib/tap-testdir-link-link-pkg-already-in-global-space/scoped-linked @@ -28,3 +38,8 @@ exports[`test/lib/link.js TAP link to globalDir when in current working dir of p {CWD}/test/lib/tap-testdir-link-link-to-globalDir-when-in-current-working-dir-of-pkg-and-no-args/global-prefix/lib/node_modules/test-pkg-link -> {CWD}/test/lib/tap-testdir-link-link-to-globalDir-when-in-current-working-dir-of-pkg-and-no-args/test-pkg-link ` + +exports[`test/lib/link.js TAP link ws to globalDir when workspace specified and no args > should create a global link to current pkg 1`] = ` +{CWD}/test/lib/tap-testdir-link-link-ws-to-globalDir-when-workspace-specified-and-no-args/global-prefix/lib/node_modules/a -> {CWD}/test/lib/tap-testdir-link-link-ws-to-globalDir-when-workspace-specified-and-no-args/test-pkg-link/packages/a + +` diff --git a/tap-snapshots/test/lib/load-all-commands.js.test.cjs b/tap-snapshots/test/lib/load-all-commands.js.test.cjs index f2d40d4161494..d40be42868184 100644 --- a/tap-snapshots/test/lib/load-all-commands.js.test.cjs +++ b/tap-snapshots/test/lib/load-all-commands.js.test.cjs @@ -521,6 +521,8 @@ Options: [--strict-peer-deps] [--package-lock] [--omit [--omit ...]] [--ignore-scripts] [--audit] [--bin-links] [--fund] [--dry-run] +[-w|--workspace [-w|--workspace ...]] +[-ws|--workspaces] alias: ln diff --git a/tap-snapshots/test/lib/utils/npm-usage.js.test.cjs b/tap-snapshots/test/lib/utils/npm-usage.js.test.cjs index a81a8f44b30b9..7fdcf0c5d2dba 100644 --- a/tap-snapshots/test/lib/utils/npm-usage.js.test.cjs +++ b/tap-snapshots/test/lib/utils/npm-usage.js.test.cjs @@ -624,6 +624,8 @@ All commands: [--strict-peer-deps] [--package-lock] [--omit [--omit ...]] [--ignore-scripts] [--audit] [--bin-links] [--fund] [--dry-run] + [-w|--workspace [-w|--workspace ...]] + [-ws|--workspaces] alias: ln diff --git a/test/lib/link.js b/test/lib/link.js index d3e66185280ae..3cad0ff90362d 100644 --- a/test/lib/link.js +++ b/test/lib/link.js @@ -84,6 +84,60 @@ t.test('link to globalDir when in current working dir of pkg and no args', (t) = }) }) +t.test('link ws to globalDir when workspace specified and no args', (t) => { + t.plan(2) + + const testdir = t.testdir({ + 'global-prefix': { + lib: { + node_modules: { + a: { + 'package.json': JSON.stringify({ + name: 'a', + version: '1.0.0', + }), + }, + }, + }, + }, + 'test-pkg-link': { + 'package.json': JSON.stringify({ + name: 'test-pkg-link', + version: '1.0.0', + workspaces: ['packages/*'], + }), + packages: { + a: { + 'package.json': JSON.stringify({ + name: 'a', + version: '1.0.0', + }), + }, + }, + }, + }) + npm.globalDir = resolve(testdir, 'global-prefix', 'lib', 'node_modules') + npm.prefix = resolve(testdir, 'test-pkg-link') + npm.localPrefix = resolve(testdir, 'test-pkg-link') + + reifyOutput = async () => { + reifyOutput = undefined + + const links = await printLinks({ + path: resolve(npm.globalDir, '..'), + global: true, + }) + + t.matchSnapshot(links, 'should create a global link to current pkg') + } + + // link.workspaces = ['a'] + // link.workspacePaths = [resolve(testdir, 'test-pkg-link/packages/a')] + link.execWorkspaces([], ['a'], (err) => { + t.error(err, 'should not error out') + }) +}) + t.test('link global linked pkg to local nm when using args', (t) => { t.plan(2) @@ -192,6 +246,124 @@ t.test('link global linked pkg to local nm when using args', (t) => { }) }) +t.test('link global linked pkg to local workspace using args', (t) => { + t.plan(2) + + const testdir = t.testdir({ + 'global-prefix': { + lib: { + node_modules: { + '@myscope': { + foo: { + 'package.json': JSON.stringify({ + name: '@myscope/foo', + version: '1.0.0', + }), + }, + bar: { + 'package.json': JSON.stringify({ + name: '@myscope/bar', + version: '1.0.0', + }), + }, + linked: t.fixture('symlink', '../../../../scoped-linked'), + }, + a: { + 'package.json': JSON.stringify({ + name: 'a', + version: '1.0.0', + }), + }, + b: { + 'package.json': JSON.stringify({ + name: 'b', + version: '1.0.0', + }), + }, + 'test-pkg-link': t.fixture('symlink', '../../../test-pkg-link'), + }, + }, + }, + 'test-pkg-link': { + 'package.json': JSON.stringify({ + name: 'test-pkg-link', + version: '1.0.0', + }), + }, + 'link-me-too': { + 'package.json': JSON.stringify({ + name: 'link-me-too', + version: '1.0.0', + }), + }, + 'scoped-linked': { + 'package.json': JSON.stringify({ + name: '@myscope/linked', + version: '1.0.0', + }), + }, + 'my-project': { + 'package.json': JSON.stringify({ + name: 'my-project', + version: '1.0.0', + workspaces: ['packages/*'], + }), + packages: { + x: { + 'package.json': JSON.stringify({ + name: 'x', + version: '1.0.0', + dependencies: { + foo: '^1.0.0', + }, + }), + }, + }, + node_modules: { + foo: { + 'package.json': JSON.stringify({ + name: 'foo', + version: '1.0.0', + }), + }, + }, + }, + }) + npm.globalDir = resolve(testdir, 'global-prefix', 'lib', 'node_modules') + npm.prefix = resolve(testdir, 'my-project') + npm.localPrefix = resolve(testdir, 'my-project') + + const _cwd = process.cwd() + process.chdir(npm.prefix) + + reifyOutput = async () => { + reifyOutput = undefined + process.chdir(_cwd) + + const links = await printLinks({ + path: npm.prefix, + }) + + t.matchSnapshot(links, 'should create a local symlink to global pkg') + } + + // installs examples for: + // - test-pkg-link: pkg linked to globalDir from local fs + // - @myscope/linked: scoped pkg linked to globalDir from local fs + // - @myscope/bar: prev installed scoped package available in globalDir + // - a: prev installed package available in globalDir + // - file:./link-me-too: pkg that needs to be reified in globalDir first + link.execWorkspaces([ + 'test-pkg-link', + '@myscope/linked', + '@myscope/bar', + 'a', + 'file:../link-me-too', + ], ['x'], (err) => { + t.error(err, 'should not error out') + }) +}) + t.test('link pkg already in global space', (t) => { t.plan(3)