Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(publish): add workspace support #3231

Merged
merged 1 commit into from
May 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/content/commands/npm-publish.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Loading