Skip to content

Commit

Permalink
feat(publish): add workspace support
Browse files Browse the repository at this point in the history
Errors will make things stop altogether, dunno if we want to bikeshed
that here or not

PR-URL: #3231
Credit: @wraithgar
Close: #3231
Reviewed-by: @ruyadorno
  • Loading branch information
wraithgar authored and ruyadorno committed May 13, 2021
1 parent 9c46a0a commit 076420c
Show file tree
Hide file tree
Showing 5 changed files with 274 additions and 46 deletions.
7 changes: 7 additions & 0 deletions docs/content/commands/npm-publish.md
Expand Up @@ -47,6 +47,13 @@ by specifying a different default registry or using a
actually publishing to the registry. Reports the details of what would
have been published.
* `[--workspaces]`: Enables workspace context while publishing. All
workspace packages will be published.
* `[--workspace]`: Enables workspaces context and limits results to only
those specified by this config item. Only the packages in the
workspaces given will be published.
The publish will fail if the package name and version combination already
exists in the specified registry.
Expand Down
76 changes: 50 additions & 26 deletions lib/publish.js
Expand Up @@ -8,13 +8,19 @@ const pacote = require('pacote')
const npa = require('npm-package-arg')
const npmFetch = require('npm-registry-fetch')

const flatten = require('./utils/config/flatten.js')
const otplease = require('./utils/otplease.js')
const { getContents, logTar } = require('./utils/tar.js')
const getWorkspaces = require('./workspaces/get-workspaces.js')

// for historical reasons, publishConfig in package.json can contain ANY config
// keys that npm supports in .npmrc files and elsewhere. We *may* want to
// revisit this at some point, and have a minimal set that's a SemVer-major
// change that ought to get a RFC written on it.
const flatten = require('./utils/config/flatten.js')

// this is the only case in the CLI where we use the old full slow
// 'read-package-json' module, because we want to pull in all the
// defaults and metadata, like git sha's and default scripts and all that.
// this is the only case in the CLI where we want to use the old full slow
// 'read-package-json' module, because we want to pull in all the defaults and
// metadata, like git sha's and default scripts and all that.
const readJson = util.promisify(require('read-package-json'))

