diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md index 4e336ae5daf21..681fc6ebc3fa1 100644 --- a/DEPENDENCIES.md +++ b/DEPENDENCIES.md @@ -218,6 +218,8 @@ graph LR; npmcli-package-json-->json-parse-even-better-errors; npmcli-package-json-->normalize-package-data; npmcli-package-json-->npm-normalize-package-bin; + npmcli-package-json-->npmcli-git["@npmcli/git"]; + npmcli-package-json-->proc-log; npmcli-run-script-->npmcli-node-gyp["@npmcli/node-gyp"]; npmcli-run-script-->npmcli-promise-spawn["@npmcli/promise-spawn"]; npmcli-run-script-->read-package-json-fast; @@ -690,6 +692,7 @@ graph LR; npmcli-mock-globals-->npmcli-eslint-config["@npmcli/eslint-config"]; npmcli-mock-globals-->npmcli-template-oss["@npmcli/template-oss"]; npmcli-mock-globals-->tap; + npmcli-mock-registry-->json-stringify-safe; npmcli-mock-registry-->nock; npmcli-mock-registry-->npm-package-arg; npmcli-mock-registry-->npmcli-arborist["@npmcli/arborist"]; @@ -703,6 +706,8 @@ graph LR; npmcli-package-json-->json-parse-even-better-errors; npmcli-package-json-->normalize-package-data; npmcli-package-json-->npm-normalize-package-bin; + npmcli-package-json-->npmcli-git["@npmcli/git"]; + npmcli-package-json-->proc-log; npmcli-promise-spawn-->which; npmcli-query-->postcss-selector-parser; npmcli-run-script-->node-gyp; @@ -821,8 +826,8 @@ packages higher up the chain. - @npmcli/arborist - @npmcli/metavuln-calculator - pacote, libnpmhook, libnpmorg, libnpmsearch, libnpmteam, npm-profile - - npm-registry-fetch, libnpmversion + - npm-registry-fetch, @npmcli/package-json, libnpmversion - @npmcli/git, make-fetch-happen, @npmcli/config, init-package-json - - @npmcli/installed-package-contents, @npmcli/map-workspaces, cacache, npm-pick-manifest, @npmcli/run-script, read-package-json, @npmcli/package-json, promzard + - @npmcli/installed-package-contents, @npmcli/map-workspaces, cacache, npm-pick-manifest, @npmcli/run-script, read-package-json, promzard - @npmcli/docs, @npmcli/fs, npm-bundled, read-package-json-fast, unique-filename, npm-install-checks, npm-package-arg, npm-packlist, normalize-package-data, bin-links, nopt, npmlog, parse-conflict-json, @npmcli/mock-globals, read - @npmcli/eslint-config, @npmcli/template-oss, ignore-walk, semver, npm-normalize-package-bin, @npmcli/name-from-folder, json-parse-even-better-errors, fs-minipass, ssri, unique-slug, @npmcli/promise-spawn, hosted-git-info, proc-log, validate-npm-package-name, @npmcli/node-gyp, minipass-fetch, @npmcli/query, cmd-shim, read-cmd-shim, write-file-atomic, abbrev, are-we-there-yet, gauge, minify-registry-metadata, ini, @npmcli/disparity-colors, mute-stream, npm-audit-report, npm-user-validate diff --git a/mock-registry/lib/index.js b/mock-registry/lib/index.js index 924af05d5b6c4..91f1de5b52e0d 100644 --- a/mock-registry/lib/index.js +++ b/mock-registry/lib/index.js @@ -2,6 +2,7 @@ const pacote = require('pacote') const Arborist = require('@npmcli/arborist') const npa = require('npm-package-arg') const Nock = require('nock') +const stringify = require('json-stringify-safe') class MockRegistry { #tap @@ -39,7 +40,7 @@ class MockRegistry { // mocked with a 404, 500, etc. // XXX: this is opt-in currently because it breaks some existing CLI // tests. We should work towards making this the default for all tests. - t.fail(`Unmatched request: ${JSON.stringify(req, null, 2)}`) + t.fail(`Unmatched request: ${stringify(req, null, 2)}`) } } @@ -337,9 +338,9 @@ class MockRegistry { } nock = nock.reply(200, manifest) if (tarballs) { - for (const version in tarballs) { + for (const [version, tarball] of Object.entries(tarballs)) { const m = manifest.versions[version] - nock = await this.tarball({ manifest: m, tarball: tarballs[version] }) + nock = await this.tarball({ manifest: m, tarball }) } } this.nock = nock diff --git a/mock-registry/package.json b/mock-registry/package.json index c85877288d3ef..6ab00a246705a 100644 --- a/mock-registry/package.json +++ b/mock-registry/package.json @@ -47,6 +47,7 @@ "@npmcli/arborist": "^6.1.1", "@npmcli/eslint-config": "^4.0.1", "@npmcli/template-oss": "4.14.1", + "json-stringify-safe": "^5.0.1", "nock": "^13.3.0", "npm-package-arg": "^10.1.0", "pacote": "^15.0.8", diff --git a/node_modules/@npmcli/git/lib/find.js b/node_modules/@npmcli/git/lib/find.js index d58f01dbcc16f..34bd310b88e5d 100644 --- a/node_modules/@npmcli/git/lib/find.js +++ b/node_modules/@npmcli/git/lib/find.js @@ -1,15 +1,15 @@ const is = require('./is.js') const { dirname } = require('path') -module.exports = async ({ cwd = process.cwd() } = {}) => { - if (await is({ cwd })) { - return cwd - } - while (cwd !== dirname(cwd)) { - cwd = dirname(cwd) +module.exports = async ({ cwd = process.cwd(), root } = {}) => { + while (true) { if (await is({ cwd })) { return cwd } + const next = dirname(cwd) + if (cwd === root || cwd === next) { + return null + } + cwd = next } - return null } diff --git a/node_modules/@npmcli/git/package.json b/node_modules/@npmcli/git/package.json index 41c78dddfa3cc..eeba1c0415788 100644 --- a/node_modules/@npmcli/git/package.json +++ b/node_modules/@npmcli/git/package.json @@ -1,6 +1,6 @@ { "name": "@npmcli/git", - "version": "4.0.4", + "version": "4.1.0", "main": "lib/index.js", "files": [ "bin/", @@ -23,8 +23,7 @@ "template-oss-apply": "template-oss-apply --force" }, "tap": { - "check-coverage": true, - "coverage-map": "map.js", + "timeout": 600, "nyc-arg": [ "--exclude", "tap-snapshots/**" @@ -32,7 +31,7 @@ }, "devDependencies": { "@npmcli/eslint-config": "^4.0.0", - "@npmcli/template-oss": "4.12.0", + "@npmcli/template-oss": "4.15.1", "npm-package-arg": "^10.0.0", "slash": "^3.0.0", "tap": "^16.0.1" @@ -52,7 +51,7 @@ }, "templateOSS": { "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", - "windowsCI": false, - "version": "4.12.0" + "version": "4.15.1", + "publish": true } } diff --git a/node_modules/@npmcli/package-json/lib/index.js b/node_modules/@npmcli/package-json/lib/index.js index 34e415b45d49f..756837cdde58a 100644 --- a/node_modules/@npmcli/package-json/lib/index.js +++ b/node_modules/@npmcli/package-json/lib/index.js @@ -38,6 +38,7 @@ class PackageJson { '_attributes', 'bundledDependencies', 'bundleDependencies', + 'bundleDependenciesDeleteFalse', 'gypfile', 'serverjs', 'scriptpath', diff --git a/node_modules/@npmcli/package-json/lib/normalize.js b/node_modules/@npmcli/package-json/lib/normalize.js index bc101cd4fde1b..9594ef3d7ff4f 100644 --- a/node_modules/@npmcli/package-json/lib/normalize.js +++ b/node_modules/@npmcli/package-json/lib/normalize.js @@ -3,10 +3,13 @@ const { glob } = require('glob') const normalizePackageBin = require('npm-normalize-package-bin') const normalizePackageData = require('normalize-package-data') const path = require('path') +const log = require('proc-log') +const git = require('@npmcli/git') -const normalize = async (pkg, { strict, steps }) => { +const normalize = async (pkg, { strict, steps, root }) => { const data = pkg.content const scripts = data.scripts || {} + const pkgId = `${data.name ?? ''}@${data.version ?? ''}` // remove attributes that start with "_" if (steps.includes('_attributes')) { @@ -20,7 +23,7 @@ const normalize = async (pkg, { strict, steps }) => { // build the "_id" attribute if (steps.includes('_id')) { if (data.name && data.version) { - data._id = `${data.name}@${data.version}` + data._id = pkgId } } @@ -34,7 +37,9 @@ const normalize = async (pkg, { strict, steps }) => { // expand "bundleDependencies: true or translate from object" if (steps.includes('bundleDependencies')) { const bd = data.bundleDependencies - if (bd === true) { + if (bd === false && !steps.includes('bundleDependenciesDeleteFalse')) { + data.bundleDependencies = [] + } else if (bd === true) { data.bundleDependencies = Object.keys(data.dependencies || {}) } else if (bd && typeof bd === 'object') { if (!Array.isArray(bd)) { @@ -158,7 +163,7 @@ const normalize = async (pkg, { strict, steps }) => { } // expand "directories.bin" - if (steps.includes('binDir') && data.directories?.bin) { + if (steps.includes('binDir') && data.directories?.bin && !data.bin) { const binsDir = path.resolve(pkg.path, path.join('.', path.join('/', data.directories.bin))) const bins = await glob('**', { cwd: binsDir }) data.bin = bins.reduce((acc, binFile) => { @@ -174,17 +179,20 @@ const normalize = async (pkg, { strict, steps }) => { // populate "gitHead" attribute if (steps.includes('gitHead') && !data.gitHead) { + const gitRoot = await git.find({ cwd: pkg.path, root }) let head - try { - head = await fs.readFile(path.resolve(pkg.path, '.git/HEAD'), 'utf8') - } catch (err) { + if (gitRoot) { + try { + head = await fs.readFile(path.resolve(gitRoot, '.git/HEAD'), 'utf8') + } catch (err) { // do nothing + } } let headData if (head) { if (head.startsWith('ref: ')) { const headRef = head.replace(/^ref: /, '').trim() - const headFile = path.resolve(pkg.path, '.git', headRef) + const headFile = path.resolve(gitRoot, '.git', headRef) try { headData = await fs.readFile(headFile, 'utf8') headData = headData.replace(/^ref: /, '').trim() @@ -192,7 +200,7 @@ const normalize = async (pkg, { strict, steps }) => { // do nothing } if (!headData) { - const packFile = path.resolve(pkg.path, '.git/packed-refs') + const packFile = path.resolve(gitRoot, '.git/packed-refs') try { let refs = await fs.readFile(packFile, 'utf8') if (refs) { @@ -271,11 +279,11 @@ const normalize = async (pkg, { strict, steps }) => { // in normalize-package-data if it had access to the file path. if (steps.includes('binRefs') && data.bin instanceof Object) { for (const key in data.bin) { - const binPath = path.resolve(pkg.path, data.bin[key]) try { - await fs.access(binPath) + await fs.access(path.resolve(pkg.path, data.bin[key])) } catch { - delete data.bin[key] + log.warn('package-json', pkgId, `No bin file found at ${data.bin[key]}`) + // XXX: should a future breaking change delete bin entries that cannot be accessed? } } } diff --git a/node_modules/@npmcli/package-json/package.json b/node_modules/@npmcli/package-json/package.json index 61607c5bb6ae7..a4e2cbab4c0bd 100644 --- a/node_modules/@npmcli/package-json/package.json +++ b/node_modules/@npmcli/package-json/package.json @@ -1,6 +1,6 @@ { "name": "@npmcli/package-json", - "version": "3.1.0", + "version": "3.1.1", "description": "Programmatic API to update package.json", "main": "lib/index.js", "files": [ @@ -26,13 +26,17 @@ "devDependencies": { "@npmcli/eslint-config": "^4.0.0", "@npmcli/template-oss": "4.15.1", + "read-package-json": "^6.0.4", + "read-package-json-fast": "^3.0.2", "tap": "^16.0.1" }, "dependencies": { + "@npmcli/git": "^4.1.0", "glob": "^10.2.2", "json-parse-even-better-errors": "^3.0.0", "normalize-package-data": "^5.0.0", - "npm-normalize-package-bin": "^3.0.1" + "npm-normalize-package-bin": "^3.0.1", + "proc-log": "^3.0.0" }, "repository": { "type": "git", diff --git a/package-lock.json b/package-lock.json index d2e83b0b7da09..6f08ec4368e66 100644 --- a/package-lock.json +++ b/package-lock.json @@ -87,7 +87,7 @@ "@npmcli/arborist": "^6.2.9", "@npmcli/config": "^6.2.0", "@npmcli/map-workspaces": "^3.0.4", - "@npmcli/package-json": "^3.1.0", + "@npmcli/package-json": "^3.1.1", "@npmcli/run-script": "^6.0.2", "abbrev": "^2.0.0", "archy": "~1.0.0", @@ -157,7 +157,7 @@ "@npmcli/docs": "^1.0.0", "@npmcli/eslint-config": "^4.0.0", "@npmcli/fs": "^3.1.0", - "@npmcli/git": "^4.0.4", + "@npmcli/git": "^4.1.0", "@npmcli/mock-globals": "^1.0.0", "@npmcli/mock-registry": "^1.0.0", "@npmcli/promise-spawn": "^6.0.2", @@ -222,6 +222,7 @@ "@npmcli/arborist": "^6.1.1", "@npmcli/eslint-config": "^4.0.1", "@npmcli/template-oss": "4.14.1", + "json-stringify-safe": "^5.0.1", "nock": "^13.3.0", "npm-package-arg": "^10.1.0", "pacote": "^15.0.8", @@ -2289,9 +2290,9 @@ } }, "node_modules/@npmcli/git": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-4.0.4.tgz", - "integrity": "sha512-5yZghx+u5M47LghaybLCkdSyFzV/w4OuH12d96HO389Ik9CDsLaDZJVynSGGVJOLn6gy/k7Dz5XYcplM3uxXRg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-4.1.0.tgz", + "integrity": "sha512-9hwoB3gStVfa0N31ymBmrX+GuDGdVA/QWShZVqE0HK2Af+7QGGrCTbZia/SW0ImUTjTne7SP91qxDmtXvDHRPQ==", "inBundle": true, "dependencies": { "@npmcli/promise-spawn": "^6.0.0", @@ -2393,15 +2394,17 @@ } }, "node_modules/@npmcli/package-json": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-3.1.0.tgz", - "integrity": "sha512-qNPy6Yf9ruFST99xcrl5EWAvrb7qFrwgVbwdzcTJlIgxbArKOq5e/bgZ6rTL1X9hDgAdPbvL8RWx/OTLSB0ToA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-3.1.1.tgz", + "integrity": "sha512-+UW0UWOYFKCkvszLoTwrYGrjNrT8tI5Ckeb/h+Z1y1fsNJEctl7HmerA5j2FgmoqFaLI2gsA1X9KgMFqx/bRmA==", "inBundle": true, "dependencies": { + "@npmcli/git": "^4.1.0", "glob": "^10.2.2", "json-parse-even-better-errors": "^3.0.0", "normalize-package-data": "^5.0.0", - "npm-normalize-package-bin": "^3.0.1" + "npm-normalize-package-bin": "^3.0.1", + "proc-log": "^3.0.0" }, "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" diff --git a/package.json b/package.json index 0f65524e0a08f..5df1321c5c55a 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "@npmcli/arborist": "^6.2.9", "@npmcli/config": "^6.2.0", "@npmcli/map-workspaces": "^3.0.4", - "@npmcli/package-json": "^3.1.0", + "@npmcli/package-json": "^3.1.1", "@npmcli/run-script": "^6.0.2", "abbrev": "^2.0.0", "archy": "~1.0.0", @@ -190,7 +190,7 @@ "@npmcli/docs": "^1.0.0", "@npmcli/eslint-config": "^4.0.0", "@npmcli/fs": "^3.1.0", - "@npmcli/git": "^4.0.4", + "@npmcli/git": "^4.1.0", "@npmcli/mock-globals": "^1.0.0", "@npmcli/mock-registry": "^1.0.0", "@npmcli/promise-spawn": "^6.0.2", diff --git a/smoke-tests/test/fixtures/setup.js b/smoke-tests/test/fixtures/setup.js index c17e9fbacee2c..fc54667eb7d77 100644 --- a/smoke-tests/test/fixtures/setup.js +++ b/smoke-tests/test/fixtures/setup.js @@ -9,7 +9,7 @@ const httpProxy = require('http-proxy') const { SMOKE_PUBLISH_NPM, SMOKE_PUBLISH_TARBALL, CI, PATH, Path, TAP_CHILD_ID = '0' } = process.env const PROXY_PORT = 12345 + (+TAP_CHILD_ID) -const HTTP_PROXY = `http://localhost:${PROXY_PORT}` +const HTTP_PROXY = `http://localhost:${PROXY_PORT}/` const NODE_PATH = process.execPath const CLI_ROOT = resolve(process.cwd(), '..') @@ -64,12 +64,13 @@ const getCleanPaths = async () => { }) } -const createRegistry = async (t, { debug } = {}) => { +const createRegistry = async (t, { debug, ...opts } = {}) => { const registry = new MockRegistry({ tap: t, registry: 'http://smoke-test-registry.club/', debug, strict: true, + ...opts, }) const proxy = httpProxy.createProxyServer({}) @@ -81,7 +82,7 @@ const createRegistry = async (t, { debug } = {}) => { return registry } -module.exports = async (t, { testdir = {}, debug } = {}) => { +module.exports = async (t, { testdir = {}, debug, registry: _registry = {} } = {}) => { const debugLog = debug || CI ? (...a) => console.error(...a) : () => {} const cleanPaths = await getCleanPaths() @@ -105,7 +106,7 @@ module.exports = async (t, { testdir = {}, debug } = {}) => { globalNodeModules: join(root, 'global', GLOBAL_NODE_MODULES), } - const registry = await createRegistry(t, { debug }) + const registry = await createRegistry(t, { ..._registry, debug }) // update notifier should never be written t.afterEach((t) => { @@ -168,7 +169,13 @@ module.exports = async (t, { testdir = {}, debug } = {}) => { return stdout } - const baseNpm = async ({ cwd, cmd, argv = [], proxy = true, ...opts } = {}, ...args) => { + const baseNpm = async (baseOpts, ...args) => { + const hasMoreOpts = args[args.length - 1] && typeof args[args.length - 1] === 'object' + const { cwd, cmd, argv = [], proxy = true, ...opts } = { + ...baseOpts, + ...hasMoreOpts ? args.pop() : {}, + } + const isGlobal = args.some(a => ['-g', '--global', '--global=true'].includes(a)) const defaultFlags = [ @@ -218,6 +225,10 @@ module.exports = async (t, { testdir = {}, debug } = {}) => { argv: [NPM_PATH], }, ...args) + const npmLocalError = async () => { + throw new Error('npmLocal cannot be called during smoke-publish') + } + // helpers for reading/writing files and their source const readFile = async (f) => { const file = await fs.readFile(join(paths.project, f), 'utf-8') @@ -226,15 +237,22 @@ module.exports = async (t, { testdir = {}, debug } = {}) => { return { npmPath, - npmLocal: SMOKE_PUBLISH_NPM ? async () => { - throw new Error('npmLocal cannot be called during smoke-publish') - } : npmLocal, + npmLocal: SMOKE_PUBLISH_NPM ? npmLocalError : npmLocal, npm: SMOKE_PUBLISH_NPM ? npmPath : npm, spawn: baseSpawn, readFile, getPath, paths, registry, + npmLocalTarball: async () => { + if (SMOKE_PUBLISH_TARBALL) { + return SMOKE_PUBLISH_TARBALL + } + if (SMOKE_PUBLISH_NPM) { + return await npmLocalError() + } + return await npmLocal('pack', `--pack-destination=${root}`).then(r => join(root, r)) + }, } } @@ -244,3 +262,4 @@ module.exports.CLI_ROOT = CLI_ROOT module.exports.WINDOWS = WINDOWS module.exports.SMOKE_PUBLISH = !!SMOKE_PUBLISH_NPM module.exports.SMOKE_PUBLISH_TARBALL = SMOKE_PUBLISH_TARBALL +module.exports.HTTP_PROXY = HTTP_PROXY diff --git a/smoke-tests/test/npm-replace-global.js b/smoke-tests/test/npm-replace-global.js index ee1ac5d89a778..a5303e48fca83 100644 --- a/smoke-tests/test/npm-replace-global.js +++ b/smoke-tests/test/npm-replace-global.js @@ -10,14 +10,40 @@ const which = async (cmd, opts) => { return path ? join(dirname(path), basename(path, extname(path))) : null } -t.test('npm replace global', async t => { +const setupNpmGlobal = async (t, opts) => { + const mock = await setup(t, opts) + + return { + ...mock, + getPaths: async () => { + const binContents = await fs.readdir(mock.paths.globalBin) + .then(r => r.filter(p => p !== '.npmrc' && p !== 'node_modules')) + .catch(() => null) + + const nodeModulesContents = await fs.readdir(join(mock.paths.globalNodeModules, 'npm')) + .catch(() => null) + + return { + npmRoot: await mock.npmPath('help').then(setup.getNpmRoot), + pathNpm: await which('npm', { path: mock.getPath(), nothrow: true }), + globalNpm: await which('npm', { nothrow: true }), + pathNpx: await which('npx', { path: mock.getPath(), nothrow: true }), + globalNpx: await which('npx', { nothrow: true }), + binContents, + nodeModulesContents, + } + }, + } +} + +t.test('pack and replace global self', async t => { const { npm, - npmLocal, + npmLocalTarball, npmPath, - getPath, - paths: { root, globalBin, globalNodeModules }, - } = await setup(t, { + getPaths, + paths: { globalBin, globalNodeModules }, + } = await setupNpmGlobal(t, { testdir: { project: { 'package.json': { name: 'npm', version: '999.999.999' }, @@ -25,25 +51,7 @@ t.test('npm replace global', async t => { }, }) - const getPaths = async () => { - const binContents = await fs.readdir(globalBin).then(results => results - .filter(p => p !== '.npmrc' && p !== 'node_modules') - .map(p => basename(p, extname(p))) - .reduce((set, p) => set.add(p), new Set())) - - return { - npmRoot: await npmPath('help').then(setup.getNpmRoot), - pathNpm: await which('npm', { path: getPath(), nothrow: true }), - globalNpm: await which('npm', { nothrow: true }), - pathNpx: await which('npx', { path: getPath(), nothrow: true }), - globalNpx: await which('npx', { nothrow: true }), - binContents: [...binContents], - nodeModulesContents: await fs.readdir(join(globalNodeModules, 'npm')), - } - } - - const tarball = setup.SMOKE_PUBLISH_TARBALL ?? - await npmLocal('pack', `--pack-destination=${root}`).then(r => join(root, r)) + const tarball = await npmLocalTarball() await npm('install', tarball, '--global') @@ -64,10 +72,15 @@ t.test('npm replace global', async t => { t.equal(prePaths.pathNpx, join(globalBin, 'npx'), 'npx bin is in the testdir') t.not(prePaths.pathNpm, prePaths.globalNpm, 'npm bin is not the same as the global one') t.not(prePaths.pathNpx, prePaths.globalNpx, 'npm bin is not the same as the global one') - t.match(prePaths.binContents, ['npm', 'npx'], 'bin has npm and npx') t.ok(prePaths.nodeModulesContents.length > 1, 'node modules has npm contents') t.ok(prePaths.nodeModulesContents.includes('node_modules'), 'npm has its node_modules') + t.strictSame( + prePaths.binContents, + ['npm', 'npx'].flatMap(p => setup.WINDOWS ? [p, `${p}.cmd`, `${p}.ps1`] : p), + 'bin has npm and npx' + ) + await npmPath('pack') await npmPath('install', 'npm-999.999.999.tgz', '--global') @@ -80,3 +93,67 @@ t.test('npm replace global', async t => { t.strictSame(postPaths.binContents, [], 'bin is empty') t.strictSame(postPaths.nodeModulesContents, ['package.json'], 'contents is only package.json') }) + +t.test('publish and replace global self', async t => { + const { + npm, + npmPath, + registry, + npmLocal, + npmLocalTarball, + getPaths, + paths: { globalBin, globalNodeModules }, + } = await setupNpmGlobal(t, { + testdir: { + home: { + '.npmrc': `${setup.HTTP_PROXY.slice(5)}:_authToken = test-token`, + }, + }, + }) + + const tarball = await npmLocalTarball() + + let publishedPackument = null + const pkg = require('../../package.json') + const { name, version } = pkg + + registry.nock.put('/npm', body => { + if (body._id === 'npm' && body.versions[version]) { + publishedPackument = body.versions[version] + return true + } + return false + }).reply(201, {}) + await npmLocal('publish', { proxy: true }) + + await registry.package({ + manifest: registry.manifest({ name, packuments: [publishedPackument] }), + tarballs: { [version]: tarball }, + times: 2, + }) + + await npm('install', 'npm', '--global', '--prefer-online') + + const paths = await getPaths() + t.equal(paths.npmRoot, join(globalNodeModules, 'npm'), 'npm root is in the testdir') + t.equal(paths.pathNpm, join(globalBin, 'npm'), 'npm bin is in the testdir') + t.equal(paths.pathNpx, join(globalBin, 'npx'), 'npx bin is in the testdir') + t.ok(paths.nodeModulesContents.length > 1, 'node modules has npm contents') + t.ok(paths.nodeModulesContents.includes('node_modules'), 'npm has its node_modules') + + t.strictSame( + paths.binContents, + ['npm', 'npx'].flatMap(p => setup.WINDOWS ? [p, `${p}.cmd`, `${p}.ps1`] : p), + 'bin has npm and npx' + ) + + await registry.package({ + manifest: registry.manifest({ name, packuments: [publishedPackument] }), + tarballs: { [version]: tarball }, + times: 2, + }) + + await npmPath('install', 'npm', '--global', '--prefer-online') + + t.strictSame(await getPaths(), paths) +}) diff --git a/tap-snapshots/test/lib/commands/publish.js.test.cjs b/tap-snapshots/test/lib/commands/publish.js.test.cjs index fd9fd74624070..60717193b3cef 100644 --- a/tap-snapshots/test/lib/commands/publish.js.test.cjs +++ b/tap-snapshots/test/lib/commands/publish.js.test.cjs @@ -99,6 +99,148 @@ exports[`test/lib/commands/publish.js TAP json > new package json 1`] = ` } ` +exports[`test/lib/commands/publish.js TAP manifest > manifest 1`] = ` +Object { + "_id": "npm@{VERSION}", + "author": Object { + "name": "GitHub Inc.", + }, + "bin": Object { + "npm": "bin/npm-cli.js", + "npx": "bin/npx-cli.js", + }, + "bugs": Object { + "url": "https://github.com/npm/cli/issues", + }, + "description": "a package manager for JavaScript", + "directories": Object { + "bin": "./bin", + "doc": "./doc", + "lib": "./lib", + "man": "./man", + }, + "exports": Object { + ".": Array [ + Object { + "default": "./index.js", + }, + "./index.js", + ], + "./package.json": "./package.json", + }, + "files": Array [ + "bin/", + "lib/", + "index.js", + "docs/content/", + "docs/output/", + "man/", + ], + "homepage": "https://docs.npmjs.com/", + "keywords": Array [ + "install", + "modules", + "package manager", + "package.json", + ], + "license": "Artistic-2.0", + "main": "./index.js", + "man": Array [ + "man/man1/npm-access.1", + "man/man1/npm-adduser.1", + "man/man1/npm-audit.1", + "man/man1/npm-bugs.1", + "man/man1/npm-cache.1", + "man/man1/npm-ci.1", + "man/man1/npm-completion.1", + "man/man1/npm-config.1", + "man/man1/npm-dedupe.1", + "man/man1/npm-deprecate.1", + "man/man1/npm-diff.1", + "man/man1/npm-dist-tag.1", + "man/man1/npm-docs.1", + "man/man1/npm-doctor.1", + "man/man1/npm-edit.1", + "man/man1/npm-exec.1", + "man/man1/npm-explain.1", + "man/man1/npm-explore.1", + "man/man1/npm-find-dupes.1", + "man/man1/npm-fund.1", + "man/man1/npm-help-search.1", + "man/man1/npm-help.1", + "man/man1/npm-hook.1", + "man/man1/npm-init.1", + "man/man1/npm-install-ci-test.1", + "man/man1/npm-install-test.1", + "man/man1/npm-install.1", + "man/man1/npm-link.1", + "man/man1/npm-login.1", + "man/man1/npm-logout.1", + "man/man1/npm-ls.1", + "man/man1/npm-org.1", + "man/man1/npm-outdated.1", + "man/man1/npm-owner.1", + "man/man1/npm-pack.1", + "man/man1/npm-ping.1", + "man/man1/npm-pkg.1", + "man/man1/npm-prefix.1", + "man/man1/npm-profile.1", + "man/man1/npm-prune.1", + "man/man1/npm-publish.1", + "man/man1/npm-query.1", + "man/man1/npm-rebuild.1", + "man/man1/npm-repo.1", + "man/man1/npm-restart.1", + "man/man1/npm-root.1", + "man/man1/npm-run-script.1", + "man/man1/npm-search.1", + "man/man1/npm-shrinkwrap.1", + "man/man1/npm-star.1", + "man/man1/npm-stars.1", + "man/man1/npm-start.1", + "man/man1/npm-stop.1", + "man/man1/npm-team.1", + "man/man1/npm-test.1", + "man/man1/npm-token.1", + "man/man1/npm-uninstall.1", + "man/man1/npm-unpublish.1", + "man/man1/npm-unstar.1", + "man/man1/npm-update.1", + "man/man1/npm-version.1", + "man/man1/npm-view.1", + "man/man1/npm-whoami.1", + "man/man1/npm.1", + "man/man1/npx.1", + "man/man5/folders.5", + "man/man5/install.5", + "man/man5/npm-global.5", + "man/man5/npm-json.5", + "man/man5/npm-shrinkwrap-json.5", + "man/man5/npmrc.5", + "man/man5/package-json.5", + "man/man5/package-lock-json.5", + "man/man7/config.7", + "man/man7/dependency-selectors.7", + "man/man7/developers.7", + "man/man7/logging.7", + "man/man7/orgs.7", + "man/man7/package-spec.7", + "man/man7/registry.7", + "man/man7/removal.7", + "man/man7/scope.7", + "man/man7/scripts.7", + "man/man7/workspaces.7", + ], + "name": "npm", + "readmeFilename": "README.md", + "repository": Object { + "type": "git", + "url": "git+https://github.com/npm/cli.git", + }, + "version": "{VERSION}", +} +` + exports[`test/lib/commands/publish.js TAP no auth dry-run > must match snapshot 1`] = ` + test-package@1.0.0 ` diff --git a/test/lib/commands/publish.js b/test/lib/commands/publish.js index 0781d94374cc2..820760bb5704d 100644 --- a/test/lib/commands/publish.js +++ b/test/lib/commands/publish.js @@ -719,3 +719,54 @@ t.test('public access', async t => { t.matchSnapshot(joinedOutput(), 'new package version') t.matchSnapshot(logs.notice) }) + +t.test('manifest', async t => { + // https://github.com/npm/cli/pull/6470#issuecomment-1571234863 + + // snapshot test that was generated against v9.6.7 originally to ensure our + // own manifest does not change unexpectedly when publishing. this test + // asserts a bunch of keys are there that will change often and then snapshots + // the rest of the manifest. + + const root = path.resolve(__dirname, '../../..') + const npmPkg = require(path.join(root, 'package.json')) + + t.cleanSnapshot = (s) => s.replace(new RegExp(npmPkg.version, 'g'), '{VERSION}') + + let manifest = null + const { npm } = await loadMockNpm(t, { + config: { + ...auth, + }, + chdir: () => root, + mocks: { + libnpmpublish: { + publish: (m) => manifest = m, + }, + }, + }) + await npm.exec('publish', []) + + const okKeys = [ + 'contributors', + 'bundleDependencies', + 'dependencies', + 'devDependencies', + 'templateOSS', + 'scripts', + 'tap', + 'readme', + 'gitHead', + 'engines', + 'workspaces', + ] + + for (const k of okKeys) { + t.ok(manifest[k], k) + delete manifest[k] + } + + manifest.man.sort() + + t.matchSnapshot(manifest, 'manifest') +})