Skip to content

Commit

Permalink
feat(init): reify on init new workspace (#4892)
Browse files Browse the repository at this point in the history
Adds a minimalistic reify step that updates the installed tree after
initializing a new workspace.

Moved the shared update logic from `lib/commands/version.js` to a
`lib/workspaces/update-workspaces.js` module that is reused between
both `npm version` and `npm init`.

Relates to: npm/rfcs#556
Relates to: #4588
  • Loading branch information
ruyadorno committed Jun 1, 2022
1 parent 66981ec commit aee6fc8
Show file tree
Hide file tree
Showing 8 changed files with 184 additions and 43 deletions.
11 changes: 11 additions & 0 deletions docs/content/commands/npm-init.md
Expand Up @@ -253,6 +253,17 @@ This value is not exported to the environment for child processes.
<!-- automatically generated, do not edit manually -->
<!-- see lib/utils/config/definitions.js -->

#### `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.

<!-- automatically generated, do not edit manually -->
<!-- see lib/utils/config/definitions.js -->

#### `include-workspace-root`

* Default: false
Expand Down
45 changes: 44 additions & 1 deletion lib/commands/init.js
Expand Up @@ -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]',
Expand Down Expand Up @@ -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 })
}
Expand All @@ -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 }) {
Expand Down Expand Up @@ -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
44 changes: 15 additions & 29 deletions lib/commands/version.js
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
})
}
}

Expand Down
40 changes: 40 additions & 0 deletions 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
49 changes: 38 additions & 11 deletions tap-snapshots/test/lib/commands/init.js.test.cjs
Expand Up @@ -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 <pkg>\` 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`] = `
Expand Down
2 changes: 1 addition & 1 deletion tap-snapshots/test/lib/load-all-commands.js.test.cjs
Expand Up @@ -396,7 +396,7 @@ npm init [<@scope>/]<name> (same as \`npx [<@scope>/]create-<name>\`)
Options:
[-y|--yes] [-f|--force]
[-w|--workspace <workspace-name> [-w|--workspace <workspace-name> ...]]
[-ws|--workspaces] [--include-workspace-root]
[-ws|--workspaces] [--no-workspaces-update] [--include-workspace-root]
aliases: create, innit
Expand Down
2 changes: 1 addition & 1 deletion tap-snapshots/test/lib/npm.js.test.cjs
Expand Up @@ -486,7 +486,7 @@ All commands:
Options:
[-y|--yes] [-f|--force]
[-w|--workspace <workspace-name> [-w|--workspace <workspace-name> ...]]
[-ws|--workspaces] [--include-workspace-root]
[-ws|--workspaces] [--no-workspaces-update] [--include-workspace-root]
aliases: create, innit
Expand Down
34 changes: 34 additions & 0 deletions test/lib/commands/init.js
Expand Up @@ -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',
Expand All @@ -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
Expand Down

0 comments on commit aee6fc8

Please sign in to comment.