const BaseCommand = require('./base-command.js')
Expand All @@ -30,7 +36,7 @@ class Publish extends BaseCommand {

/* istanbul ignore next - see test/lib/load-all-commands.js */
static get params () {
return ['tag', 'access', 'dry-run']
return ['tag', 'access', 'dry-run', 'workspace', 'workspaces']
}

/* istanbul ignore next - see test/lib/load-all-commands.js */
Expand All @@ -44,6 +50,10 @@ class Publish extends BaseCommand {
this.publish(args).then(() => cb()).catch(cb)
}

execWorkspaces (args, filters, cb) {
this.publishWorkspaces(args, filters).then(() => cb()).catch(cb)
}

async publish (args) {
if (args.length === 0)
args = ['.']
Expand All @@ -56,6 +66,7 @@ class Publish extends BaseCommand {
const dryRun = this.npm.config.get('dry-run')
const json = this.npm.config.get('json')
const defaultTag = this.npm.config.get('tag')
const silent = log.level === 'silent'

if (semver.validRange(defaultTag))
throw new Error('Tag name must not be a valid SemVer range: ' + defaultTag.trim())
Expand All @@ -68,7 +79,7 @@ class Publish extends BaseCommand {
let manifest = await this.getManifest(spec, opts)

if (manifest.publishConfig)
Object.assign(opts, this.publishConfigToOpts(manifest.publishConfig))
flatten(manifest.publishConfig, opts)

// only run scripts for directory type publishes
if (spec.type === 'directory') {
Expand All @@ -77,7 +88,7 @@ class Publish extends BaseCommand {
path: spec.fetchSpec,
stdio: 'inherit',
pkg: manifest,
banner: log.level !== 'silent',
banner: !silent,
})
}

Expand All @@ -89,7 +100,7 @@ class Publish extends BaseCommand {
// note that publishConfig might have changed as well!
manifest = await this.getManifest(spec, opts)
if (manifest.publishConfig)
Object.assign(opts, this.publishConfigToOpts(manifest.publishConfig))
flatten(manifest.publishConfig, opts)

// note that logTar calls npmlog.notice(), so if we ARE in silent mode,
// this will do nothing, but we still want it in the debuglog if it fails.
Expand All @@ -114,44 +125,57 @@ class Publish extends BaseCommand {
path: spec.fetchSpec,
stdio: 'inherit',
pkg: manifest,
banner: log.level !== 'silent',
banner: !silent,
})

await runScript({
event: 'postpublish',
path: spec.fetchSpec,
stdio: 'inherit',
pkg: manifest,
banner: log.level !== 'silent',
banner: !silent,
})
}

const silent = log.level === 'silent'
if (!silent && json)
this.npm.output(JSON.stringify(pkgContents, null, 2))
else if (!silent)
this.npm.output(`+ ${pkgContents.id}`)
if (!this.workspaces) {
if (!silent && json)
this.npm.output(JSON.stringify(pkgContents, null, 2))
else if (!silent)
this.npm.output(`+ ${pkgContents.id}`)
}

return pkgContents
}

async publishWorkspaces (args, filters) {
// Suppresses JSON output in publish() so we can handle it here
this.workspaces = true

const results = {}
const json = this.npm.config.get('json')
const silent = log.level === 'silent'
const workspaces =
await getWorkspaces(filters, { path: this.npm.localPrefix })
for (const [name, workspace] of workspaces.entries()) {
const pkgContents = await this.publish([workspace])
// This needs to be in-line w/ the rest of the output that non-JSON
// publish generates
if (!silent && !json)
this.npm.output(`+ ${pkgContents.id}`)
else
results[name] = pkgContents
}

if (!silent && json)
this.npm.output(JSON.stringify(results, null, 2))
}

// if it's a directory, read it from the file system
// otherwise, get the full metadata from whatever it is
getManifest (spec, opts) {
if (spec.type === 'directory')
return readJson(`${spec.fetchSpec}/package.json`)
return pacote.manifest(spec, { ...opts, fullMetadata: true })
}

// for historical reasons, publishConfig in package.json can contain
// ANY config keys that npm supports in .npmrc files and elsewhere.
// We *may* want to revisit this at some point, and have a minimal set
// that's a SemVer-major change that ought to get a RFC written on it.
publishConfigToOpts (publishConfig) {
// create a new object that inherits from the config stack
// then squash the css-case into camelCase opts, like we do
// this is Object.assign()'ed onto the base npm.flatOptions
return flatten(publishConfig, {})
}
}
module.exports = Publish
103 changes: 103 additions & 0 deletions tap-snapshots/test/lib/publish.js.test.cjs
Expand Up @@ -15,6 +15,109 @@ npm publish [<folder>]
Options:
[--tag <tag>] [--access <restricted|public>] [--dry-run]
[-w|--workspace <workspace-name> [-w|--workspace <workspace-name> ...]]
[-ws|--workspaces]
Run "npm help publish" for more info
`

exports[`test/lib/publish.js TAP workspaces all workspaces > should output all publishes 1`] = `
Array [
"+ workspace-a@1.2.3-a",
"+ workspace-b@1.2.3-n",
]
`

exports[`test/lib/publish.js TAP workspaces all workspaces > should publish all workspaces 1`] = `
Array [
Object {
"_id": "workspace-a@1.2.3-a",
"name": "workspace-a",
"readme": "ERROR: No README data found!",
"repository": Object {
"type": "git",
"url": "http://repo.workspace-a/",
},
"version": "1.2.3-a",
},
Object {
"_id": "workspace-b@1.2.3-n",
"bugs": Object {
"url": "https://github.com/npm/workspace-b/issues",
},
"homepage": "https://github.com/npm/workspace-b#readme",
"name": "workspace-b",
"readme": "ERROR: No README data found!",
"repository": Object {
"type": "git",
"url": "git+https://github.com/npm/workspace-b.git",
},
"version": "1.2.3-n",
},
]
`

exports[`test/lib/publish.js TAP workspaces json > should output all publishes as json 1`] = `
Array [
String(
{
"workspace-a": {
"id": "workspace-a@1.2.3-a"
},
"workspace-b": {
"id": "workspace-b@1.2.3-n"
}
}
),
]
`

exports[`test/lib/publish.js TAP workspaces json > should publish all workspaces 1`] = `
Array [
Object {
"_id": "workspace-a@1.2.3-a",
"name": "workspace-a",
"readme": "ERROR: No README data found!",
"repository": Object {
"type": "git",
"url": "http://repo.workspace-a/",
},
"version": "1.2.3-a",
},
Object {
"_id": "workspace-b@1.2.3-n",
"bugs": Object {
"url": "https://github.com/npm/workspace-b/issues",
},
"homepage": "https://github.com/npm/workspace-b#readme",
"name": "workspace-b",
"readme": "ERROR: No README data found!",
"repository": Object {
"type": "git",
"url": "git+https://github.com/npm/workspace-b.git",
},
"version": "1.2.3-n",
},
]
`

exports[`test/lib/publish.js TAP workspaces one workspace > should output one publish 1`] = `
Array [
"+ workspace-a@1.2.3-a",
]
`

exports[`test/lib/publish.js TAP workspaces one workspace > should publish given workspace 1`] = `
Array [
Object {
"_id": "workspace-a@1.2.3-a",
"name": "workspace-a",
"readme": "ERROR: No README data found!",
"repository": Object {
"type": "git",
"url": "http://repo.workspace-a/",
},
"version": "1.2.3-a",
},
]
`
2 changes: 2 additions & 0 deletions tap-snapshots/test/lib/utils/npm-usage.js.test.cjs
Expand Up @@ -697,6 +697,8 @@ All commands:
Options:
[--tag <tag>] [--access <restricted|public>] [--dry-run]
[-w|--workspace <workspace-name> [-w|--workspace <workspace-name> ...]]
[-ws|--workspaces]
Run "npm help publish" for more info
Expand Down

0 comments on commit 076420c

Please sign in to comment.