From f3ac7b7460e1d9e1f9d3d8056317e36bb9813d5d Mon Sep 17 00:00:00 2001 From: Reggi Date: Fri, 6 Dec 2024 11:39:50 -0500 Subject: [PATCH] feat!: no implicit latest tag on publish when latest > version (#7939) BREAKING CHANGE: Upon publishing, in order to apply a default "latest" dist tag, the command now retrieves all prior versions of the package. It will require that the version you're trying to publish is above the latest semver version in the registry, not including pre-release tags. Implements [npm RFC7](https://github.com/npm/rfcs/blob/main/accepted/0007-publish-without-tag.md). Related to prerelease dist-tag: https://github.com/npm/cli/pull/7910 A part of npm 11 roadmap: https://github.com/npm/statusboard/issues/898 --------- Co-authored-by: Jordan Harband --- lib/commands/deprecate.js | 6 +- lib/commands/dist-tag.js | 8 +- lib/commands/doctor.js | 4 +- lib/commands/publish.js | 32 +- lib/commands/star.js | 6 +- lib/commands/stars.js | 4 +- lib/utils/ping.js | 4 +- lib/utils/verify-signatures.js | 6 +- mock-registry/lib/index.js | 61 +- smoke-tests/test/fixtures/setup.js | 6 +- smoke-tests/test/npm-replace-global.js | 1 + test/fixtures/mock-npm.js | 20 +- test/lib/commands/publish.js | 559 +++++++----------- workspaces/arborist/lib/audit-report.js | 4 +- workspaces/arborist/lib/query-selector-all.js | 4 +- workspaces/libnpmorg/lib/index.js | 8 +- 16 files changed, 346 insertions(+), 387 deletions(-) diff --git a/lib/commands/deprecate.js b/lib/commands/deprecate.js index 977fd9fce11da..95eaf429120fa 100644 --- a/lib/commands/deprecate.js +++ b/lib/commands/deprecate.js @@ -1,4 +1,4 @@ -const fetch = require('npm-registry-fetch') +const npmFetch = require('npm-registry-fetch') const { otplease } = require('../utils/auth.js') const npa = require('npm-package-arg') const { log } = require('proc-log') @@ -47,7 +47,7 @@ class Deprecate extends BaseCommand { } const uri = '/' + p.escapedName - const packument = await fetch.json(uri, { + const packument = await npmFetch.json(uri, { ...this.npm.flatOptions, spec: p, query: { write: true }, @@ -60,7 +60,7 @@ class Deprecate extends BaseCommand { for (const v of versions) { packument.versions[v].deprecated = msg } - return otplease(this.npm, this.npm.flatOptions, opts => fetch(uri, { + return otplease(this.npm, this.npm.flatOptions, opts => npmFetch(uri, { ...opts, spec: p, method: 'PUT', diff --git a/lib/commands/dist-tag.js b/lib/commands/dist-tag.js index ba9d6446c869f..3fdecd926a564 100644 --- a/lib/commands/dist-tag.js +++ b/lib/commands/dist-tag.js @@ -1,5 +1,5 @@ const npa = require('npm-package-arg') -const regFetch = require('npm-registry-fetch') +const npmFetch = require('npm-registry-fetch') const semver = require('semver') const { log, output } = require('proc-log') const { otplease } = require('../utils/auth.js') @@ -119,7 +119,7 @@ class DistTag extends BaseCommand { }, spec, } - await otplease(this.npm, reqOpts, o => regFetch(url, o)) + await otplease(this.npm, reqOpts, o => npmFetch(url, o)) output.standard(`+${t}: ${spec.name}@${version}`) } @@ -145,7 +145,7 @@ class DistTag extends BaseCommand { method: 'DELETE', spec, } - await otplease(this.npm, reqOpts, o => regFetch(url, o)) + await otplease(this.npm, reqOpts, o => npmFetch(url, o)) output.standard(`-${tag}: ${spec.name}@${version}`) } @@ -191,7 +191,7 @@ class DistTag extends BaseCommand { } async fetchTags (spec, opts) { - const data = await regFetch.json( + const data = await npmFetch.json( `/-/package/${spec.escapedName}/dist-tags`, { ...opts, 'prefer-online': true, spec } ) diff --git a/lib/commands/doctor.js b/lib/commands/doctor.js index 64a8423a32fa3..8f87fdc17891c 100644 --- a/lib/commands/doctor.js +++ b/lib/commands/doctor.js @@ -1,6 +1,6 @@ const cacache = require('cacache') const { access, lstat, readdir, constants: { R_OK, W_OK, X_OK } } = require('node:fs/promises') -const fetch = require('make-fetch-happen') +const npmFetch = require('make-fetch-happen') const which = require('which') const pacote = require('pacote') const { resolve } = require('node:path') @@ -166,7 +166,7 @@ class Doctor extends BaseCommand { const currentRange = `^${current}` const url = 'https://nodejs.org/dist/index.json' log.info('doctor', 'Getting Node.js release information') - const res = await fetch(url, { method: 'GET', ...this.npm.flatOptions }) + const res = await npmFetch(url, { method: 'GET', ...this.npm.flatOptions }) const data = await res.json() let maxCurrent = '0.0.0' let maxLTS = '0.0.0' diff --git a/lib/commands/publish.js b/lib/commands/publish.js index d15bb5a2eb272..c59588fefb241 100644 --- a/lib/commands/publish.js +++ b/lib/commands/publish.js @@ -120,7 +120,7 @@ class Publish extends BaseCommand { const isDefaultTag = this.npm.config.isDefault('tag') if (isPreRelease && isDefaultTag) { - throw new Error('You must specify a tag using --tag when publishing a prerelease version') + throw new Error('You must specify a tag using --tag when publishing a prerelease version.') } // If we are not in JSON mode then we show the user the contents of the tarball @@ -157,6 +157,14 @@ class Publish extends BaseCommand { } } + const latestVersion = await this.#latestPublishedVersion(resolved, registry) + const latestSemverIsGreater = !!latestVersion && semver.gte(latestVersion, manifest.version) + + if (latestSemverIsGreater && isDefaultTag) { + /* eslint-disable-next-line max-len */ + throw new Error(`Cannot implicitly apply the "latest" tag because published version ${latestVersion} is higher than the new version ${manifest.version}. You must specify a tag using --tag.`) + } + const access = opts.access === null ? 'default' : opts.access let msg = `Publishing to ${outputRegistry} with tag ${defaultTag} and ${access} access` if (dryRun) { @@ -196,6 +204,28 @@ class Publish extends BaseCommand { } } + async #latestPublishedVersion (spec, registry) { + try { + const packument = await pacote.packument(spec, { + ...this.npm.flatOptions, + preferOnline: true, + registry, + }) + if (typeof packument?.versions === 'undefined') { + return null + } + const ordered = Object.keys(packument?.versions) + .flatMap(v => { + const s = new semver.SemVer(v) + return s.prerelease.length > 0 ? [] : s + }) + .sort((a, b) => b.compare(a)) + return ordered.length >= 1 ? ordered[0].version : null + } catch (e) { + return null + } + } + // if it's a directory, read it from the file system // otherwise, get the full metadata from whatever it is // XXX can't pacote read the manifest from a directory? diff --git a/lib/commands/star.js b/lib/commands/star.js index 1b76955810c72..7d1be1d389730 100644 --- a/lib/commands/star.js +++ b/lib/commands/star.js @@ -1,4 +1,4 @@ -const fetch = require('npm-registry-fetch') +const npmFetch = require('npm-registry-fetch') const npa = require('npm-package-arg') const { log, output } = require('proc-log') const getIdentity = require('../utils/get-identity') @@ -32,7 +32,7 @@ class Star extends BaseCommand { const username = await getIdentity(this.npm, this.npm.flatOptions) for (const pkg of pkgs) { - const fullData = await fetch.json(pkg.escapedName, { + const fullData = await npmFetch.json(pkg.escapedName, { ...this.npm.flatOptions, spec: pkg, query: { write: true }, @@ -55,7 +55,7 @@ class Star extends BaseCommand { log.verbose('unstar', 'unstarring', body) } - const data = await fetch.json(pkg.escapedName, { + const data = await npmFetch.json(pkg.escapedName, { ...this.npm.flatOptions, spec: pkg, method: 'PUT', diff --git a/lib/commands/stars.js b/lib/commands/stars.js index 1059569979daf..d059d01250900 100644 --- a/lib/commands/stars.js +++ b/lib/commands/stars.js @@ -1,4 +1,4 @@ -const fetch = require('npm-registry-fetch') +const npmFetch = require('npm-registry-fetch') const { log, output } = require('proc-log') const getIdentity = require('../utils/get-identity.js') const BaseCommand = require('../base-cmd.js') @@ -16,7 +16,7 @@ class Stars extends BaseCommand { user = await getIdentity(this.npm, this.npm.flatOptions) } - const { rows } = await fetch.json('/-/_view/starredByUser', { + const { rows } = await npmFetch.json('/-/_view/starredByUser', { ...this.npm.flatOptions, query: { key: `"${user}"` }, }) diff --git a/lib/utils/ping.js b/lib/utils/ping.js index 1c8c9e827a4ea..3d47ca1ecaf54 100644 --- a/lib/utils/ping.js +++ b/lib/utils/ping.js @@ -1,7 +1,7 @@ // ping the npm registry // used by the ping and doctor commands -const fetch = require('npm-registry-fetch') +const npmFetch = require('npm-registry-fetch') module.exports = async (flatOptions) => { - const res = await fetch('/-/ping', { ...flatOptions, cache: false }) + const res = await npmFetch('/-/ping', { ...flatOptions, cache: false }) return res.json().catch(() => ({})) } diff --git a/lib/utils/verify-signatures.js b/lib/utils/verify-signatures.js index eca8e78c2e354..0a32742b5ee2a 100644 --- a/lib/utils/verify-signatures.js +++ b/lib/utils/verify-signatures.js @@ -1,4 +1,4 @@ -const fetch = require('npm-registry-fetch') +const npmFetch = require('npm-registry-fetch') const localeCompare = require('@isaacs/string-locale-compare')('en') const npa = require('npm-package-arg') const pacote = require('pacote') @@ -202,7 +202,7 @@ class VerifySignatures { // If keys not found in Sigstore TUF repo, fallback to registry keys API if (!keys) { - keys = await fetch.json('/-/npm/v1/keys', { + keys = await npmFetch.json('/-/npm/v1/keys', { ...this.npm.flatOptions, registry, }).then(({ keys: ks }) => ks.map((key) => ({ @@ -253,7 +253,7 @@ class VerifySignatures { } getSpecRegistry (spec) { - return fetch.pickRegistry(spec, this.npm.flatOptions) + return npmFetch.pickRegistry(spec, this.npm.flatOptions) } getValidPackageInfo (edge) { diff --git a/mock-registry/lib/index.js b/mock-registry/lib/index.js index 8fdb46902a373..3b06681b7ed31 100644 --- a/mock-registry/lib/index.js +++ b/mock-registry/lib/index.js @@ -351,13 +351,30 @@ class MockRegistry { } // full unpublish of an entire package - async unpublish ({ manifest }) { + unpublish ({ manifest }) { let nock = this.nock const spec = npa(manifest.name) nock = nock.delete(this.fullPath(`/${spec.escapedName}/-rev/${manifest._rev}`)).reply(201) return nock } + publish (name, { + packageJson, access, noPut, putCode, manifest, packuments, + } = {}) { + // this getPackage call is used to get the latest semver version before publish + if (manifest) { + this.getPackage(name, { code: 200, resp: manifest }) + } else if (packuments) { + this.getPackage(name, { code: 200, resp: this.manifest({ name, packuments }) }) + } else { + // assumes the package does not exist yet and will 404 x2 from pacote.manifest + this.getPackage(name, { times: 2, code: 404 }) + } + if (!noPut) { + this.putPackage(name, { code: putCode, packageJson, access }) + } + } + getPackage (name, { times = 1, code = 200, query, resp = {} }) { let nock = this.nock nock = nock.get(`/${npa(name).escapedName}`).times(times) @@ -372,6 +389,48 @@ class MockRegistry { this.nock = nock } + putPackage (name, { code = 200, resp = {}, ...putPackagePayload }) { + this.nock.put(`/${npa(name).escapedName}`, body => { + return this.#tap.match(body, this.putPackagePayload({ name, ...putPackagePayload })) + }).reply(code, resp) + } + + putPackagePayload (opts) { + const pkg = opts.packageJson + const name = opts.name || pkg?.name + const registry = opts.registry || pkg?.publishConfig?.registry || 'https://registry.npmjs.org' + const access = opts.access || null + + const nameProperties = !name ? {} : { + _id: name, + name: name, + } + + const packageProperties = !pkg ? {} : { + 'dist-tags': { latest: pkg.version }, + versions: { + [pkg.version]: { + _id: `${pkg.name}@${pkg.version}`, + dist: { + shasum: /\.*/, + tarball: + `http://${new URL(registry).host}/${pkg.name}/-/${pkg.name}-${pkg.version}.tgz`, + }, + ...pkg, + }, + }, + _attachments: { + [`${pkg.name}-${pkg.version}.tgz`]: {}, + }, + } + + return { + access, + ...nameProperties, + ...packageProperties, + } + } + getTokens (tokens) { return this.nock.get('/-/npm/v1/tokens') .reply(200, { diff --git a/smoke-tests/test/fixtures/setup.js b/smoke-tests/test/fixtures/setup.js index 18492d0d52f8f..2d0c54c984243 100644 --- a/smoke-tests/test/fixtures/setup.js +++ b/smoke-tests/test/fixtures/setup.js @@ -73,7 +73,9 @@ const getCleanPaths = async () => { }) } -module.exports = async (t, { testdir = {}, debug, mockRegistry = true, useProxy = false } = {}) => { +module.exports = async (t, { + testdir = {}, debug, mockRegistry = true, strictRegistryNock = true, useProxy = false, +} = {}) => { const debugLog = debug || CI ? (...a) => t.comment(...a) : () => {} debugLog({ SMOKE_PUBLISH_TARBALL, CI }) @@ -103,7 +105,7 @@ module.exports = async (t, { testdir = {}, debug, mockRegistry = true, useProxy tap: t, registry: MOCK_REGISTRY, debug, - strict: true, + strict: strictRegistryNock, }) const proxyEnv = {} diff --git a/smoke-tests/test/npm-replace-global.js b/smoke-tests/test/npm-replace-global.js index d3638158e2ca6..c53beb10875af 100644 --- a/smoke-tests/test/npm-replace-global.js +++ b/smoke-tests/test/npm-replace-global.js @@ -103,6 +103,7 @@ t.test('publish and replace global self', async t => { getPaths, paths: { globalBin, globalNodeModules, cache }, } = await setupNpmGlobal(t, { + strictRegistryNock: false, testdir: { home: { '.npmrc': `//${setup.MOCK_REGISTRY.host}/:_authToken = test-token`, diff --git a/test/fixtures/mock-npm.js b/test/fixtures/mock-npm.js index 9e9113972d6a3..bfeb9a05615a2 100644 --- a/test/fixtures/mock-npm.js +++ b/test/fixtures/mock-npm.js @@ -292,12 +292,26 @@ const setupMockNpm = async (t, { const loadNpmWithRegistry = async (t, opts) => { const mock = await setupMockNpm(t, opts) + return { + ...mock, + ...loadRegistry(t, mock, opts), + ...loadFsAssertions(t, mock), + } +} + +const loadRegistry = (t, mock, opts) => { const registry = new MockRegistry({ tap: t, - registry: mock.npm.config.get('registry'), - strict: true, + registry: opts.registry ?? mock.npm.config.get('registry'), + authorization: opts.authorization, + basic: opts.basic, + debug: opts.debugRegistry ?? false, + strict: opts.strictRegistryNock ?? true, }) + return { registry } +} +const loadFsAssertions = (t, mock) => { const fileShouldExist = (filePath) => { t.equal( fsSync.existsSync(path.join(mock.npm.prefix, filePath)), true, `${filePath} should exist` @@ -352,7 +366,7 @@ const loadNpmWithRegistry = async (t, opts) => { packageDirty, } - return { registry, assert, ...mock } + return { assert } } /** breaks down a spec "abbrev@1.1.1" into different parts for mocking */ diff --git a/test/lib/commands/publish.js b/test/lib/commands/publish.js index 5462b30cb7a79..10dc9b33deda4 100644 --- a/test/lib/commands/publish.js +++ b/test/lib/commands/publish.js @@ -1,12 +1,10 @@ const t = require('tap') -const { load: loadMockNpm } = require('../../fixtures/mock-npm') +const { loadNpmWithRegistry } = require('../../fixtures/mock-npm') const { cleanZlib } = require('../../fixtures/clean-snapshot') -const MockRegistry = require('@npmcli/mock-registry') const pacote = require('pacote') const Arborist = require('@npmcli/arborist') const path = require('node:path') const fs = require('node:fs') -const npa = require('npm-package-arg') const pkg = 'test-package' const token = 'test-auth-token' @@ -23,54 +21,28 @@ const pkgJson = { t.cleanSnapshot = data => cleanZlib(data) t.test('respects publishConfig.registry, runs appropriate scripts', async t => { - const { npm, joinedOutput, prefix } = await loadMockNpm(t, { + const packageJson = { + ...pkgJson, + scripts: { + prepublishOnly: 'touch scripts-prepublishonly', + prepublish: 'touch scripts-prepublish', // should NOT run this one + publish: 'touch scripts-publish', + postpublish: 'touch scripts-postpublish', + }, + publishConfig: { registry: alternateRegistry }, + } + const { npm, joinedOutput, prefix, registry } = await loadNpmWithRegistry(t, { config: { loglevel: 'silent', [`${alternateRegistry.slice(6)}/:_authToken`]: 'test-other-token', }, prefixDir: { - 'package.json': JSON.stringify({ - ...pkgJson, - scripts: { - prepublishOnly: 'touch scripts-prepublishonly', - prepublish: 'touch scripts-prepublish', // should NOT run this one - publish: 'touch scripts-publish', - postpublish: 'touch scripts-postpublish', - }, - publishConfig: { registry: alternateRegistry }, - }, null, 2), + 'package.json': JSON.stringify(packageJson, null, 2), }, - }) - const registry = new MockRegistry({ - tap: t, registry: alternateRegistry, authorization: 'test-other-token', }) - registry.nock.put(`/${pkg}`, body => { - return t.match(body, { - _id: pkg, - name: pkg, - 'dist-tags': { latest: '1.0.0' }, - access: null, - versions: { - '1.0.0': { - name: pkg, - version: '1.0.0', - _id: `${pkg}@1.0.0`, - dist: { - shasum: /\.*/, - tarball: `http:${alternateRegistry.slice(6)}/test-package/-/test-package-1.0.0.tgz`, - }, - publishConfig: { - registry: alternateRegistry, - }, - }, - }, - _attachments: { - [`${pkg}-1.0.0.tgz`]: {}, - }, - }) - }).reply(200, {}) + registry.publish(pkg, { packageJson }) await npm.exec('publish', []) t.matchSnapshot(joinedOutput(), 'new package version') t.equal(fs.existsSync(path.join(prefix, 'scripts-prepublishonly')), true, 'ran prepublishOnly') @@ -80,115 +52,66 @@ t.test('respects publishConfig.registry, runs appropriate scripts', async t => { }) t.test('re-loads publishConfig.registry if added during script process', async t => { - const { joinedOutput, npm } = await loadMockNpm(t, { + const initPackageJson = { + ...pkgJson, + scripts: { + prepare: 'cp new.json package.json', + }, + } + const packageJson = { + ...initPackageJson, + publishConfig: { registry: alternateRegistry }, + } + const { joinedOutput, npm, registry } = await loadNpmWithRegistry(t, { config: { [`${alternateRegistry.slice(6)}/:_authToken`]: 'test-other-token', // Keep output from leaking into tap logs for readability 'foreground-scripts': false, }, prefixDir: { - 'package.json': JSON.stringify({ - ...pkgJson, - scripts: { - prepare: 'cp new.json package.json', - }, - }, null, 2), - 'new.json': JSON.stringify({ - ...pkgJson, - publishConfig: { registry: alternateRegistry }, - }), + 'package.json': JSON.stringify(initPackageJson, null, 2), + 'new.json': JSON.stringify(packageJson, null, 2), }, - }) - const registry = new MockRegistry({ - tap: t, registry: alternateRegistry, authorization: 'test-other-token', }) - registry.nock.put(`/${pkg}`, body => { - return t.match(body, { - _id: pkg, - name: pkg, - 'dist-tags': { latest: '1.0.0' }, - access: null, - versions: { - '1.0.0': { - name: pkg, - version: '1.0.0', - _id: `${pkg}@1.0.0`, - dist: { - shasum: /\.*/, - tarball: `http:${alternateRegistry.slice(6)}/test-package/-/test-package-1.0.0.tgz`, - }, - publishConfig: { - registry: alternateRegistry, - }, - }, - }, - _attachments: { - [`${pkg}-1.0.0.tgz`]: {}, - }, - }) - }).reply(200, {}) + registry.publish(pkg, { packageJson }) await npm.exec('publish', []) t.matchSnapshot(joinedOutput(), 'new package version') }) t.test('prioritize CLI flags over publishConfig', async t => { - const publishConfig = { registry: 'http://publishconfig' } - const { joinedOutput, npm } = await loadMockNpm(t, { + const initPackageJson = { + ...pkgJson, + scripts: { + prepare: 'cp new.json package.json', + }, + } + const packageJson = { + ...initPackageJson, + publishConfig: { registry: alternateRegistry }, + } + const { joinedOutput, npm, registry } = await loadNpmWithRegistry(t, { config: { [`${alternateRegistry.slice(6)}/:_authToken`]: 'test-other-token', // Keep output from leaking into tap logs for readability 'foreground-scripts': false, }, prefixDir: { - 'package.json': JSON.stringify({ - ...pkgJson, - scripts: { - prepare: 'cp new.json package.json', - }, - }, null, 2), - 'new.json': JSON.stringify({ - ...pkgJson, - publishConfig, - }), + 'package.json': JSON.stringify(initPackageJson, null, 2), + 'new.json': JSON.stringify(packageJson, null, 2), }, argv: ['--registry', alternateRegistry], - }) - const registry = new MockRegistry({ - tap: t, - registry: alternateRegistry, + registryUrl: alternateRegistry, authorization: 'test-other-token', }) - registry.nock.put(`/${pkg}`, body => { - return t.match(body, { - _id: pkg, - name: pkg, - 'dist-tags': { latest: '1.0.0' }, - access: null, - versions: { - '1.0.0': { - name: pkg, - version: '1.0.0', - _id: `${pkg}@1.0.0`, - dist: { - shasum: /\.*/, - tarball: `http:${alternateRegistry.slice(6)}/test-package/-/test-package-1.0.0.tgz`, - }, - publishConfig, - }, - }, - _attachments: { - [`${pkg}-1.0.0.tgz`]: {}, - }, - }) - }).reply(200, {}) + registry.publish(pkg, { packageJson }) await npm.exec('publish', []) t.matchSnapshot(joinedOutput(), 'new package version') }) t.test('json', async t => { - const { joinedOutput, npm, logs } = await loadMockNpm(t, { + const { joinedOutput, npm, logs, registry } = await loadNpmWithRegistry(t, { config: { json: true, ...auth, @@ -196,20 +119,16 @@ t.test('json', async t => { prefixDir: { 'package.json': JSON.stringify(pkgJson, null, 2), }, - }) - const registry = new MockRegistry({ - tap: t, - registry: npm.config.get('registry'), authorization: token, }) - registry.nock.put(`/${pkg}`).reply(200, {}) + registry.publish(pkg) await npm.exec('publish', []) t.matchSnapshot(logs.notice) t.matchSnapshot(joinedOutput(), 'new package json') }) t.test('dry-run', async t => { - const { joinedOutput, npm, logs } = await loadMockNpm(t, { + const { joinedOutput, npm, logs, registry } = await loadNpmWithRegistry(t, { config: { 'dry-run': true, ...auth, @@ -217,14 +136,16 @@ t.test('dry-run', async t => { prefixDir: { 'package.json': JSON.stringify(pkgJson, null, 2), }, + authorization: token, }) + registry.publish(pkg, { noPut: true }) await npm.exec('publish', []) t.equal(joinedOutput(), `+ ${pkg}@1.0.0`) t.matchSnapshot(logs.notice) }) t.test('foreground-scripts defaults to true', async t => { - const { outputs, npm, logs } = await loadMockNpm(t, { + const { outputs, npm, logs, registry } = await loadNpmWithRegistry(t, { config: { 'dry-run': true, ...auth, @@ -241,11 +162,9 @@ t.test('foreground-scripts defaults to true', async t => { ), }, }) - + registry.publish('test-fg-scripts', { noPut: true }) await npm.exec('publish', []) - t.matchSnapshot(logs.notice) - t.strictSame( outputs, [ @@ -257,7 +176,7 @@ t.test('foreground-scripts defaults to true', async t => { }) t.test('foreground-scripts can still be set to false', async t => { - const { outputs, npm, logs } = await loadMockNpm(t, { + const { outputs, npm, logs, registry } = await loadNpmWithRegistry(t, { config: { 'dry-run': true, 'foreground-scripts': false, @@ -276,6 +195,7 @@ t.test('foreground-scripts can still be set to false', async t => { }, }) + registry.publish('test-fg-scripts', { noPut: true }) await npm.exec('publish', []) t.matchSnapshot(logs.notice) @@ -287,12 +207,12 @@ t.test('foreground-scripts can still be set to false', async t => { }) t.test('shows usage with wrong set of arguments', async t => { - const { publish } = await loadMockNpm(t, { command: 'publish' }) + const { publish } = await loadNpmWithRegistry(t, { command: 'publish' }) await t.rejects(publish.exec(['a', 'b', 'c']), publish.usage) }) t.test('throws when invalid tag is semver', async t => { - const { npm } = await loadMockNpm(t, { + const { npm } = await loadNpmWithRegistry(t, { config: { tag: '0.0.13', }, @@ -307,7 +227,7 @@ t.test('throws when invalid tag is semver', async t => { }) t.test('throws when invalid tag when not url encodable', async t => { - const { npm } = await loadMockNpm(t, { + const { npm } = await loadNpmWithRegistry(t, { config: { tag: '@test', }, @@ -325,7 +245,7 @@ t.test('throws when invalid tag when not url encodable', async t => { }) t.test('tarball', async t => { - const { npm, joinedOutput, logs, home } = await loadMockNpm(t, { + const { npm, joinedOutput, logs, home, registry } = await loadNpmWithRegistry(t, { config: { 'fetch-retries': 0, ...auth, @@ -338,27 +258,19 @@ t.test('tarball', async t => { }, null, 2), 'index.js': 'console.log("hello world"}', }, + authorization: token, }) const tarball = await pacote.tarball(home, { Arborist }) const tarFilename = path.join(home, 'tarball.tgz') fs.writeFileSync(tarFilename, tarball) - const registry = new MockRegistry({ - tap: t, - registry: npm.config.get('registry'), - authorization: token, - }) - registry.nock.put('/test-tar-package', body => { - return t.match(body, { - name: 'test-tar-package', - }) - }).reply(200, {}) + registry.publish('test-tar-package') await npm.exec('publish', [tarFilename]) t.matchSnapshot(logs.notice) t.matchSnapshot(joinedOutput(), 'new package json') }) t.test('no auth default registry', async t => { - const { npm } = await loadMockNpm(t, { + const { npm } = await loadNpmWithRegistry(t, { prefixDir: { 'package.json': JSON.stringify(pkgJson, null, 2), }, @@ -373,7 +285,7 @@ t.test('no auth default registry', async t => { }) t.test('no auth dry-run', async t => { - const { npm, joinedOutput, logs } = await loadMockNpm(t, { + const { npm, joinedOutput, logs, registry } = await loadNpmWithRegistry(t, { config: { 'dry-run': true, }, @@ -381,13 +293,14 @@ t.test('no auth dry-run', async t => { 'package.json': JSON.stringify(pkgJson, null, 2), }, }) + registry.publish(pkg, { noPut: true }) await npm.exec('publish', []) t.matchSnapshot(joinedOutput()) t.matchSnapshot(logs.warn, 'warns about auth being needed') }) t.test('no auth for configured registry', async t => { - const { npm } = await loadMockNpm(t, { + const { npm } = await loadNpmWithRegistry(t, { config: { registry: alternateRegistry, ...auth, @@ -406,7 +319,7 @@ t.test('no auth for configured registry', async t => { }) t.test('no auth for scope configured registry', async t => { - const { npm } = await loadMockNpm(t, { + const { npm } = await loadNpmWithRegistry(t, { config: { scope: '@npm', registry: alternateRegistry, @@ -429,8 +342,7 @@ t.test('no auth for scope configured registry', async t => { }) t.test('has token auth for scope configured registry', async t => { - const spec = npa('@npm/test-package') - const { npm, joinedOutput } = await loadMockNpm(t, { + const { npm, joinedOutput, registry } = await loadNpmWithRegistry(t, { config: { scope: '@npm', registry: alternateRegistry, @@ -442,22 +354,16 @@ t.test('has token auth for scope configured registry', async t => { version: '1.0.0', }, null, 2), }, - }) - const registry = new MockRegistry({ - tap: t, registry: alternateRegistry, authorization: 'test-scope-token', }) - registry.nock.put(`/${spec.escapedName}`, body => { - return t.match(body, { name: '@npm/test-package' }) - }).reply(200, {}) + registry.publish('@npm/test-package') await npm.exec('publish', []) t.matchSnapshot(joinedOutput(), 'new package version') }) t.test('has mTLS auth for scope configured registry', async t => { - const spec = npa('@npm/test-package') - const { npm, joinedOutput } = await loadMockNpm(t, { + const { npm, joinedOutput, registry } = await loadNpmWithRegistry(t, { config: { scope: '@npm', registry: alternateRegistry, @@ -470,14 +376,9 @@ t.test('has mTLS auth for scope configured registry', async t => { version: '1.0.0', }, null, 2), }, - }) - const registry = new MockRegistry({ - tap: t, registry: alternateRegistry, }) - registry.nock.put(`/${spec.escapedName}`, body => { - return t.match(body, { name: '@npm/test-package' }) - }).reply(200, {}) + registry.publish('@npm/test-package') await npm.exec('publish', []) t.matchSnapshot(joinedOutput(), 'new package version') }) @@ -519,7 +420,7 @@ t.test('workspaces', t => { } t.test('all workspaces - no color', async t => { - const { npm, joinedOutput, logs } = await loadMockNpm(t, { + const { npm, joinedOutput, logs, registry } = await loadNpmWithRegistry(t, { config: { tag: 'latest', color: false, @@ -527,29 +428,18 @@ t.test('workspaces', t => { workspaces: true, }, prefixDir: dir, - }) - const registry = new MockRegistry({ - tap: t, - registry: npm.config.get('registry'), authorization: token, }) - registry.nock - .put('/workspace-a', body => { - return t.match(body, { name: 'workspace-a' }) - }).reply(200, {}) - .put('/workspace-b', body => { - return t.match(body, { name: 'workspace-b' }) - }).reply(200, {}) - .put('/workspace-n', body => { - return t.match(body, { name: 'workspace-n' }) - }).reply(200, {}) + ;['workspace-a', 'workspace-b', 'workspace-n'].forEach(name => { + registry.publish(name) + }) await npm.exec('publish', []) t.matchSnapshot(joinedOutput(), 'all public workspaces') t.matchSnapshot(logs.warn, 'warns about skipped private workspace') }) t.test('all workspaces - color', async t => { - const { npm, joinedOutput, logs } = await loadMockNpm(t, { + const { npm, joinedOutput, logs, registry } = await loadNpmWithRegistry(t, { config: { ...auth, tag: 'latest', @@ -557,67 +447,44 @@ t.test('workspaces', t => { workspaces: true, }, prefixDir: dir, - }) - const registry = new MockRegistry({ - tap: t, - registry: npm.config.get('registry'), authorization: token, }) - registry.nock - .put('/workspace-a', body => { - return t.match(body, { name: 'workspace-a' }) - }).reply(200, {}) - .put('/workspace-b', body => { - return t.match(body, { name: 'workspace-b' }) - }).reply(200, {}) - .put('/workspace-n', body => { - return t.match(body, { name: 'workspace-n' }) - }).reply(200, {}) + ;['workspace-a', 'workspace-b', 'workspace-n'].forEach(name => { + registry.publish(name) + }) await npm.exec('publish', []) t.matchSnapshot(joinedOutput(), 'all public workspaces') t.matchSnapshot(logs.warn, 'warns about skipped private workspace in color') }) t.test('one workspace - success', async t => { - const { npm, joinedOutput } = await loadMockNpm(t, { + const { npm, joinedOutput, registry } = await loadNpmWithRegistry(t, { config: { ...auth, tag: 'latest', workspace: ['workspace-a'], }, prefixDir: dir, - }) - const registry = new MockRegistry({ - tap: t, - registry: npm.config.get('registry'), authorization: token, }) - registry.nock - .put('/workspace-a', body => { - return t.match(body, { name: 'workspace-a' }) - }).reply(200, {}) + ;['workspace-a'].forEach(name => { + registry.publish(name) + }) await npm.exec('publish', []) t.matchSnapshot(joinedOutput(), 'single workspace') }) t.test('one workspace - failure', async t => { - const { npm } = await loadMockNpm(t, { + const { npm, registry } = await loadNpmWithRegistry(t, { config: { ...auth, tag: 'latest', workspace: ['workspace-a'], }, prefixDir: dir, - }) - const registry = new MockRegistry({ - tap: t, - registry: npm.config.get('registry'), authorization: token, }) - registry.nock - .put('/workspace-a', body => { - return t.match(body, { name: 'workspace-a' }) - }).reply(404, {}) + registry.publish('workspace-a', { putCode: 404 }) await t.rejects(npm.exec('publish', []), { code: 'E404' }) }) @@ -643,29 +510,22 @@ t.test('workspaces', t => { }, } - const { npm, joinedOutput } = await loadMockNpm(t, { + const { npm, joinedOutput, registry } = await loadNpmWithRegistry(t, { config: { ...auth, tag: 'latest', workspaces: true, }, prefixDir: testDir, - }) - const registry = new MockRegistry({ - tap: t, - registry: npm.config.get('registry'), authorization: token, }) - registry.nock - .put('/workspace-a', body => { - return t.match(body, { name: 'workspace-a' }) - }).reply(200, {}) + registry.publish('workspace-a') await npm.exec('publish', []) t.matchSnapshot(joinedOutput(), 'one marked private') }) t.test('invalid workspace', async t => { - const { npm } = await loadMockNpm(t, { + const { npm } = await loadNpmWithRegistry(t, { config: { ...auth, tag: 'latest', @@ -680,7 +540,7 @@ t.test('workspaces', t => { }) t.test('json', async t => { - const { npm, joinedOutput } = await loadMockNpm(t, { + const { npm, joinedOutput, registry } = await loadNpmWithRegistry(t, { config: { ...auth, tag: 'latest', @@ -688,22 +548,11 @@ t.test('workspaces', t => { json: true, }, prefixDir: dir, - }) - const registry = new MockRegistry({ - tap: t, - registry: npm.config.get('registry'), authorization: token, }) - registry.nock - .put('/workspace-a', body => { - return t.match(body, { name: 'workspace-a' }) - }).reply(200, {}) - .put('/workspace-b', body => { - return t.match(body, { name: 'workspace-b' }) - }).reply(200, {}) - .put('/workspace-n', body => { - return t.match(body, { name: 'workspace-n' }) - }).reply(200, {}) + ;['workspace-a', 'workspace-b', 'workspace-n'].forEach(name => { + registry.publish(name) + }) await npm.exec('publish', []) t.matchSnapshot(joinedOutput(), 'all workspaces in json') }) @@ -729,23 +578,16 @@ t.test('workspaces', t => { }, } - const { npm, joinedOutput } = await loadMockNpm(t, { + const { npm, joinedOutput, registry } = await loadNpmWithRegistry(t, { config: { ...auth, tag: 'latest', }, prefixDir: testDir, chdir: ({ prefix }) => path.resolve(prefix, './workspace-a'), - }) - const registry = new MockRegistry({ - tap: t, - registry: npm.config.get('registry'), authorization: token, }) - registry.nock - .put('/pkg', body => { - return t.match(body, { name: 'pkg' }) - }).reply(200, {}) + registry.publish('pkg') await npm.exec('publish', ['../dir/pkg']) t.matchSnapshot(joinedOutput(), 'publish different package spec') }) @@ -754,7 +596,7 @@ t.test('workspaces', t => { }) t.test('ignore-scripts', async t => { - const { npm, joinedOutput, prefix } = await loadMockNpm(t, { + const { npm, joinedOutput, prefix, registry } = await loadNpmWithRegistry(t, { config: { ...auth, 'ignore-scripts': true, @@ -770,13 +612,9 @@ t.test('ignore-scripts', async t => { }, }, null, 2), }, - }) - const registry = new MockRegistry({ - tap: t, - registry: npm.config.get('registry'), authorization: token, }) - registry.nock.put(`/${pkg}`).reply(200, {}) + registry.publish(pkg) await npm.exec('publish', []) t.matchSnapshot(joinedOutput(), 'new package version') t.equal( @@ -802,27 +640,22 @@ t.test('ignore-scripts', async t => { }) t.test('_auth config default registry', async t => { - const { npm, joinedOutput } = await loadMockNpm(t, { + const { npm, joinedOutput, registry } = await loadNpmWithRegistry(t, { config: { '//registry.npmjs.org/:_auth': basic, }, prefixDir: { 'package.json': JSON.stringify(pkgJson), }, - }) - const registry = new MockRegistry({ - tap: t, - registry: npm.config.get('registry'), basic, }) - registry.nock.put(`/${pkg}`).reply(200, {}) + registry.publish(pkg) await npm.exec('publish', []) t.matchSnapshot(joinedOutput(), 'new package version') }) t.test('bare _auth and registry config', async t => { - const spec = npa('@npm/test-package') - const { npm, joinedOutput } = await loadMockNpm(t, { + const { npm, joinedOutput, registry } = await loadNpmWithRegistry(t, { config: { registry: alternateRegistry, '//other.registry.npmjs.org/:_auth': basic, @@ -833,19 +666,16 @@ t.test('bare _auth and registry config', async t => { version: '1.0.0', }, null, 2), }, - }) - const registry = new MockRegistry({ - tap: t, registry: alternateRegistry, basic, }) - registry.nock.put(`/${spec.escapedName}`).reply(200, {}) + registry.publish('@npm/test-package') await npm.exec('publish', []) t.matchSnapshot(joinedOutput(), 'new package version') }) t.test('bare _auth config scoped registry', async t => { - const { npm } = await loadMockNpm(t, { + const { npm } = await loadNpmWithRegistry(t, { config: { scope: '@npm', registry: alternateRegistry, @@ -865,8 +695,7 @@ t.test('bare _auth config scoped registry', async t => { }) t.test('scoped _auth config scoped registry', async t => { - const spec = npa('@npm/test-package') - const { npm, joinedOutput } = await loadMockNpm(t, { + const { npm, joinedOutput, registry } = await loadNpmWithRegistry(t, { config: { scope: '@npm', registry: alternateRegistry, @@ -878,48 +707,37 @@ t.test('scoped _auth config scoped registry', async t => { version: '1.0.0', }, null, 2), }, - }) - const registry = new MockRegistry({ - tap: t, registry: alternateRegistry, basic, }) - registry.nock.put(`/${spec.escapedName}`).reply(200, {}) + registry.publish('@npm/test-package') await npm.exec('publish', []) t.matchSnapshot(joinedOutput(), 'new package version') }) t.test('restricted access', async t => { - const spec = npa('@npm/test-package') - const { npm, joinedOutput, logs } = await loadMockNpm(t, { + const packageJson = { + name: '@npm/test-package', + version: '1.0.0', + } + const { npm, joinedOutput, logs, registry } = await loadNpmWithRegistry(t, { config: { ...auth, access: 'restricted', }, prefixDir: { - 'package.json': JSON.stringify({ - name: '@npm/test-package', - version: '1.0.0', - }, null, 2), + 'package.json': JSON.stringify(packageJson, null, 2), }, - }) - const registry = new MockRegistry({ - tap: t, - registry: npm.config.get('registry'), authorization: token, }) - registry.nock.put(`/${spec.escapedName}`, body => { - t.equal(body.access, 'restricted', 'access is explicitly set to restricted') - return true - }).reply(200, {}) + registry.publish('@npm/test-package', { packageJson, access: 'restricted' }) await npm.exec('publish', []) t.matchSnapshot(joinedOutput(), 'new package version') t.matchSnapshot(logs.notice) }) t.test('public access', async t => { - const spec = npa('@npm/test-package') - const { npm, joinedOutput, logs } = await loadMockNpm(t, { + const { npm, joinedOutput, logs, registry } = await loadNpmWithRegistry(t, { config: { ...auth, access: 'public', @@ -930,16 +748,9 @@ t.test('public access', async t => { version: '1.0.0', }, null, 2), }, - }) - const registry = new MockRegistry({ - tap: t, - registry: npm.config.get('registry'), authorization: token, }) - registry.nock.put(`/${spec.escapedName}`, body => { - t.equal(body.access, 'public', 'access is explicitly set to public') - return true - }).reply(200, {}) + registry.publish('@npm/test-package', { access: 'public' }) await npm.exec('publish', []) t.matchSnapshot(joinedOutput(), 'new package version') t.matchSnapshot(logs.notice) @@ -959,7 +770,7 @@ t.test('manifest', async t => { t.cleanSnapshot = (s) => s.replace(new RegExp(npmPkg.version, 'g'), '{VERSION}') let manifest = null - const { npm } = await loadMockNpm(t, { + const { npm, registry } = await loadNpmWithRegistry(t, { config: { ...auth, tag: 'latest', @@ -972,6 +783,9 @@ t.test('manifest', async t => { }, }, }) + + registry.publish('npm', { noPut: true }) + await npm.exec('publish', []) const okKeys = [ @@ -998,74 +812,113 @@ t.test('manifest', async t => { t.matchSnapshot(manifest, 'manifest') }) -t.test('aborts when prerelease and no tag', async t => { - const { npm } = await loadMockNpm(t, { - config: { - loglevel: 'silent', - [`${alternateRegistry.slice(6)}/:_authToken`]: 'test-other-token', - }, - prefixDir: { - 'package.json': JSON.stringify({ - ...pkgJson, - version: '1.0.0-0', - publishConfig: { registry: alternateRegistry }, - }, null, 2), - }, +t.test('prerelease dist tag', (t) => { + t.test('aborts when prerelease and no tag', async t => { + const { npm } = await loadNpmWithRegistry(t, { + config: { + loglevel: 'silent', + [`${alternateRegistry.slice(6)}/:_authToken`]: 'test-other-token', + }, + prefixDir: { + 'package.json': JSON.stringify({ + ...pkgJson, + version: '1.0.0-0', + publishConfig: { registry: alternateRegistry }, + }, null, 2), + }, + }) + await t.rejects(async () => { + await npm.exec('publish', []) + }, new Error('You must specify a tag using --tag when publishing a prerelease version')) }) - await t.rejects(async () => { + t.test('does not abort when prerelease and authored tag latest', async t => { + const packageJson = { + ...pkgJson, + version: '1.0.0-0', + publishConfig: { registry: alternateRegistry }, + } + const { npm, registry } = await loadNpmWithRegistry(t, { + config: { + loglevel: 'silent', + tag: 'latest', + [`${alternateRegistry.slice(6)}/:_authToken`]: 'test-other-token', + }, + prefixDir: { + 'package.json': JSON.stringify(packageJson, null, 2), + }, + registry: alternateRegistry, + authorization: 'test-other-token', + }) + registry.publish(pkg, { packageJson }) await npm.exec('publish', []) - }, new Error('You must specify a tag using --tag when publishing a prerelease version')) + }) + + t.end() }) -t.test('does not abort when prerelease and authored tag latest', async t => { - const prereleasePkg = { - ...pkgJson, - version: '1.0.0-0', - } - const { npm } = await loadMockNpm(t, { +t.test('latest dist tag', (t) => { + const init = (version) => ({ config: { loglevel: 'silent', - tag: 'latest', - [`${alternateRegistry.slice(6)}/:_authToken`]: 'test-other-token', + ...auth, }, prefixDir: { 'package.json': JSON.stringify({ - ...prereleasePkg, - publishConfig: { registry: alternateRegistry }, + ...pkgJson, + version, }, null, 2), }, + authorization: token, }) - const registry = new MockRegistry({ - tap: t, - registry: alternateRegistry, - authorization: 'test-other-token', + + const packuments = [ + // this needs more than one item in it to cover the sort logic + { version: '50.0.0' }, + { version: '100.0.0' }, + { version: '105.0.0-pre' }, + ] + + t.test('PREVENTS publish when latest version is HIGHER than publishing version', async t => { + const version = '99.0.0' + const { npm, registry } = await loadNpmWithRegistry(t, init(version)) + registry.publish(pkg, { noPut: true, packuments }) + await t.rejects(async () => { + await npm.exec('publish', []) + /* eslint-disable-next-line max-len */ + }, new Error('Cannot implicitly apply the "latest" tag because published version 100.0.0 is higher than the new version 99.0.0. You must specify a tag using --tag.')) }) - registry.nock.put(`/${pkg}`, body => { - return t.match(body, { - _id: pkg, - name: pkg, - 'dist-tags': { latest: prereleasePkg.version }, - access: null, - versions: { - [prereleasePkg.version]: { - name: pkg, - version: prereleasePkg.version, - _id: `${pkg}@${prereleasePkg.version}`, - dist: { - shasum: /\.*/, - // eslint-disable-next-line max-len - tarball: `http:${alternateRegistry.slice(6)}/test-package/-/test-package-${prereleasePkg.version}.tgz`, - }, - publishConfig: { - registry: alternateRegistry, - }, - }, - }, - _attachments: { - [`${pkg}-${prereleasePkg.version}.tgz`]: {}, - }, + + t.test('ALLOWS publish when latest is HIGHER than publishing version and flag', async t => { + const version = '99.0.0' + const { npm, registry } = await loadNpmWithRegistry(t, { + ...init(version), + argv: ['--tag', 'latest'], }) - }).reply(200, {}) - await npm.exec('publish', []) + registry.publish(pkg, { packuments }) + await npm.exec('publish', []) + }) + + t.test('ALLOWS publish when latest versions are LOWER than publishing version', async t => { + const version = '101.0.0' + const { npm, registry } = await loadNpmWithRegistry(t, init(version)) + registry.publish(pkg, { packuments }) + await npm.exec('publish', []) + }) + + t.test('ALLOWS publish when packument has empty versions (for coverage)', async t => { + const version = '1.0.0' + const { npm, registry } = await loadNpmWithRegistry(t, init(version)) + registry.publish(pkg, { manifest: { versions: { } } }) + await npm.exec('publish', []) + }) + + t.test('ALLOWS publish when packument has empty manifest (for coverage)', async t => { + const version = '1.0.0' + const { npm, registry } = await loadNpmWithRegistry(t, init(version)) + registry.publish(pkg, { manifest: {} }) + await npm.exec('publish', []) + }) + + t.end() }) diff --git a/workspaces/arborist/lib/audit-report.js b/workspaces/arborist/lib/audit-report.js index 836c2ed5a20ea..dbd9be8bd3865 100644 --- a/workspaces/arborist/lib/audit-report.js +++ b/workspaces/arborist/lib/audit-report.js @@ -15,7 +15,7 @@ const _init = Symbol('init') const _omit = Symbol('omit') const { log, time } = require('proc-log') -const fetch = require('npm-registry-fetch') +const npmFetch = require('npm-registry-fetch') class AuditReport extends Map { static load (tree, opts) { @@ -291,7 +291,7 @@ class AuditReport extends Map { return null } - const res = await fetch('/-/npm/v1/security/advisories/bulk', { + const res = await npmFetch('/-/npm/v1/security/advisories/bulk', { ...this.options, registry: this.options.auditRegistry || this.options.registry, method: 'POST', diff --git a/workspaces/arborist/lib/query-selector-all.js b/workspaces/arborist/lib/query-selector-all.js index fa48d5f84b556..c2cd00d0a2e2e 100644 --- a/workspaces/arborist/lib/query-selector-all.js +++ b/workspaces/arborist/lib/query-selector-all.js @@ -8,7 +8,7 @@ const { minimatch } = require('minimatch') const npa = require('npm-package-arg') const pacote = require('pacote') const semver = require('semver') -const fetch = require('npm-registry-fetch') +const npmFetch = require('npm-registry-fetch') // handle results for parsed query asts, results are stored in a map that has a // key that points to each ast selector node and stores the resulting array of @@ -461,7 +461,7 @@ class Results { packages[node.name].push(node.version) } }) - const res = await fetch('/-/npm/v1/security/advisories/bulk', { + const res = await npmFetch('/-/npm/v1/security/advisories/bulk', { ...this.flatOptions, registry: this.flatOptions.auditRegistry || this.flatOptions.registry, method: 'POST', diff --git a/workspaces/libnpmorg/lib/index.js b/workspaces/libnpmorg/lib/index.js index 4684b516d2b4a..f3d361b8be6d7 100644 --- a/workspaces/libnpmorg/lib/index.js +++ b/workspaces/libnpmorg/lib/index.js @@ -1,7 +1,7 @@ 'use strict' const eu = encodeURIComponent -const fetch = require('npm-registry-fetch') +const npmFetch = require('npm-registry-fetch') const validate = require('aproba') // From https://github.com/npm/registry/blob/master/docs/orgs/memberships.md @@ -19,7 +19,7 @@ cmd.set = (org, user, role, opts = {}) => { validate('SSSO|SSZO', [org, user, role, opts]) user = user.replace(/^@?/, '') org = org.replace(/^@?/, '') - return fetch.json(`/-/org/${eu(org)}/user`, { + return npmFetch.json(`/-/org/${eu(org)}/user`, { ...opts, method: 'PUT', body: { user, role }, @@ -30,7 +30,7 @@ cmd.rm = (org, user, opts = {}) => { validate('SSO', [org, user, opts]) user = user.replace(/^@?/, '') org = org.replace(/^@?/, '') - return fetch(`/-/org/${eu(org)}/user`, { + return npmFetch(`/-/org/${eu(org)}/user`, { ...opts, method: 'DELETE', body: { user }, @@ -55,7 +55,7 @@ cmd.ls = (org, opts = {}) => { cmd.ls.stream = (org, opts = {}) => { validate('SO', [org, opts]) org = org.replace(/^@?/, '') - return fetch.json.stream(`/-/org/${eu(org)}/user`, '*', { + return npmFetch.json.stream(`/-/org/${eu(org)}/user`, '*', { ...opts, mapJSON: (value, [key]) => { return [key, value]