Skip to content

Commit ed9a207

Browse files
committed
fix: reenable npm link from registry
Being able to npm link a package that is not currently available in the global space should still be a supported feature, this change puts that functionality back in place but also improves it by avoiding reify any package that may already be found in the global directory.
1 parent 174703e commit ed9a207

File tree

3 files changed

+88
-13
lines changed

3 files changed

+88
-13
lines changed

lib/link.js

Lines changed: 64 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ const { readdir } = require('fs')
44
const { resolve } = require('path')
55

66
const Arborist = require('@npmcli/arborist')
7+
const npa = require('npm-package-arg')
8+
const rpj = require('read-package-json-fast')
9+
const semver = require('semver')
710

811
const npm = require('./npm.js')
912
const usageUtil = require('./utils/usage.js')
@@ -17,7 +20,7 @@ const completion = (opts, cb) => {
1720
const usage = usageUtil(
1821
'link',
1922
'npm link (in package dir)' +
20-
'\nnpm link [<@scope>/]<pkg>'
23+
'\nnpm link [<@scope>/]<pkg>[@<version>]'
2124
)
2225

2326
const cmd = (args, cb) => link(args).then(() => cb()).catch(cb)
@@ -41,29 +44,81 @@ const link = async args => {
4144
: linkPkg()
4245
}
4346

47+
// Returns a list of items that can't be fulfilled by
48+
// things found in the current arborist inventory
49+
const missingArgsFromTree = (tree, args) => {
50+
const foundNodes = []
51+
const missing = args.filter(a => {
52+
const arg = npa(a)
53+
const nodes = tree.children.values()
54+
const argFound = [...nodes].every(node => {
55+
// TODO: write tests for unmatching version specs, this is hard to test
56+
// atm but should be simple once we have a mocked registry again
57+
if (arg.name !== node.name /* istanbul ignore next */ || (
58+
arg.version &&
59+
!semver.satisfies(node.version, arg.version)
60+
)) {
61+
foundNodes.push(node)
62+
return true
63+
}
64+
})
65+
return argFound
66+
})
67+
68+
// remote nodes from the loaded tree in order
69+
// to avoid dropping them later when reifying
70+
for (const node of foundNodes) {
71+
node.parent = null
72+
}
73+
74+
return missing
75+
}
76+
4477
const linkInstall = async args => {
4578
// load current packages from the global space,
4679
// and then add symlinks installs locally
4780
const globalTop = resolve(npm.globalDir, '..')
48-
const globalArb = new Arborist({
81+
const globalOpts = {
4982
...npm.flatOptions,
5083
path: globalTop,
51-
global: true
84+
global: true,
85+
prune: false
86+
}
87+
const globalArb = new Arborist(globalOpts)
88+
89+
// get only current top-level packages from the global space
90+
const globals = await globalArb.loadActual({
91+
filter: (node, kid) =>
92+
!node.isRoot || args.some(a => npa(a).name === kid)
5293
})
5394

54-
const globals = await globalArb.loadActual()
95+
// any extra arg that is missing from the current
96+
// global space should be reified there first
97+
const missing = missingArgsFromTree(globals, args)
98+
await globalArb.reify({
99+
...globalOpts,
100+
add: missing
101+
})
55102

56-
const links = [
57-
...globals.children.values()
58-
]
59-
.filter(i => args.some(j => j === i.name))
103+
// get a list of module names that should be linked in the local prefix
104+
const names = []
105+
for (const a of args) {
106+
const arg = npa(a)
107+
names.push(
108+
arg.type === 'directory'
109+
? (await rpj(resolve(arg.fetchSpec, 'package.json'))).name
110+
: arg.name
111+
)
112+
}
60113

114+
// create a new arborist instance for the local prefix and
115+
// reify all the pending names as symlinks there
61116
const localArb = new Arborist({
62117
...npm.flatOptions,
63118
path: npm.prefix
64119
})
65120
await localArb.reify({
66-
add: links.map(l => `file:${resolve(globalTop, 'node_modules', l.path)}`)
121+
add: names.map(l => `file:${resolve(globalTop, 'node_modules', l)}`)
67122
})
68123

69124
reifyOutput(localArb)

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@
66
*/
77
'use strict'
88
exports[`test/lib/link.js TAP link global linked pkg to local nm when using args > should create a local symlink to global pkg 1`] = `
9-
{CWD}/test/lib/link-link-global-linked-pkg-to-local-nm-when-using-args/my-project/node_modules/a -> {CWD}/test/lib/link-link-global-linked-pkg-to-local-nm-when-using-args/global-prefix/lib/node_modules/a
109
{CWD}/test/lib/link-link-global-linked-pkg-to-local-nm-when-using-args/my-project/node_modules/@myscope/bar -> {CWD}/test/lib/link-link-global-linked-pkg-to-local-nm-when-using-args/global-prefix/lib/node_modules/@myscope/bar
11-
{CWD}/test/lib/link-link-global-linked-pkg-to-local-nm-when-using-args/my-project/node_modules/test-pkg-link -> {CWD}/test/lib/link-link-global-linked-pkg-to-local-nm-when-using-args/test-pkg-link
1210
{CWD}/test/lib/link-link-global-linked-pkg-to-local-nm-when-using-args/my-project/node_modules/@myscope/linked -> {CWD}/test/lib/link-link-global-linked-pkg-to-local-nm-when-using-args/scoped-linked
11+
{CWD}/test/lib/link-link-global-linked-pkg-to-local-nm-when-using-args/my-project/node_modules/a -> {CWD}/test/lib/link-link-global-linked-pkg-to-local-nm-when-using-args/global-prefix/lib/node_modules/a
12+
{CWD}/test/lib/link-link-global-linked-pkg-to-local-nm-when-using-args/my-project/node_modules/link-me-too -> {CWD}/test/lib/link-link-global-linked-pkg-to-local-nm-when-using-args/link-me-too
13+
{CWD}/test/lib/link-link-global-linked-pkg-to-local-nm-when-using-args/my-project/node_modules/test-pkg-link -> {CWD}/test/lib/link-link-global-linked-pkg-to-local-nm-when-using-args/test-pkg-link
1314
1415
`
1516

test/lib/link.js

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,11 @@ const printLinks = async (opts) => {
2828
const arb = new Arborist(opts)
2929
const tree = await arb.loadActual()
3030
const linkedItems = [...tree.inventory.values()]
31+
.sort((a, b) => a.pkgid.localeCompare(b.pkgid))
3132
for (const item of linkedItems) {
32-
if (item.target)
33+
if (item.target) {
3334
res += `${item.path} -> ${item.target.path}\n`
35+
}
3436
}
3537
return res
3638
}
@@ -128,6 +130,12 @@ t.test('link global linked pkg to local nm when using args', (t) => {
128130
version: '1.0.0'
129131
})
130132
},
133+
'link-me-too': {
134+
'package.json': JSON.stringify({
135+
name: 'link-me-too',
136+
version: '1.0.0'
137+
})
138+
},
131139
'scoped-linked': {
132140
'package.json': JSON.stringify({
133141
name: '@myscope/linked',
@@ -155,8 +163,12 @@ t.test('link global linked pkg to local nm when using args', (t) => {
155163
npm.globalDir = resolve(testdir, 'global-prefix', 'lib', 'node_modules')
156164
npm.prefix = resolve(testdir, 'my-project')
157165

166+
const _cwd = process.cwd()
167+
process.chdir(npm.prefix)
168+
158169
reifyOutput = async () => {
159170
reifyOutput = undefined
171+
process.chdir(_cwd)
160172

161173
const links = await printLinks({
162174
path: npm.prefix
@@ -170,7 +182,14 @@ t.test('link global linked pkg to local nm when using args', (t) => {
170182
// - @myscope/linked: scoped pkg linked to globalDir from local fs
171183
// - @myscope/bar: prev installed scoped package available in globalDir
172184
// - a: prev installed package available in globalDir
173-
link(['test-pkg-link', '@myscope/linked', '@myscope/bar', 'a'], (err) => {
185+
// - file:./link-me-too: pkg that needs to be reified in globalDir first
186+
link([
187+
'test-pkg-link',
188+
'@myscope/linked',
189+
'@myscope/bar',
190+
'a',
191+
'file:../link-me-too'
192+
], (err) => {
174193
t.ifError(err, 'should not error out')
175194
})
176195
})

0 commit comments

Comments
 (0)