Skip to content

Commit 399ff8c

Browse files
isaacswraithgar
authored andcommitted
feat(link): add workspace support
PR-URL: #3312 Credit: @isaacs Close: #3312 Reviewed-by: @wraithgar
1 parent 96367f9 commit 399ff8c

File tree

8 files changed

+243
-3
lines changed

8 files changed

+243
-3
lines changed

docs/content/commands/npm-link.md

+42
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,16 @@ relevant metadata by running `npm install <dep> --package-lock-only`.
9999
If you _want_ to save the `file:` reference in your `package.json` and
100100
`package-lock.json` files, you can use `npm link <dep> --save` to do so.
101101

102+
### Workspace Usage
103+
104+
`npm link <pkg> --workspace <name>` will link the relevant package as a
105+
dependency of the specified workspace(s). Note that It may actually be
106+
linked into the parent project's `node_modules` folder, if there are no
107+
conflicting dependencies.
108+
109+
`npm link --workspace <name>` will create a global link to the specified
110+
workspace(s).
111+
102112
### Configuration
103113

104114
<!-- AUTOGENERATED CONFIG DESCRIPTIONS START -->
@@ -261,6 +271,38 @@ commands that modify your local installation, eg, `install`, `update`,
261271
Note: This is NOT honored by other network related commands, eg `dist-tags`,
262272
`owner`, etc.
263273

274+
#### `workspace`
275+
276+
* Default:
277+
* Type: String (can be set multiple times)
278+
279+
Enable running a command in the context of the configured workspaces of the
280+
current project while filtering by running only the workspaces defined by
281+
this configuration option.
282+
283+
Valid values for the `workspace` config are either:
284+
285+
* Workspace names
286+
* Path to a workspace directory
287+
* Path to a parent workspace directory (will result to selecting all of the
288+
nested workspaces)
289+
290+
When set for the `npm init` command, this may be set to the folder of a
291+
workspace which does not yet exist, to create the folder and set it up as a
292+
brand new workspace within the project.
293+
294+
This value is not exported to the environment for child processes.
295+
296+
#### `workspaces`
297+
298+
* Default: false
299+
* Type: Boolean
300+
301+
Enable running a command in the context of **all** the configured
302+
workspaces.
303+
304+
This value is not exported to the environment for child processes.
305+
264306
<!-- AUTOGENERATED CONFIG DESCRIPTIONS END -->
265307

266308
### See Also

lib/base-command.js

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ class BaseCommand {
77
this.wrapWidth = 80
88
this.npm = npm
99
this.workspaces = null
10+
this.workspacePaths = null
1011
}
1112

