Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/npm/cli.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorRuy Adorno <ruyadorno@hotmail.com>2021-04-07 18:03:41 +0300
committerRuy Adorno <ruyadorno@hotmail.com>2021-04-22 22:06:29 +0300
commit4c1f16d2c29a7a56c19b97f2820e6305a6075083 (patch)
tree460fd0dfa65b168d3f35bac5e0ad363b14684a1d /lib
parent2aecec591df6866e27d0b17dc49cef8f7d738d77 (diff)
feat: add init workspaces
Add workspaces support to `npm init` - Fixes `npm exec` respecting `script-shell` option value - Refactored `lib/exec.js` into `libnpmexec` - Updates init-package-json@2.0.3 - Added ability to create a new workspace using the -w config PR-URL: https://github.com/npm/cli/pull/3095 Credit: @ruyadorno Close: #3095 Reviewed-by: @wraithgar
Diffstat (limited to 'lib')
-rw-r--r--lib/exec.js302
-rw-r--r--lib/exec/get-workspace-location-msg.js25
-rw-r--r--lib/init.js190
-rw-r--r--lib/utils/config/definitions.js3
4 files changed, 216 insertions, 304 deletions
diff --git a/lib/exec.js b/lib/exec.js
index f8c76eeed..3da672f99 100644
--- a/lib/exec.js
+++ b/lib/exec.js
@@ -1,18 +1,6 @@
-const { promisify } = require('util')
-const read = promisify(require('read'))
-const chalk = require('chalk')
-const mkdirp = require('mkdirp-infer-owner')
-const readPackageJson = require('read-package-json-fast')
-const Arborist = require('@npmcli/arborist')
-const runScript = require('@npmcli/run-script')
-const { resolve, delimiter } = require('path')
-const ciDetect = require('@npmcli/ci-detect')
-const crypto = require('crypto')
-const pacote = require('pacote')
-const npa = require('npm-package-arg')
-const fileExists = require('./utils/file-exists.js')
-const PATH = require('./utils/path.js')
+const libexec = require('libnpmexec')
const BaseCommand = require('./base-command.js')
+const getLocationMsg = require('./exec/get-workspace-location-msg.js')
const getWorkspaces = require('./workspaces/get-workspaces.js')
// it's like this:
@@ -40,13 +28,6 @@ const getWorkspaces = require('./workspaces/get-workspaces.js')
// runScript({ pkg, event: 'npx', ... })
// process.env.npm_lifecycle_event = 'npx'
-const nocolor = {
- reset: s => s,
- bold: s => s,
- dim: s => s,
- green: s => s,
-}
-
class Exec extends BaseCommand {
/* istanbul ignore next - see test/lib/load-all-commands.js */
static get description () {
@@ -86,276 +67,50 @@ class Exec extends BaseCommand {
// When commands go async and we can dump the boilerplate exec methods this
// can be named correctly
async _exec (_args, { locationMsg, path, runPath }) {
+ const args = [..._args]
+ const cache = this.npm.config.get('cache')
const call = this.npm.config.get('call')
- const shell = this.npm.config.get('shell')
- // dereferenced because we manipulate it later
- const packages = [...this.npm.config.get('package')]
+ const color = this.npm.config.get('color')
+ const {
+ flatOptions,
+ localBin,
+ log,
+ globalBin,
+ output,
+ } = this.npm
+ const scriptShell = this.npm.config.get('script-shell') || undefined
+ const packages = this.npm.config.get('package')
+ const yes = this.npm.config.get('yes')
if (call && _args.length)
throw this.usage
- const args = [..._args]
- const pathArr = [...PATH]
-
- // nothing to maybe install, skip the arborist dance
- if (!call && !args.length && !packages.length) {
- return await this.run({
- args,
- call,
- locationMsg,
- shell,
- path,
- pathArr,
- runPath,
- })
- }
-
- const needPackageCommandSwap = args.length && !packages.length
- // if there's an argument and no package has been explicitly asked for
- // check the local and global bin paths for a binary named the same as
- // the argument and run it if it exists, otherwise fall through to
- // the behavior of treating the single argument as a package name
- if (needPackageCommandSwap) {
- let binExists = false
- if (await fileExists(`${this.npm.localBin}/${args[0]}`)) {
- pathArr.unshift(this.npm.localBin)
- binExists = true
- } else if (await fileExists(`${this.npm.globalBin}/${args[0]}`)) {
- pathArr.unshift(this.npm.globalBin)
- binExists = true
- }
-
- if (binExists) {
- return await this.run({
- args,
- call,
- locationMsg,
- path,
- pathArr,
- runPath,
- shell,
- })
- }
-
- packages.push(args[0])
- }
-
- // If we do `npm exec foo`, and have a `foo` locally, then we'll
- // always use that, so we don't really need to fetch the manifest.
- // So: run npa on each packages entry, and if it is a name with a
- // rawSpec==='', then try to readPackageJson at
- // node_modules/${name}/package.json, and only pacote fetch if
- // that fails.
- const manis = await Promise.all(packages.map(async p => {
- const spec = npa(p, path)
- if (spec.type === 'tag' && spec.rawSpec === '') {
- // fall through to the pacote.manifest() approach
- try {
- const pj = resolve(path, 'node_modules', spec.name)
- return await readPackageJson(pj)
- } catch (er) {}
- }
- // Force preferOnline to true so we are making sure to pull in the latest
- // This is especially useful if the user didn't give us a version, and
- // they expect to be running @latest
- return await pacote.manifest(p, {
- ...this.npm.flatOptions,
- preferOnline: true,
- })
- }))
-
- if (needPackageCommandSwap)
- args[0] = this.getBinFromManifest(manis[0])
-
- // figure out whether we need to install stuff, or if local is fine
- const localArb = new Arborist({
- ...this.npm.flatOptions,
- path,
- })
- const tree = await localArb.loadActual()
-
- // do we have all the packages in manifest list?
- const needInstall = manis.some(mani => this.manifestMissing(tree, mani))
-
- if (needInstall) {
- const installDir = this.cacheInstallDir(packages)
- await mkdirp(installDir)
- const arb = new Arborist({
- ...this.npm.flatOptions,
- log: this.npm.log,
- path: installDir,
- })
- const tree = await arb.loadActual()
-
- // at this point, we have to ensure that we get the exact same
- // version, because it's something that has only ever been installed
- // by npm exec in the cache install directory
- const add = manis.filter(mani => this.manifestMissing(tree, {
- ...mani,
- _from: `${mani.name}@${mani.version}`,
- }))
- .map(mani => mani._from)
- .sort((a, b) => a.localeCompare(b))
-
- // no need to install if already present
- if (add.length) {
- if (!this.npm.config.get('yes')) {
- // set -n to always say no
- if (this.npm.config.get('yes') === false)
- throw new Error('canceled')
-
- if (!process.stdin.isTTY || ciDetect()) {
- this.npm.log.warn('exec', `The following package${
- add.length === 1 ? ' was' : 's were'
- } not found and will be installed: ${
- add.map((pkg) => pkg.replace(/@$/, '')).join(', ')
- }`)
- } else {
- const addList = add.map(a => ` ${a.replace(/@$/, '')}`)
- .join('\n') + '\n'
- const prompt = `Need to install the following packages:\n${
- addList
- }Ok to proceed? `
- const confirm = await read({ prompt, default: 'y' })
- if (confirm.trim().toLowerCase().charAt(0) !== 'y')
- throw new Error('canceled')
- }
- }
- await arb.reify({
- ...this.npm.flatOptions,
- log: this.npm.log,
- add,
- })
- }
- pathArr.unshift(resolve(installDir, 'node_modules/.bin'))
- }
-
- return await this.run({
+ return libexec({
+ ...flatOptions,
args,
call,
+ cache,
+ color,
+ localBin,
locationMsg,
+ log,
+ globalBin,
+ output,
+ packages,
path,
- pathArr,
runPath,
- shell,
+ scriptShell,
+ yes,
})
}
- async run ({ args, call, locationMsg, path, pathArr, runPath, shell }) {
- // turn list of args into command string
- const script = call || args.shift() || shell
-
- // do the fakey runScript dance
- // still should work if no package.json in cwd
- const realPkg = await readPackageJson(`${path}/package.json`)
- .catch(() => ({}))
- const pkg = {
- ...realPkg,
- scripts: {
- ...(realPkg.scripts || {}),
- npx: script,
- },
- }
-
- this.npm.log.disableProgress()
- try {
- if (script === shell) {
- if (process.stdin.isTTY) {
- if (ciDetect())
- return this.npm.log.warn('exec', 'Interactive mode disabled in CI environment')
-
- const color = this.npm.config.get('color')
- const colorize = color ? chalk : nocolor
-
- locationMsg = locationMsg || ` at location:\n${colorize.dim(runPath)}`
-
- this.npm.output(`${
- colorize.reset('\nEntering npm script environment')
- }${
- colorize.reset(locationMsg)
- }${
- colorize.bold('\nType \'exit\' or ^D when finished\n')
- }`)
- }
- }
- return await runScript({
- ...this.npm.flatOptions,
- pkg,
- banner: false,
- // we always run in cwd, not --prefix
- path: runPath,
- stdioString: true,
- event: 'npx',
- args,
- env: {
- PATH: pathArr.join(delimiter),
- },
- stdio: 'inherit',
- })
- } finally {
- this.npm.log.enableProgress()
- }
- }
-
- manifestMissing (tree, mani) {
- // if the tree doesn't have a child by that name/version, return true
- // true means we need to install it
- const child = tree.children.get(mani.name)
- // if no child, we have to load it
- if (!child)
- return true
-
- // if no version/tag specified, allow whatever's there
- if (mani._from === `${mani.name}@`)
- return false
-
- // otherwise the version has to match what we WOULD get
- return child.version !== mani.version
- }
-
- getBinFromManifest (mani) {
- // if we have a bin matching (unscoped portion of) packagename, use that
- // otherwise if there's 1 bin or all bin value is the same (alias), use
- // that, otherwise fail
- const bin = mani.bin || {}
- if (new Set(Object.values(bin)).size === 1)
- return Object.keys(bin)[0]
-
- // XXX probably a util to parse this better?
- const name = mani.name.replace(/^@[^/]+\//, '')
- if (bin[name])
- return name
-
- // XXX need better error message
- throw Object.assign(new Error('could not determine executable to run'), {
- pkgid: mani._id,
- })
- }
-
- cacheInstallDir (packages) {
- // only packages not found in ${prefix}/node_modules
- return resolve(this.npm.config.get('cache'), '_npx', this.getHash(packages))
- }
-
- getHash (packages) {
- return crypto.createHash('sha512')
- .update(packages.sort((a, b) => a.localeCompare(b)).join('\n'))
- .digest('hex')
- .slice(0, 16)
- }
-
async _execWorkspaces (args, filters) {
const workspaces =
await getWorkspaces(filters, { path: this.npm.localPrefix })
- const getLocationMsg = async path => {
- const color = this.npm.config.get('color')
- const colorize = color ? chalk : nocolor
- const { _id } = await readPackageJson(`${path}/package.json`)
- return ` in workspace ${colorize.green(_id)} at location:\n${colorize.dim(path)}`
- }
+ const color = this.npm.config.get('color')
for (const workspacePath of workspaces.values()) {
- const locationMsg = await getLocationMsg(workspacePath)
+ const locationMsg = await getLocationMsg({ color, path: workspacePath })
await this._exec(args, {
locationMsg,
path: workspacePath,
@@ -364,4 +119,5 @@ class Exec extends BaseCommand {
}
}
}
+
module.exports = Exec
diff --git a/lib/exec/get-workspace-location-msg.js b/lib/exec/get-workspace-location-msg.js
new file mode 100644
index 000000000..813b11e78
--- /dev/null
+++ b/lib/exec/get-workspace-location-msg.js
@@ -0,0 +1,25 @@
+const chalk = require('chalk')
+const readPackageJson = require('read-package-json-fast')
+
+const nocolor = {
+ dim: s => s,
+ green: s => s,
+}
+
+const getLocationMsg = async ({ color, path }) => {
+ const colorize = color ? chalk : nocolor
+ const { _id } =
+ await readPackageJson(`${path}/package.json`)
+ .catch(() => ({}))
+
+ const workspaceMsg = _id
+ ? ` in workspace ${colorize.green(_id)}`
+ : ` in a ${colorize.green('new')} workspace`
+ const locationMsg = ` at location:\n${
+ colorize.dim(path)
+ }`
+
+ return `${workspaceMsg}${locationMsg}`
+}
+
+module.exports = getLocationMsg
diff --git a/lib/init.js b/lib/init.js
index 81c673388..7d7f6bab3 100644
--- a/lib/init.js
+++ b/lib/init.js
@@ -1,6 +1,14 @@
+const fs = require('fs')
+const { relative, resolve } = require('path')
+const mkdirp = require('mkdirp-infer-owner')
const initJson = require('init-package-json')
const npa = require('npm-package-arg')
+const rpj = require('read-package-json-fast')
+const libexec = require('libnpmexec')
+const parseJSON = require('json-parse-even-better-errors')
+const mapWorkspaces = require('@npmcli/map-workspaces')
+const getLocationMsg = require('./exec/get-workspace-location-msg.js')
const BaseCommand = require('./base-command.js')
class Init extends BaseCommand {
@@ -10,6 +18,11 @@ class Init extends BaseCommand {
}
/* istanbul ignore next - see test/lib/load-all-commands.js */
+ static get params () {
+ return ['workspace', 'workspaces']
+ }
+
+ /* istanbul ignore next - see test/lib/load-all-commands.js */
static get name () {
return 'init'
}
@@ -27,42 +40,107 @@ class Init extends BaseCommand {
this.init(args).then(() => cb()).catch(cb)
}
+ execWorkspaces (args, filters, cb) {
+ this.initWorkspaces(args, filters).then(() => cb()).catch(cb)
+ }
+
async init (args) {
- // the new npx style way
+ // npm exec style
+ if (args.length)
+ return (await this.execCreate({ args, path: process.cwd() }))
+
+ // no args, uses classic init-package-json boilerplate
+ await this.template()
+ }
+
+ async initWorkspaces (args, filters) {
+ // reads package.json for the top-level folder first, by doing this we
+ // ensure the command throw if no package.json is found before trying
+ // to create a workspace package.json file or its folders
+ const pkg = await rpj(resolve(this.npm.localPrefix, 'package.json'))
+ const wPath = filterArg => resolve(this.npm.localPrefix, filterArg)
+
+ // npm-exec style, runs in the context of each workspace filter
if (args.length) {
- const initerName = args[0]
- let packageName = initerName
- if (/^@[^/]+$/.test(initerName))
- packageName = initerName + '/create'
- else {
- const req = npa(initerName)
- if (req.type === 'git' && req.hosted) {
- const { user, project } = req.hosted
- packageName = initerName
- .replace(user + '/' + project, user + '/create-' + project)
- } else if (req.registry) {
- packageName = req.name.replace(/^(@[^/]+\/)?/, '$1create-')
- if (req.rawSpec)
- packageName += '@' + req.rawSpec
- } else {
- throw Object.assign(new Error(
- 'Unrecognized initializer: ' + initerName +
- '\nFor more package binary executing power check out `npx`:' +
- '\nhttps://www.npmjs.com/package/npx'
- ), { code: 'EUNSUPPORTED' })
- }
+ for (const filterArg of filters) {
+ const path = wPath(filterArg)
+ await mkdirp(path)
+ await this.execCreate({ args, path })
+ await this.setWorkspace({ pkg, workspacePath: path })
+ }
+ return
+ }
+
+ // no args, uses classic init-package-json boilerplate
+ for (const filterArg of filters) {
+ const path = wPath(filterArg)
+ await mkdirp(path)
+ await this.template(path)
+ await this.setWorkspace({ pkg, workspacePath: path })
+ }
+ }
+
+ async execCreate ({ args, path }) {
+ const [initerName, ...otherArgs] = args
+ let packageName = initerName
+
+ if (/^@[^/]+$/.test(initerName))
+ packageName = initerName + '/create'
+ else {
+ const req = npa(initerName)
+ if (req.type === 'git' && req.hosted) {
+ const { user, project } = req.hosted
+ packageName = initerName
+ .replace(user + '/' + project, user + '/create-' + project)
+ } else if (req.registry) {
+ packageName = req.name.replace(/^(@[^/]+\/)?/, '$1create-')
+ if (req.rawSpec)
+ packageName += '@' + req.rawSpec
+ } else {
+ throw Object.assign(new Error(
+ 'Unrecognized initializer: ' + initerName +
+ '\nFor more package binary executing power check out `npx`:' +
+ '\nhttps://www.npmjs.com/package/npx'
+ ), { code: 'EUNSUPPORTED' })
}
- this.npm.config.set('package', [])
- const newArgs = [packageName, ...args.slice(1)]
- return new Promise((res, rej) => {
- this.npm.commands.exec(newArgs, er => er ? rej(er) : res())
- })
}
- // the old way
- const dir = process.cwd()
+ const newArgs = [packageName, ...otherArgs]
+ const cache = this.npm.config.get('cache')
+ const { color } = this.npm.flatOptions
+ const {
+ flatOptions,
+ localBin,
+ log,
+ globalBin,
+ output,
+ } = this.npm
+ const locationMsg = await getLocationMsg({ color, path })
+ const runPath = path
+ const scriptShell = this.npm.config.get('script-shell') || undefined
+ const yes = this.npm.config.get('yes')
+
+ await libexec({
+ ...flatOptions,
+ args: newArgs,
+ cache,
+ color,
+ localBin,
+ locationMsg,
+ log,
+ globalBin,
+ output,
+ path,
+ runPath,
+ scriptShell,
+ yes,
+ })
+ }
+
+ async template (path = process.cwd()) {
this.npm.log.pause()
this.npm.log.disableProgress()
+
const initFile = this.npm.config.get('init-module')
if (!this.npm.config.get('yes') && !this.npm.config.get('force')) {
this.npm.output([
@@ -78,9 +156,10 @@ class Init extends BaseCommand {
'Press ^C at any time to quit.',
].join('\n'))
}
+
// XXX promisify init-package-json
await new Promise((res, rej) => {
- initJson(dir, initFile, this.npm.config, (er, data) => {
+ initJson(path, initFile, this.npm.config, (er, data) => {
this.npm.log.resume()
this.npm.log.enableProgress()
this.npm.log.silly('package data', data)
@@ -97,5 +176,56 @@ class Init extends BaseCommand {
})
})
}
+
+ async setWorkspace ({ pkg, workspacePath }) {
+ const workspaces = await mapWorkspaces({ cwd: this.npm.localPrefix, pkg })
+
+ // skip setting workspace if current package.json glob already satisfies it
+ for (const wPath of workspaces.values()) {
+ if (wPath === workspacePath)
+ return
+ }
+
+ // if a create-pkg didn't generate a package.json at the workspace
+ // folder level, it might not be recognized as a workspace by
+ // mapWorkspaces, so we're just going to avoid touching the
+ // top-level package.json
+ try {
+ fs.statSync(resolve(workspacePath, 'package.json'))
+ } catch (err) {
+ return
+ }
+
+ let manifest
+ try {
+ manifest =
+ fs.readFileSync(resolve(this.npm.localPrefix, 'package.json'), 'utf-8')
+ } catch (error) {
+ throw new Error('package.json not found')
+ }
+
+ try {
+ manifest = parseJSON(manifest)
+ } catch (error) {
+ throw new Error(`Invalid package.json: ${error}`)
+ }
+
+ if (!manifest.workspaces)
+ manifest.workspaces = []
+
+ manifest.workspaces.push(relative(this.npm.localPrefix, workspacePath))
+
+ // format content
+ const {
+ [Symbol.for('indent')]: indent,
+ [Symbol.for('newline')]: newline,
+ } = manifest
+
+ const content = (JSON.stringify(manifest, null, indent) + '\n')
+ .replace(/\n/g, newline)
+
+ fs.writeFileSync(resolve(this.npm.localPrefix, 'package.json'), content)
+ }
}
+
module.exports = Init
diff --git a/lib/utils/config/definitions.js b/lib/utils/config/definitions.js
index db1f25e95..f8c6b41f3 100644
--- a/lib/utils/config/definitions.js
+++ b/lib/utils/config/definitions.js
@@ -1660,7 +1660,8 @@ define('script-shell', {
`,
type: [null, String],
description: `
- The shell to use for scripts run with the \`npm run\` command.
+ The shell to use for scripts run with the \`npm exec\`,
+ \`npm run\` and \`npm init <pkg>\` commands.
`,
flatten (key, obj, flatOptions) {
flatOptions.scriptShell = obj[key] || undefined