diff --git a/docs/content/commands/npm-init.md b/docs/content/commands/npm-init.md index 5a7dd4c97ac60..35343cceb4aa1 100644 --- a/docs/content/commands/npm-init.md +++ b/docs/content/commands/npm-init.md @@ -253,6 +253,17 @@ This value is not exported to the environment for child processes. +#### `workspaces-update` + +* Default: true +* Type: Boolean + +If set to true, the npm cli will run an update after operations that may +possibly change the workspaces installed to the `node_modules` folder. + + + + #### `include-workspace-root` * Default: false diff --git a/lib/commands/init.js b/lib/commands/init.js index 2a6b6aaddc7e6..4c299e65137be 100644 --- a/lib/commands/init.js +++ b/lib/commands/init.js @@ -8,13 +8,22 @@ const libexec = require('libnpmexec') const mapWorkspaces = require('@npmcli/map-workspaces') const PackageJson = require('@npmcli/package-json') const log = require('../utils/log-shim.js') +const updateWorkspaces = require('../workspaces/update-workspaces.js') const getLocationMsg = require('../exec/get-workspace-location-msg.js') const BaseCommand = require('../base-command.js') class Init extends BaseCommand { static description = 'Create a package.json file' - static params = ['yes', 'force', 'workspace', 'workspaces', 'include-workspace-root'] + static params = [ + 'yes', + 'force', + 'workspace', + 'workspaces', + 'workspaces-update', + 'include-workspace-root', + ] + static name = 'init' static usage = [ '[--force|-f|--yes|-y|--scope]', @@ -46,11 +55,13 @@ class Init extends BaseCommand { const pkg = await rpj(resolve(this.npm.localPrefix, 'package.json')) const wPath = filterArg => resolve(this.npm.localPrefix, filterArg) + const workspacesPaths = [] // npm-exec style, runs in the context of each workspace filter if (args.length) { for (const filterArg of filters) { const path = wPath(filterArg) await mkdirp(path) + workspacesPaths.push(path) await this.execCreate({ args, path }) await this.setWorkspace({ pkg, workspacePath: path }) } @@ -61,9 +72,13 @@ class Init extends BaseCommand { for (const filterArg of filters) { const path = wPath(filterArg) await mkdirp(path) + workspacesPaths.push(path) await this.template(path) await this.setWorkspace({ pkg, workspacePath: path }) } + + // reify packages once all workspaces have been initialized + await this.update(workspacesPaths) } async execCreate ({ args, path }) { @@ -196,6 +211,34 @@ class Init extends BaseCommand { await pkgJson.save() } + + async update (workspacesPaths) { + // translate workspaces paths into an array containing workspaces names + const workspaces = [] + for (const path of workspacesPaths) { + const pkgPath = resolve(path, 'package.json') + const { name } = await rpj(pkgPath) + .catch(() => ({})) + + if (name) { + workspaces.push(name) + } + } + + const { + config, + flatOptions, + localPrefix, + } = this.npm + + await updateWorkspaces({ + config, + flatOptions, + localPrefix, + npm: this.npm, + workspaces, + }) + } } module.exports = Init diff --git a/lib/commands/version.js b/lib/commands/version.js index ed506f663e89f..ab59fff5a308c 100644 --- a/lib/commands/version.js +++ b/lib/commands/version.js @@ -3,9 +3,7 @@ const { resolve } = require('path') const { promisify } = require('util') const readFile = promisify(require('fs').readFile) -const Arborist = require('@npmcli/arborist') -const reifyFinish = require('../utils/reify-finish.js') - +const updateWorkspaces = require('../workspaces/update-workspaces.js') const BaseCommand = require('../base-command.js') class Version extends BaseCommand { @@ -137,32 +135,20 @@ class Version extends BaseCommand { return this.list(results) } - async update (args) { - if (!this.npm.flatOptions.workspacesUpdate || !args.length) { - return - } - - // default behavior is to not save by default in order to avoid - // race condition problems when publishing multiple workspaces - // that have dependencies on one another, it might still be useful - // in some cases, which then need to set --save - const save = this.npm.config.isDefault('save') - ? false - : this.npm.config.get('save') - - // runs a minimalistic reify update, targetting only the workspaces - // that had version updates and skipping fund/audit/save - const opts = { - ...this.npm.flatOptions, - audit: false, - fund: false, - path: this.npm.localPrefix, - save, - } - const arb = new Arborist(opts) - - await arb.reify({ ...opts, update: args }) - await reifyFinish(this.npm, arb) + async update (workspaces) { + const { + config, + flatOptions, + localPrefix, + } = this.npm + + await updateWorkspaces({ + config, + flatOptions, + localPrefix, + npm: this.npm, + workspaces, + }) } } diff --git a/lib/workspaces/update-workspaces.js b/lib/workspaces/update-workspaces.js new file mode 100644 index 0000000000000..4cba1245ac2e5 --- /dev/null +++ b/lib/workspaces/update-workspaces.js @@ -0,0 +1,40 @@ +'use strict' + +const Arborist = require('@npmcli/arborist') +const reifyFinish = require('../utils/reify-finish.js') + +async function updateWorkspaces ({ + config, + flatOptions, + localPrefix, + npm, + workspaces, +}) { + if (!flatOptions.workspacesUpdate || !workspaces.length) { + return + } + + // default behavior is to not save by default in order to avoid + // race condition problems when publishing multiple workspaces + // that have dependencies on one another, it might still be useful + // in some cases, which then need to set --save + const save = config.isDefault('save') + ? false + : config.get('save') + + // runs a minimalistic reify update, targetting only the workspaces + // that had version updates and skipping fund/audit/save + const opts = { + ...flatOptions, + audit: false, + fund: false, + path: localPrefix, + save, + } + const arb = new Arborist(opts) + + await arb.reify({ ...opts, update: workspaces }) + await reifyFinish(npm, arb) +} + +module.exports = updateWorkspaces diff --git a/tap-snapshots/test/lib/commands/init.js.test.cjs b/tap-snapshots/test/lib/commands/init.js.test.cjs index 3ca9d93175ec5..97a6722f3ece4 100644 --- a/tap-snapshots/test/lib/commands/init.js.test.cjs +++ b/tap-snapshots/test/lib/commands/init.js.test.cjs @@ -10,26 +10,53 @@ Array [] ` exports[`test/lib/commands/init.js TAP workspaces no args > should print helper info 1`] = ` +Array [] +` + +exports[`test/lib/commands/init.js TAP workspaces no args, existing folder > should print helper info 1`] = ` +Array [] +` + +exports[`test/lib/commands/init.js TAP workspaces post workspace-init reify > should print helper info 1`] = ` Array [ Array [ String( - This utility will walk you through creating a package.json file. - It only covers the most common items, and tries to guess sensible defaults. - - See \`npm help init\` for definitive documentation on these fields - and exactly what they do. - - Use \`npm install \` afterwards to install a package and - save it as a dependency in the package.json file. - Press ^C at any time to quit. + added 1 package in 100ms ), ], ] ` -exports[`test/lib/commands/init.js TAP workspaces no args, existing folder > should print helper info 1`] = ` -Array [] +exports[`test/lib/commands/init.js TAP workspaces post workspace-init reify > should reify tree on init ws complete 1`] = ` +{ + "name": "top-level", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "top-level", + "workspaces": [ + "a" + ] + }, + "a": { + "version": "1.0.0", + "license": "ISC", + "devDependencies": {} + }, + "node_modules/a": { + "resolved": "a", + "link": true + } + }, + "dependencies": { + "a": { + "version": "file:a" + } + } +} + ` exports[`test/lib/commands/init.js TAP workspaces with arg but missing workspace folder > should print helper info 1`] = ` diff --git a/tap-snapshots/test/lib/load-all-commands.js.test.cjs b/tap-snapshots/test/lib/load-all-commands.js.test.cjs index b8c274b085a7a..7bcdd949b90da 100644 --- a/tap-snapshots/test/lib/load-all-commands.js.test.cjs +++ b/tap-snapshots/test/lib/load-all-commands.js.test.cjs @@ -396,7 +396,7 @@ npm init [<@scope>/] (same as \`npx [<@scope>/]create-\`) Options: [-y|--yes] [-f|--force] [-w|--workspace [-w|--workspace ...]] -[-ws|--workspaces] [--include-workspace-root] +[-ws|--workspaces] [--no-workspaces-update] [--include-workspace-root] aliases: create, innit diff --git a/tap-snapshots/test/lib/npm.js.test.cjs b/tap-snapshots/test/lib/npm.js.test.cjs index e71bc8268c495..d15cae4428216 100644 --- a/tap-snapshots/test/lib/npm.js.test.cjs +++ b/tap-snapshots/test/lib/npm.js.test.cjs @@ -486,7 +486,7 @@ All commands: Options: [-y|--yes] [-f|--force] [-w|--workspace [-w|--workspace ...]] - [-ws|--workspaces] [--include-workspace-root] + [-ws|--workspaces] [--no-workspaces-update] [--include-workspace-root] aliases: create, innit diff --git a/test/lib/commands/init.js b/test/lib/commands/init.js index 82e7e0524cee9..32816adbc272e 100644 --- a/test/lib/commands/init.js +++ b/test/lib/commands/init.js @@ -288,6 +288,7 @@ t.test('workspaces', t => { t.teardown(() => { npm._mockOutputs.length = 0 }) + npm._mockOutputs.length = 0 npm.localPrefix = t.testdir({ 'package.json': JSON.stringify({ name: 'top-level', @@ -306,6 +307,39 @@ t.test('workspaces', t => { t.matchSnapshot(npm._mockOutputs, 'should print helper info') }) + t.test('post workspace-init reify', async t => { + const _consolelog = console.log + console.log = () => null + t.teardown(() => { + console.log = _consolelog + npm._mockOutputs.length = 0 + delete npm.flatOptions.workspacesUpdate + }) + npm.started = Date.now() + npm._mockOutputs.length = 0 + npm.flatOptions.workspacesUpdate = true + npm.localPrefix = t.testdir({ + 'package.json': JSON.stringify({ + name: 'top-level', + }), + }) + + const Init = t.mock('../../../lib/commands/init.js', { + ...mocks, + 'init-package-json': (dir, initFile, config, cb) => { + t.equal(dir, resolve(npm.localPrefix, 'a'), 'should use the ws path') + return require('init-package-json')(dir, initFile, config, cb) + }, + }) + const init = new Init(npm) + await init.execWorkspaces([], ['a']) + const output = npm._mockOutputs.map(arr => arr.map(i => i.replace(/[0-9]*ms$/, '100ms'))) + t.matchSnapshot(output, 'should print helper info') + const lockFilePath = resolve(npm.localPrefix, 'package-lock.json') + const lockFile = fs.readFileSync(lockFilePath, { encoding: 'utf8' }) + t.matchSnapshot(lockFile, 'should reify tree on init ws complete') + }) + t.test('no args, existing folder', async t => { t.teardown(() => { npm._mockOutputs.length = 0