1213
get name () {

lib/link.js

+8-3
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ const semver = require('semver')
1010

1111
const reifyFinish = require('./utils/reify-finish.js')
1212

13-
const BaseCommand = require('./base-command.js')
14-
class Link extends BaseCommand {
13+
const ArboristWorkspaceCmd = require('./workspaces/arborist-cmd.js')
14+
class Link extends ArboristWorkspaceCmd {
1515
/* istanbul ignore next - see test/lib/load-all-commands.js */
1616
static get description () {
1717
return 'Symlink a package folder'
@@ -46,6 +46,7 @@ class Link extends BaseCommand {
4646
'bin-links',
4747
'fund',
4848
'dry-run',
49+
...super.params,
4950
]
5051
}
5152

@@ -143,12 +144,16 @@ class Link extends BaseCommand {
143144
log: this.npm.log,
144145
add: names.map(l => `file:${resolve(globalTop, 'node_modules', l)}`),
145146
save,
147+
workspaces: this.workspaces,
146148
})
147149

148150
await reifyFinish(this.npm, localArb)
149151
}
150152

151153
async linkPkg () {
154+
const wsp = this.workspacePaths
155+
const paths = wsp && wsp.length ? wsp : [this.npm.prefix]
156+
const add = paths.map(path => `file:${path}`)
152157
const globalTop = resolve(this.npm.globalDir, '..')
153158
const arb = new Arborist({
154159
...this.npm.flatOptions,
@@ -157,7 +162,7 @@ class Link extends BaseCommand {
157162
global: true,
158163
})
159164
await arb.reify({
160-
add: [`file:${this.npm.prefix}`],
165+
add,
161166
log: this.npm.log,
162167
})
163168
await reifyFinish(this.npm, arb)

lib/workspaces/arborist-cmd.js

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ class ArboristCmd extends BaseCommand {
1717
getWorkspaces(filters, { path: this.npm.localPrefix })
1818
.then(workspaces => {
1919
this.workspaces = [...workspaces.keys()]
20+
this.workspacePaths = [...workspaces.values()]
2021
this.exec(args, cb)
2122
})
2223
.catch(er => cb(er))

tap-snapshots/test/lib/link.js.test.cjs

+15
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,16 @@ exports[`test/lib/link.js TAP link global linked pkg to local nm when using args
1414
1515
`
1616

17+
exports[`test/lib/link.js TAP link global linked pkg to local workspace using args > should create a local symlink to global pkg 1`] = `
18+
{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
19+
{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
20+
{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
21+
{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
22+
{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
23+
{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
24+
25+
`
26+
1727
exports[`test/lib/link.js TAP link pkg already in global space > should create a local symlink to global pkg 1`] = `
1828
{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
1929
@@ -28,3 +38,8 @@ exports[`test/lib/link.js TAP link to globalDir when in current working dir of p
2838
{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
2939
3040
`
41+
42+
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`] = `
43+
{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
44+
45+
`

tap-snapshots/test/lib/load-all-commands.js.test.cjs

+2
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,8 @@ Options:
521521
[--strict-peer-deps] [--package-lock]
522522
[--omit <dev|optional|peer> [--omit <dev|optional|peer> ...]] [--ignore-scripts]
523523
[--audit] [--bin-links] [--fund] [--dry-run]
524+
[-w|--workspace <workspace-name> [-w|--workspace <workspace-name> ...]]
525+
[-ws|--workspaces]
524526
525527
alias: ln
526528

tap-snapshots/test/lib/utils/npm-usage.js.test.cjs

+2
Original file line numberDiff line numberDiff line change
@@ -624,6 +624,8 @@ All commands:
624624
[--strict-peer-deps] [--package-lock]
625625
[--omit <dev|optional|peer> [--omit <dev|optional|peer> ...]] [--ignore-scripts]
626626
[--audit] [--bin-links] [--fund] [--dry-run]
627+
[-w|--workspace <workspace-name> [-w|--workspace <workspace-name> ...]]
628+
[-ws|--workspaces]
627629
628630
alias: ln
629631

test/lib/link.js

+172
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,60 @@ t.test('link to globalDir when in current working dir of pkg and no args', (t) =
8484
})
8585
})
8686

87+
t.test('link ws to globalDir when workspace specified and no args', (t) => {
88+
t.plan(2)
89+
90+
const testdir = t.testdir({
91+
'global-prefix': {
92+
lib: {
93+
node_modules: {
94+
a: {
95+
'package.json': JSON.stringify({
96+
name: 'a',
97+
version: '1.0.0',
98+
}),
99+
},
100+
},
101+
},
102+
},
103+
'test-pkg-link': {
104+
'package.json': JSON.stringify({
105+
name: 'test-pkg-link',
106+
version: '1.0.0',
107+
workspaces: ['packages/*'],
108+
}),
109+
packages: {
110+
a: {
111+
'package.json': JSON.stringify({
112+
name: 'a',
113+
version: '1.0.0',
114+
}),
115+
},
116+
},
117+
},
118+
})
119+
npm.globalDir = resolve(testdir, 'global-prefix', 'lib', 'node_modules')
120+
npm.prefix = resolve(testdir, 'test-pkg-link')
121+
npm.localPrefix = resolve(testdir, 'test-pkg-link')
122+
123+
reifyOutput = async () => {
124+
reifyOutput = undefined
125+
126+
const links = await printLinks({
127+
path: resolve(npm.globalDir, '..'),
128+
global: true,
129+
})
130+
131+
t.matchSnapshot(links, 'should create a global link to current pkg')
132+
}
133+
134+
// link.workspaces = ['a']
135+
// link.workspacePaths = [resolve(testdir, 'test-pkg-link/packages/a')]
136+
link.execWorkspaces([], ['a'], (err) => {
137+
t.error(err, 'should not error out')
138+
})
139+
})
140+
87141
t.test('link global linked pkg to local nm when using args', (t) => {
88142
t.plan(2)
89143

@@ -192,6 +246,124 @@ t.test('link global linked pkg to local nm when using args', (t) => {
192246
})
193247
})
194248

249+
t.test('link global linked pkg to local workspace using args', (t) => {
250+
t.plan(2)
251+
252+
const testdir = t.testdir({
253+
'global-prefix': {
254+
lib: {
255+
node_modules: {
256+
'@myscope': {
257+
foo: {
258+
'package.json': JSON.stringify({
259+
name: '@myscope/foo',
260+
version: '1.0.0',
261+
}),
262+
},
263+
bar: {
264+
'package.json': JSON.stringify({
265+
name: '@myscope/bar',
266+
version: '1.0.0',
267+
}),
268+
},
269+
linked: t.fixture('symlink', '../../../../scoped-linked'),
270+
},
271+
a: {
272+
'package.json': JSON.stringify({
273+
name: 'a',
274+
version: '1.0.0',
275+
}),
276+
},
277+
b: {
278+
'package.json': JSON.stringify({
279+
name: 'b',
280+
version: '1.0.0',
281+
}),
282+
},
283+
'test-pkg-link': t.fixture('symlink', '../../../test-pkg-link'),
284+
},
285+
},
286+
},
287+
'test-pkg-link': {
288+
'package.json': JSON.stringify({
289+
name: 'test-pkg-link',
290+
version: '1.0.0',
291+
}),
292+
},
293+
'link-me-too': {
294+
'package.json': JSON.stringify({
295+
name: 'link-me-too',
296+
version: '1.0.0',
297+
}),
298+
},
299+
'scoped-linked': {
300+
'package.json': JSON.stringify({
301+
name: '@myscope/linked',
302+
version: '1.0.0',
303+
}),
304+
},
305+
'my-project': {
306+
'package.json': JSON.stringify({
307+
name: 'my-project',
308+
version: '1.0.0',
309+
workspaces: ['packages/*'],
310+
}),
311+
packages: {
312+
x: {
313+
'package.json': JSON.stringify({
314+
name: 'x',
315+
version: '1.0.0',
316+
dependencies: {
317+
foo: '^1.0.0',
318+
},
319+
}),
320+
},
321+
},
322+
node_modules: {
323+
foo: {
324+
'package.json': JSON.stringify({
325+
name: 'foo',
326+
version: '1.0.0',
327+
}),
328+
},
329+
},
330+
},
331+
})
332+
npm.globalDir = resolve(testdir, 'global-prefix', 'lib', 'node_modules')
333+
npm.prefix = resolve(testdir, 'my-project')
334+
npm.localPrefix = resolve(testdir, 'my-project')
335+
336+
const _cwd = process.cwd()
337+
process.chdir(npm.prefix)
338+
339+
reifyOutput = async () => {
340+
reifyOutput = undefined
341+
process.chdir(_cwd)
342+
343+
const links = await printLinks({
344+
path: npm.prefix,
345+
})
346+
347+
t.matchSnapshot(links, 'should create a local symlink to global pkg')
348+
}
349+
350+
// installs examples for:
351+
// - test-pkg-link: pkg linked to globalDir from local fs
352+
// - @myscope/linked: scoped pkg linked to globalDir from local fs
353+
// - @myscope/bar: prev installed scoped package available in globalDir
354+
// - a: prev installed package available in globalDir
355+
// - file:./link-me-too: pkg that needs to be reified in globalDir first
356+
link.execWorkspaces([
357+
'test-pkg-link',
358+
'@myscope/linked',
359+
'@myscope/bar',
360+
'a',
361+
'file:../link-me-too',
362+
], ['x'], (err) => {
363+
t.error(err, 'should not error out')
364+
})
365+
})
366+
195367
t.test('link pkg already in global space', (t) => {
196368
t.plan(3)
197369

0 commit comments

Comments
 (0)