Skip to content

Commit

Permalink
feat(link): add workspace support
Browse files Browse the repository at this point in the history
PR-URL: #3312
Credit: @isaacs
Close: #3312
Reviewed-by: @wraithgar
  • Loading branch information
isaacs authored and wraithgar committed May 26, 2021
1 parent 96367f9 commit 399ff8c
Show file tree
Hide file tree
Showing 8 changed files with 243 additions and 3 deletions.
42 changes: 42 additions & 0 deletions docs/content/commands/npm-link.md
Expand Up @@ -99,6 +99,16 @@ relevant metadata by running `npm install <dep> --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 <dep> --save` to do so.

### Workspace Usage

`npm link <pkg> --workspace <name>` 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 <name>` will create a global link to the specified
workspace(s).

### Configuration

<!-- AUTOGENERATED CONFIG DESCRIPTIONS START -->
Expand Down Expand Up @@ -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.

<!-- AUTOGENERATED CONFIG DESCRIPTIONS END -->

### See Also
Expand Down
1 change: 1 addition & 0 deletions lib/base-command.js
Expand Up @@ -7,6 +7,7 @@ class BaseCommand {
this.wrapWidth = 80
this.npm = npm
this.workspaces = null
this.workspacePaths = null
}

get name () {
Expand Down
11 changes: 8 additions & 3 deletions lib/link.js
Expand Up @@ -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'
Expand Down Expand Up @@ -46,6 +46,7 @@ class Link extends BaseCommand {
'bin-links',
'fund',
'dry-run',
...super.params,
]
}

Expand Down Expand Up @@ -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,
Expand All @@ -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)
Expand Down
1 change: 1 addition & 0 deletions lib/workspaces/arborist-cmd.js
Expand Up @@ -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))
Expand Down
15 changes: 15 additions & 0 deletions tap-snapshots/test/lib/link.js.test.cjs
Expand Up @@ -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
Expand All @@ -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
`
2 changes: 2 additions & 0 deletions tap-snapshots/test/lib/load-all-commands.js.test.cjs
Expand Up @@ -521,6 +521,8 @@ Options:
[--strict-peer-deps] [--package-lock]
[--omit <dev|optional|peer> [--omit <dev|optional|peer> ...]] [--ignore-scripts]
[--audit] [--bin-links] [--fund] [--dry-run]
[-w|--workspace <workspace-name> [-w|--workspace <workspace-name> ...]]
[-ws|--workspaces]
alias: ln
Expand Down
2 changes: 2 additions & 0 deletions tap-snapshots/test/lib/utils/npm-usage.js.test.cjs
Expand Up @@ -624,6 +624,8 @@ All commands:
[--strict-peer-deps] [--package-lock]
[--omit <dev|optional|peer> [--omit <dev|optional|peer> ...]] [--ignore-scripts]
[--audit] [--bin-links] [--fund] [--dry-run]
[-w|--workspace <workspace-name> [-w|--workspace <workspace-name> ...]]
[-ws|--workspaces]
alias: ln
Expand Down
172 changes: 172 additions & 0 deletions test/lib/link.js
Expand Up @@ -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)

Expand Down Expand Up @@ -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)

Expand Down

0 comments on commit 399ff8c

Please sign in to comment.