1const t = require('tap') 2const { resolve, join } = require('path') 3const fs = require('fs') 4const Arborist = require('@npmcli/arborist') 5const { cleanCwd } = require('../../fixtures/clean-snapshot.js') 6const mockNpm = require('../../fixtures/mock-npm') 7 8t.cleanSnapshot = (str) => cleanCwd(str) 9 10const mockLink = async (t, { globalPrefixDir, ...opts } = {}) => { 11 const mock = await mockNpm(t, { 12 ...opts, 13 command: 'link', 14 globalPrefixDir, 15 mocks: { 16 ...opts.mocks, 17 '{LIB}/utils/reify-output.js': async () => {}, 18 }, 19 }) 20 21 const printLinks = async ({ global = false } = {}) => { 22 let res = '' 23 const arb = new Arborist(global ? { 24 path: resolve(mock.npm.globalDir, '..'), 25 global: true, 26 } : { path: mock.prefix }) 27 const tree = await arb.loadActual() 28 const linkedItems = [...tree.inventory.values()] 29 .sort((a, b) => a.pkgid.localeCompare(b.pkgid, 'en')) 30 for (const item of linkedItems) { 31 if (item.isLink) { 32 res += `${item.path} -> ${item.target.path}\n` 33 } 34 } 35 return res 36 } 37 38 return { 39 ...mock, 40 printLinks, 41 } 42} 43 44t.test('link to globalDir when in current working dir of pkg and no args', async t => { 45 const { link, printLinks } = await mockLink(t, { 46 globalPrefixDir: { 47 node_modules: { 48 a: { 49 'package.json': JSON.stringify({ 50 name: 'a', 51 version: '1.0.0', 52 }), 53 }, 54 }, 55 }, 56 prefixDir: { 57 'package.json': JSON.stringify({ 58 name: 'test-pkg-link', 59 version: '1.0.0', 60 }), 61 }, 62 }) 63 64 await link.exec() 65 t.matchSnapshot(await printLinks({ global: true }), 'should create a global link to current pkg') 66}) 67 68t.test('link ws to globalDir when workspace specified and no args', async t => { 69 const { link, printLinks } = await mockLink(t, { 70 globalPrefixDir: { 71 node_modules: { 72 a: { 73 'package.json': JSON.stringify({ 74 name: 'a', 75 version: '1.0.0', 76 }), 77 }, 78 }, 79 }, 80 prefixDir: { 81 'package.json': JSON.stringify({ 82 name: 'test-pkg-link', 83 version: '1.0.0', 84 workspaces: ['packages/*'], 85 }), 86 packages: { 87 a: { 88 'package.json': JSON.stringify({ 89 name: 'a', 90 version: '1.0.0', 91 }), 92 }, 93 }, 94 }, 95 config: { workspace: 'a' }, 96 }) 97 98 await link.exec() 99 t.matchSnapshot(await printLinks({ global: true }), 'should create a global link to current pkg') 100}) 101 102t.test('link global linked pkg to local nm when using args', async t => { 103 const { link, printLinks } = await mockLink(t, { 104 globalPrefixDir: { 105 node_modules: { 106 '@myscope': { 107 foo: { 108 'package.json': JSON.stringify({ 109 name: '@myscope/foo', 110 version: '1.0.0', 111 }), 112 }, 113 bar: { 114 'package.json': JSON.stringify({ 115 name: '@myscope/bar', 116 version: '1.0.0', 117 }), 118 }, 119 linked: t.fixture('symlink', '../../../other/scoped-linked'), 120 }, 121 a: { 122 'package.json': JSON.stringify({ 123 name: 'a', 124 version: '1.0.0', 125 }), 126 }, 127 b: { 128 'package.json': JSON.stringify({ 129 name: 'b', 130 version: '1.0.0', 131 }), 132 }, 133 'test-pkg-link': t.fixture('symlink', '../../other/test-pkg-link'), 134 }, 135 }, 136 otherDirs: { 137 'test-pkg-link': { 138 'package.json': JSON.stringify({ 139 name: 'test-pkg-link', 140 version: '1.0.0', 141 }), 142 }, 143 'link-me-too': { 144 'package.json': JSON.stringify({ 145 name: 'link-me-too', 146 version: '1.0.0', 147 }), 148 }, 149 'scoped-linked': { 150 'package.json': JSON.stringify({ 151 name: '@myscope/linked', 152 version: '1.0.0', 153 }), 154 }, 155 }, 156 prefixDir: { 157 'package.json': JSON.stringify({ 158 name: 'my-project', 159 version: '1.0.0', 160 dependencies: { 161 foo: '^1.0.0', 162 }, 163 }), 164 node_modules: { 165 foo: { 166 'package.json': JSON.stringify({ 167 name: 'foo', 168 version: '1.0.0', 169 }), 170 }, 171 }, 172 }, 173 }) 174 175 // installs examples for: 176 // - test-pkg-link: pkg linked to globalDir from local fs 177 // - @myscope/linked: scoped pkg linked to globalDir from local fs 178 // - @myscope/bar: prev installed scoped package available in globalDir 179 // - a: prev installed package available in globalDir 180 // - file:./link-me-too: pkg that needs to be reified in globalDir first 181 await link.exec([ 182 'test-pkg-link', 183 '@myscope/linked', 184 '@myscope/bar', 185 'a', 186 'file:../other/link-me-too', 187 ]) 188 189 t.matchSnapshot(await printLinks(), 'should create a local symlink to global pkg') 190}) 191 192t.test('link global linked pkg to local workspace using args', async t => { 193 const { link, printLinks } = await mockLink(t, { 194 globalPrefixDir: { 195 node_modules: { 196 '@myscope': { 197 foo: { 198 'package.json': JSON.stringify({ 199 name: '@myscope/foo', 200 version: '1.0.0', 201 }), 202 }, 203 bar: { 204 'package.json': JSON.stringify({ 205 name: '@myscope/bar', 206 version: '1.0.0', 207 }), 208 }, 209 linked: t.fixture('symlink', '../../../other/scoped-linked'), 210 }, 211 a: { 212 'package.json': JSON.stringify({ 213 name: 'a', 214 version: '1.0.0', 215 }), 216 }, 217 b: { 218 'package.json': JSON.stringify({ 219 name: 'b', 220 version: '1.0.0', 221 }), 222 }, 223 'test-pkg-link': t.fixture('symlink', '../../other/test-pkg-link'), 224 }, 225 }, 226 otherDirs: { 227 'test-pkg-link': { 228 'package.json': JSON.stringify({ 229 name: 'test-pkg-link', 230 version: '1.0.0', 231 }), 232 }, 233 'link-me-too': { 234 'package.json': JSON.stringify({ 235 name: 'link-me-too', 236 version: '1.0.0', 237 }), 238 }, 239 'scoped-linked': { 240 'package.json': JSON.stringify({ 241 name: '@myscope/linked', 242 version: '1.0.0', 243 }), 244 }, 245 }, 246 prefixDir: { 247 'package.json': JSON.stringify({ 248 name: 'my-project', 249 version: '1.0.0', 250 workspaces: ['packages/*'], 251 }), 252 packages: { 253 x: { 254 'package.json': JSON.stringify({ 255 name: 'x', 256 version: '1.0.0', 257 dependencies: { 258 foo: '^1.0.0', 259 }, 260 }), 261 }, 262 }, 263 node_modules: { 264 foo: { 265 'package.json': JSON.stringify({ 266 name: 'foo', 267 version: '1.0.0', 268 }), 269 }, 270 }, 271 }, 272 config: { workspace: 'x' }, 273 }) 274 275 // installs examples for: 276 // - test-pkg-link: pkg linked to globalDir from local fs 277 // - @myscope/linked: scoped pkg linked to globalDir from local fs 278 // - @myscope/bar: prev installed scoped package available in globalDir 279 // - a: prev installed package available in globalDir 280 // - file:./link-me-too: pkg that needs to be reified in globalDir first 281 await link.exec([ 282 'test-pkg-link', 283 '@myscope/linked', 284 '@myscope/bar', 285 'a', 286 'file:../other/link-me-too', 287 ]) 288 289 t.matchSnapshot(await printLinks(), 'should create a local symlink to global pkg') 290}) 291 292t.test('link pkg already in global space', async t => { 293 const { npm, link, printLinks, prefix } = await mockLink(t, { 294 globalPrefixDir: { 295 node_modules: { 296 '@myscope': { 297 linked: t.fixture('symlink', '../../../other/scoped-linked'), 298 }, 299 }, 300 }, 301 otherDirs: { 302 'scoped-linked': { 303 'package.json': JSON.stringify({ 304 name: '@myscope/linked', 305 version: '1.0.0', 306 }), 307 }, 308 }, 309 prefixDir: { 310 'package.json': JSON.stringify({ 311 name: 'my-project', 312 version: '1.0.0', 313 }), 314 }, 315 }) 316 317 // XXX: how to convert this to a config that gets passed in? 318 npm.config.find = () => 'default' 319 320 await link.exec(['@myscope/linked']) 321 322 t.equal( 323 require(resolve(prefix, 'package.json')).dependencies, 324 undefined, 325 'should not save to package.json upon linking' 326 ) 327 328 t.matchSnapshot(await printLinks(), 'should create a local symlink to global pkg') 329}) 330 331t.test('link pkg already in global space when prefix is a symlink', async t => { 332 const { npm, link, printLinks, prefix } = await mockLink(t, { 333 globalPrefixDir: t.fixture('symlink', './other/real-global-prefix'), 334 otherDirs: { 335 // mockNpm does this automatically but only for globalPrefixDir so here we 336 // need to do it manually since we are making a symlink somewhere else 337 'real-global-prefix': mockNpm.setGlobalNodeModules({ 338 node_modules: { 339 '@myscope': { 340 linked: t.fixture('symlink', '../../../scoped-linked'), 341 }, 342 }, 343 }), 344 'scoped-linked': { 345 'package.json': JSON.stringify({ 346 name: '@myscope/linked', 347 version: '1.0.0', 348 }), 349 }, 350 }, 351 prefixDir: { 352 'package.json': JSON.stringify({ 353 name: 'my-project', 354 version: '1.0.0', 355 }), 356 }, 357 }) 358 359 npm.config.find = () => 'default' 360 361 await link.exec(['@myscope/linked']) 362 363 t.equal( 364 require(resolve(prefix, 'package.json')).dependencies, 365 undefined, 366 'should not save to package.json upon linking' 367 ) 368 369 t.matchSnapshot(await printLinks(), 'should create a local symlink to global pkg') 370}) 371 372t.test('should not save link to package file', async t => { 373 const { link, prefix } = await mockLink(t, { 374 globalPrefixDir: { 375 node_modules: { 376 '@myscope': { 377 linked: t.fixture('symlink', '../../../other/scoped-linked'), 378 }, 379 }, 380 }, 381 otherDirs: { 382 'scoped-linked': { 383 'package.json': JSON.stringify({ 384 name: '@myscope/linked', 385 version: '1.0.0', 386 }), 387 }, 388 }, 389 prefixDir: { 390 'package.json': JSON.stringify({ 391 name: 'my-project', 392 version: '1.0.0', 393 }), 394 }, 395 config: { save: false }, 396 }) 397 398 await link.exec(['@myscope/linked']) 399 t.match( 400 require(resolve(prefix, 'package.json')).dependencies, 401 undefined, 402 'should not save to package.json upon linking' 403 ) 404}) 405 406t.test('should not prune dependencies when linking packages', async t => { 407 const { link, prefix } = await mockLink(t, { 408 globalPrefixDir: { 409 node_modules: { 410 linked: t.fixture('symlink', '../../other/linked'), 411 }, 412 }, 413 otherDirs: { 414 linked: { 415 'package.json': JSON.stringify({ 416 name: 'linked', 417 version: '1.0.0', 418 }), 419 }, 420 }, 421 prefixDir: { 422 node_modules: { 423 foo: { 424 'package.json': JSON.stringify({ name: 'foo', version: '1.0.0' }), 425 }, 426 }, 427 'package.json': JSON.stringify({ 428 name: 'my-project', 429 version: '1.0.0', 430 }), 431 }, 432 }) 433 434 await link.exec(['linked']) 435 436 t.ok( 437 fs.statSync(resolve(prefix, 'node_modules/foo')), 438 'should not prune any extraneous dep when running npm link' 439 ) 440}) 441 442t.test('completion', async t => { 443 const { link } = await mockLink(t, { 444 globalPrefixDir: { 445 node_modules: { 446 foo: {}, 447 bar: {}, 448 lorem: {}, 449 ipsum: {}, 450 }, 451 }, 452 }) 453 454 const words = await link.completion({}) 455 456 t.same( 457 words, 458 ['bar', 'foo', 'ipsum', 'lorem'], 459 'should list all package names available in globalDir' 460 ) 461}) 462 463t.test('--global option', async t => { 464 const { link } = await mockLink(t, { 465 config: { global: true }, 466 }) 467 await t.rejects( 468 link.exec([]), 469 /link should never be --global/, 470 'should throw an useful error' 471 ) 472}) 473 474t.test('hash character in working directory path', async t => { 475 const { link, printLinks } = await mockLink(t, { 476 globalPrefixDir: { 477 node_modules: { 478 a: { 479 'package.json': JSON.stringify({ 480 name: 'a', 481 version: '1.0.0', 482 }), 483 }, 484 }, 485 }, 486 otherDirs: { 487 'i_like_#_in_my_paths': { 488 'test-pkg-link': { 489 'package.json': JSON.stringify({ 490 name: 'test-pkg-link', 491 version: '1.0.0', 492 }), 493 }, 494 }, 495 }, 496 chdir: ({ other }) => join(other, 'i_like_#_in_my_paths', 'test-pkg-link'), 497 }) 498 await link.exec([]) 499 500 t.matchSnapshot(await printLinks({ global: true }), 501 'should create a global link to current pkg, even within path with hash') 502}) 503 504t.test('test linked installed as symlinks', async t => { 505 const { link, prefix, printLinks } = await mockLink(t, { 506 otherDirs: { 507 mylink: { 508 'package.json': JSON.stringify({ 509 name: 'mylink', 510 version: '1.0.0', 511 }), 512 }, 513 }, 514 }) 515 516 await link.exec([ 517 join('file:../other/mylink'), 518 ]) 519 520 t.ok(fs.lstatSync(join(prefix, 'node_modules', 'mylink')).isSymbolicLink(), 521 'linked path should by symbolic link' 522 ) 523 524 t.matchSnapshot(await printLinks(), 'linked package should not be installed') 525}) 526