/
githubmirror
/
node
Обзор
Документация
Войти
/
githubmirror
/
node
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
deps/npm/test/lib/commands/link.js
664 строки
17 KB
npm CLI robot
deps: upgrade npm to 11.18.0
08 июл 2026, 21:19
Не верифицирован
08 июл 2026, 21:19
fd35018
Код
Авторство
О чём код?
const t = require('tap') const { resolve, join } = require('node:path') const fs = require('node:fs') const Arborist = require('@npmcli/arborist') const { cleanCwd } = require('../../fixtures/clean-snapshot.js') const mockNpm = require('../../fixtures/mock-npm') t.cleanSnapshot = (str) => cleanCwd(str) const mockLink = async (t, { globalPrefixDir, ...opts } = {}) => { const mock = await mockNpm(t, { ...opts, command: 'link', globalPrefixDir, mocks: { ...opts.mocks, '{LIB}/utils/reify-output.js': async () => {}, }, }) const printLinks = async ({ global = false } = {}) => { let res = '' const arb = new Arborist(global ? { path: resolve(mock.npm.globalDir, '..'), global: true, } : { path: mock.prefix }) const tree = await arb.loadActual() const linkedItems = [...tree.inventory.values()] .sort((a, b) => a.pkgid.localeCompare(b.pkgid, 'en')) for (const item of linkedItems) { if (item.isLink) { res += `${item.path} -> ${item.target.path}\n` } } return res } return { ...mock, printLinks, } } t.test('link to globalDir when in current working dir of pkg and no args', async t => { const { link, printLinks } = await mockLink(t, { globalPrefixDir: { node_modules: { a: { 'package.json': JSON.stringify({ name: 'a', version: '1.0.0', }), }, }, }, prefixDir: { 'package.json': JSON.stringify({ name: 'test-pkg-link', version: '1.0.0', }), }, }) await link.exec() t.matchSnapshot(await printLinks({ global: true }), 'should create a global link to current pkg') }) t.test('link ws to globalDir when workspace specified and no args', async t => { const { link, printLinks } = await mockLink(t, { globalPrefixDir: { node_modules: { a: { 'package.json': JSON.stringify({ name: 'a', version: '1.0.0', }), }, }, }, prefixDir: { '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', }), }, }, }, config: { workspace: 'a' }, }) await link.exec() t.matchSnapshot(await printLinks({ global: true }), 'should create a global link to current pkg') }) t.test('link global linked pkg to local nm when using args', async t => { const { link, printLinks } = await mockLink(t, { globalPrefixDir: { 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', '../../../other/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', '../../other/test-pkg-link'), }, }, otherDirs: { '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', }), }, }, prefixDir: { 'package.json': JSON.stringify({ name: 'my-project', version: '1.0.0', dependencies: { foo: '^1.0.0', }, }), node_modules: { foo: { 'package.json': JSON.stringify({ name: 'foo', version: '1.0.0', }), }, }, }, }) // 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 await link.exec([ 'test-pkg-link', '@myscope/linked', '@myscope/bar', 'a', 'file:../other/link-me-too', ]) t.matchSnapshot(await printLinks(), 'should create a local symlink to global pkg') }) t.test('link global linked pkg to local workspace using args', async t => { const { link, printLinks } = await mockLink(t, { globalPrefixDir: { 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', '../../../other/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', '../../other/test-pkg-link'), }, }, otherDirs: { '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', }), }, }, prefixDir: { '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', }), }, }, }, config: { workspace: 'x' }, }) // 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 await link.exec([ 'test-pkg-link', '@myscope/linked', '@myscope/bar', 'a', 'file:../other/link-me-too', ]) t.matchSnapshot(await printLinks(), 'should create a local symlink to global pkg') }) t.test('link --workspace --save targets the workspace manifest, not the root', async t => { const { link, prefix } = await mockLink(t, { globalPrefixDir: { node_modules: { a: { 'package.json': JSON.stringify({ name: 'a', version: '1.0.0', }), }, }, }, prefixDir: { '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', }), }, }, }, config: { workspace: 'x', save: true }, }) await link.exec(['a']) const root = JSON.parse(fs.readFileSync(join(prefix, 'package.json'), 'utf8')) const ws = JSON.parse(fs.readFileSync(join(prefix, 'packages', 'x', 'package.json'), 'utf8')) t.notOk(root.dependencies, 'root manifest should not get the dependency') t.match(ws.dependencies, { a: /^file:/ }, 'workspace manifest should get the file: dependency') }) t.test('link pkg already in global space', async t => { const { npm, link, printLinks, prefix } = await mockLink(t, { globalPrefixDir: { node_modules: { '@myscope': { linked: t.fixture('symlink', '../../../other/scoped-linked'), }, }, }, otherDirs: { 'scoped-linked': { 'package.json': JSON.stringify({ name: '@myscope/linked', version: '1.0.0', }), }, }, prefixDir: { 'package.json': JSON.stringify({ name: 'my-project', version: '1.0.0', }), }, }) // XXX: how to convert this to a config that gets passed in? npm.config.find = () => 'default' await link.exec(['@myscope/linked']) t.equal( require(resolve(prefix, 'package.json')).dependencies, undefined, 'should not save to package.json upon linking' ) t.matchSnapshot(await printLinks(), 'should create a local symlink to global pkg') }) t.test('link pkg already in global space when prefix is a symlink', async t => { const { npm, link, printLinks, prefix } = await mockLink(t, { globalPrefixDir: t.fixture('symlink', './other/real-global-prefix'), otherDirs: { // mockNpm does this automatically but only for globalPrefixDir so here we // need to do it manually since we are making a symlink somewhere else 'real-global-prefix': mockNpm.setGlobalNodeModules({ node_modules: { '@myscope': { linked: t.fixture('symlink', '../../../scoped-linked'), }, }, }), 'scoped-linked': { 'package.json': JSON.stringify({ name: '@myscope/linked', version: '1.0.0', }), }, }, prefixDir: { 'package.json': JSON.stringify({ name: 'my-project', version: '1.0.0', }), }, }) npm.config.find = () => 'default' await link.exec(['@myscope/linked']) t.equal( require(resolve(prefix, 'package.json')).dependencies, undefined, 'should not save to package.json upon linking' ) t.matchSnapshot(await printLinks(), 'should create a local symlink to global pkg') }) t.test('should not save link to package file', async t => { const { link, prefix } = await mockLink(t, { globalPrefixDir: { node_modules: { '@myscope': { linked: t.fixture('symlink', '../../../other/scoped-linked'), }, }, }, otherDirs: { 'scoped-linked': { 'package.json': JSON.stringify({ name: '@myscope/linked', version: '1.0.0', }), }, }, prefixDir: { 'package.json': JSON.stringify({ name: 'my-project', version: '1.0.0', }), }, config: { save: false }, }) await link.exec(['@myscope/linked']) t.match( require(resolve(prefix, 'package.json')).dependencies, undefined, 'should not save to package.json upon linking' ) }) t.test('should not prune dependencies when linking packages', async t => { const { link, prefix } = await mockLink(t, { globalPrefixDir: { node_modules: { linked: t.fixture('symlink', '../../other/linked'), }, }, otherDirs: { linked: { 'package.json': JSON.stringify({ name: 'linked', version: '1.0.0', }), }, }, prefixDir: { node_modules: { foo: { 'package.json': JSON.stringify({ name: 'foo', version: '1.0.0' }), }, }, 'package.json': JSON.stringify({ name: 'my-project', version: '1.0.0', }), }, }) await link.exec(['linked']) t.ok( fs.statSync(resolve(prefix, 'node_modules/foo')), 'should not prune any extraneous dep when running npm link' ) }) t.test('completion', async t => { const { link } = await mockLink(t, { globalPrefixDir: { node_modules: { foo: {}, bar: {}, lorem: {}, ipsum: {}, }, }, }) const words = await link.completion({}) t.same( words, ['bar', 'foo', 'ipsum', 'lorem'], 'should list all package names available in globalDir' ) }) t.test('--global option', async t => { const { link } = await mockLink(t, { config: { global: true }, }) await t.rejects( link.exec([]), /link should never be --global/, 'should throw an useful error' ) }) t.test('hash character in working directory path', async t => { const { link, printLinks } = await mockLink(t, { globalPrefixDir: { node_modules: { a: { 'package.json': JSON.stringify({ name: 'a', version: '1.0.0', }), }, }, }, otherDirs: { 'i_like_#_in_my_paths': { 'test-pkg-link': { 'package.json': JSON.stringify({ name: 'test-pkg-link', version: '1.0.0', }), }, }, }, chdir: ({ other }) => join(other, 'i_like_#_in_my_paths', 'test-pkg-link'), }) await link.exec([]) t.matchSnapshot(await printLinks({ global: true }), 'should create a global link to current pkg, even within path with hash') }) t.test('test linked installed as symlinks', async t => { const { link, prefix, printLinks } = await mockLink(t, { otherDirs: { mylink: { 'package.json': JSON.stringify({ name: 'mylink', version: '1.0.0', }), }, }, }) await link.exec([ join('file:../other/mylink'), ]) t.ok(fs.lstatSync(join(prefix, 'node_modules', 'mylink')).isSymbolicLink(), 'linked path should by symbolic link' ) t.matchSnapshot(await printLinks(), 'linked package should not be installed') }) t.test('link threads allowScripts policy through to arborist', async t => { const capturedOpts = [] const FakeArborist = function (opts) { capturedOpts.push(opts) this.options = opts this.actualTree = { inventory: new Map() } } FakeArborist.prototype.loadActual = async () => ({ isLink: false, children: new Map() }) FakeArborist.prototype.reify = async () => {} const mock = await mockNpm(t, { command: 'link', prefixDir: { 'package.json': JSON.stringify({ name: 'host', version: '1.0.0', allowScripts: { canvas: true }, }), }, mocks: { '@npmcli/arborist': FakeArborist, '{LIB}/utils/reify-finish.js': async () => {}, }, }) await mock.npm.exec('link', ['canvas']) // the local Arborist is the last one constructed in linkInstall const localOpts = capturedOpts[capturedOpts.length - 1] t.strictSame(localOpts.allowScripts, { canvas: true }, 'local arborist opts.allowScripts populated from package.json') }) t.test('link threads allowScripts policy to the global install', async t => { const capturedOpts = [] const FakeArborist = function (opts) { capturedOpts.push(opts) this.options = opts this.actualTree = { inventory: new Map() } } FakeArborist.prototype.loadActual = async () => ({ isLink: false, children: new Map() }) FakeArborist.prototype.reify = async () => {} const mock = await mockNpm(t, { command: 'link', prefixDir: { 'package.json': JSON.stringify({ name: 'host', version: '1.0.0', allowScripts: { canvas: true }, }), }, mocks: { '@npmcli/arborist': FakeArborist, '{LIB}/utils/reify-finish.js': async () => {}, }, }) await mock.npm.exec('link', ['canvas']) // the global Arborist is constructed first; its missing-package install // must carry the project policy. t.strictSame(capturedOpts[0].allowScripts, { canvas: true }, 'global arborist opts.allowScripts populated from package.json') }) t.test('link runs the strict-allow-scripts preflight before the global install', async t => { // The mocked preflight stands in for strict mode finding an uncovered // script. It must run and throw before the global reify. const calls = [] const FakeArborist = function (opts) { this.options = opts this.actualTree = { inventory: new Map() } } FakeArborist.prototype.loadActual = async () => ({ isLink: false, children: new Map() }) FakeArborist.prototype.reify = async () => { calls.push('reify') } const mock = await mockNpm(t, { command: 'link', prefixDir: { 'package.json': JSON.stringify({ name: 'host', version: '1.0.0', }), }, mocks: { '@npmcli/arborist': FakeArborist, '{LIB}/utils/reify-finish.js': async () => {}, '{LIB}/utils/strict-allow-scripts-preflight.js': async () => { calls.push('preflight') throw Object.assign(new Error('blocked'), { code: 'ESTRICTALLOWSCRIPTS' }) }, }, }) await t.rejects( mock.npm.exec('link', ['canvas']), { code: 'ESTRICTALLOWSCRIPTS' }, 'the strict preflight blocks the link' ) t.strictSame(calls, ['preflight'], 'preflight ran and the global reify never executed') })