Skip to content

Commit aee6fc8

Browse files
authored
feat(init): reify on init new workspace (#4892)
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
1 parent 66981ec commit aee6fc8

File tree

8 files changed

+184
-43
lines changed

8 files changed

+184
-43
lines changed

docs/content/commands/npm-init.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,17 @@ This value is not exported to the environment for child processes.
253253
<!-- automatically generated, do not edit manually -->
254254
<!-- see lib/utils/config/definitions.js -->
255255

256+
#### `workspaces-update`
257+
258+
* Default: true
259+
* Type: Boolean
260+
261+
If set to true, the npm cli will run an update after operations that may
262+
possibly change the workspaces installed to the `node_modules` folder.
263+
264+
<!-- automatically generated, do not edit manually -->
265+
<!-- see lib/utils/config/definitions.js -->
266+
256267
#### `include-workspace-root`
257268

258269
* Default: false

lib/commands/init.js

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,22 @@ const libexec = require('libnpmexec')
88
const mapWorkspaces = require('@npmcli/map-workspaces')
99
const PackageJson = require('@npmcli/package-json')
1010
const log = require('../utils/log-shim.js')
11+
const updateWorkspaces = require('../workspaces/update-workspaces.js')
1112

1213
const getLocationMsg = require('../exec/get-workspace-location-msg.js')
1314
const BaseCommand = require('../base-command.js')
1415

1516
class Init extends BaseCommand {
1617
static description = 'Create a package.json file'
17-
static params = ['yes', 'force', 'workspace', 'workspaces', 'include-workspace-root']
18+
static params = [
19+
'yes',
20+
'force',
21+
'workspace',
22+
'workspaces',
23+
'workspaces-update',
24+
'include-workspace-root',
25+
]
26+
1827
static name = 'init'
1928
static usage = [
2029
'[--force|-f|--yes|-y|--scope]',
@@ -46,11 +55,13 @@ class Init extends BaseCommand {
4655
const pkg = await rpj(resolve(this.npm.localPrefix, 'package.json'))
4756
const wPath = filterArg => resolve(this.npm.localPrefix, filterArg)
4857

58+
const workspacesPaths = []
4959
// npm-exec style, runs in the context of each workspace filter
5060
if (args.length) {
5161
for (const filterArg of filters) {
5262
const path = wPath(filterArg)
5363
await mkdirp(path)
64+
workspacesPaths.push(path)
5465
await this.execCreate({ args, path })
5566
await this.setWorkspace({ pkg, workspacePath: path })
5667
}
@@ -61,9 +72,13 @@ class Init extends BaseCommand {
6172
for (const filterArg of filters) {
6273
const path = wPath(filterArg)
6374
await mkdirp(path)
75+
workspacesPaths.push(path)
6476
await this.template(path)
6577
await this.setWorkspace({ pkg, workspacePath: path })
6678
}
79+
80+
// reify packages once all workspaces have been initialized
81+
await this.update(workspacesPaths)
6782
}
6883

6984
async execCreate ({ args, path }) {
@@ -196,6 +211,34 @@ class Init extends BaseCommand {
196211

197212
await pkgJson.save()
198213
}
214+
215+
async update (workspacesPaths) {
216+
// translate workspaces paths into an array containing workspaces names
217+
const workspaces = []
218+
for (const path of workspacesPaths) {
219+
const pkgPath = resolve(path, 'package.json')
220+
const { name } = await rpj(pkgPath)
221+
.catch(() => ({}))
222+
223+
if (name) {
224+
workspaces.push(name)
225+
}
226+
}
227+
228+
const {
229+
config,
230+
flatOptions,
231+
localPrefix,
232+
} = this.npm
233+
234+
await updateWorkspaces({
235+
config,
236+
flatOptions,
237+
localPrefix,
238+
npm: this.npm,
239+
workspaces,
240+
})
241+
}
199242
}
200243

201244
module.exports = Init

lib/commands/version.js

Lines changed: 15 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@ const { resolve } = require('path')
33
const { promisify } = require('util')
44
const readFile = promisify(require('fs').readFile)
55

6-
const Arborist = require('@npmcli/arborist')
7-
const reifyFinish = require('../utils/reify-finish.js')
8-
6+
const updateWorkspaces = require('../workspaces/update-workspaces.js')
97
const BaseCommand = require('../base-command.js')
108

119
class Version extends BaseCommand {
@@ -137,32 +135,20 @@ class Version extends BaseCommand {
137135
return this.list(results)
138136
}
139137

140-
async update (args) {
141-
if (!this.npm.flatOptions.workspacesUpdate || !args.length) {
142-
return
143-
}
144-
145-
// default behavior is to not save by default in order to avoid
146-
// race condition problems when publishing multiple workspaces
147-
// that have dependencies on one another, it might still be useful
148-
// in some cases, which then need to set --save
149-
const save = this.npm.config.isDefault('save')
150-
? false
151-
: this.npm.config.get('save')
152-
153-
// runs a minimalistic reify update, targetting only the workspaces
154-
// that had version updates and skipping fund/audit/save
155-
const opts = {
156-
...this.npm.flatOptions,
157-
audit: false,
158-
fund: false,
159-
path: this.npm.localPrefix,
160-
save,
161-
}
162-
const arb = new Arborist(opts)
163-
164-
await arb.reify({ ...opts, update: args })
165-
await reifyFinish(this.npm, arb)
138+
async update (workspaces) {
139+
const {
140+
config,
141+
flatOptions,
142+
localPrefix,
143+
} = this.npm
144+
145+
await updateWorkspaces({
146+
config,
147+
flatOptions,
148+
localPrefix,
149+
npm: this.npm,
150+
workspaces,
151+
})
166152
}
167153
}
168154

lib/workspaces/update-workspaces.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
'use strict'
2+
3+
const Arborist = require('@npmcli/arborist')
4+
const reifyFinish = require('../utils/reify-finish.js')
5+
6+
async function updateWorkspaces ({
7+
config,
8+
flatOptions,
9+
localPrefix,
10+
npm,
11+
workspaces,
12+
}) {
13+
if (!flatOptions.workspacesUpdate || !workspaces.length) {
14+
return
15+
}
16+
17+
// default behavior is to not save by default in order to avoid
18+
// race condition problems when publishing multiple workspaces
19+
// that have dependencies on one another, it might still be useful
20+
// in some cases, which then need to set --save
21+
const save = config.isDefault('save')
22+
? false
23+
: config.get('save')
24+
25+
// runs a minimalistic reify update, targetting only the workspaces
26+
// that had version updates and skipping fund/audit/save
27+
const opts = {
28+
...flatOptions,
29+
audit: false,
30+
fund: false,
31+
path: localPrefix,
32+
save,
33+
}
34+
const arb = new Arborist(opts)
35+
36+
await arb.reify({ ...opts, update: workspaces })
37+
await reifyFinish(npm, arb)
38+
}
39+
40+
module.exports = updateWorkspaces

tap-snapshots/test/lib/commands/init.js.test.cjs

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,26 +10,53 @@ Array []
1010
`
1111

1212
exports[`test/lib/commands/init.js TAP workspaces no args > should print helper info 1`] = `
13+
Array []
14+
`
15+
16+
exports[`test/lib/commands/init.js TAP workspaces no args, existing folder > should print helper info 1`] = `
17+
Array []
18+
`
19+
20+
exports[`test/lib/commands/init.js TAP workspaces post workspace-init reify > should print helper info 1`] = `
1321
Array [
1422
Array [
1523
String(
16-
This utility will walk you through creating a package.json file.
17-
It only covers the most common items, and tries to guess sensible defaults.
18-
19-
See \`npm help init\` for definitive documentation on these fields
20-
and exactly what they do.
21-
22-
Use \`npm install <pkg>\` afterwards to install a package and
23-
save it as a dependency in the package.json file.
2424
25-
Press ^C at any time to quit.
25+
added 1 package in 100ms
2626
),
2727
],
2828
]
2929
`
3030

31-
exports[`test/lib/commands/init.js TAP workspaces no args, existing folder > should print helper info 1`] = `
32-
Array []
31+
exports[`test/lib/commands/init.js TAP workspaces post workspace-init reify > should reify tree on init ws complete 1`] = `
32+
{
33+
"name": "top-level",
34+
"lockfileVersion": 2,
35+
"requires": true,
36+
"packages": {
37+
"": {
38+
"name": "top-level",
39+
"workspaces": [
40+
"a"
41+
]
42+
},
43+
"a": {
44+
"version": "1.0.0",
45+
"license": "ISC",
46+
"devDependencies": {}
47+
},
48+
"node_modules/a": {
49+
"resolved": "a",
50+
"link": true
51+
}
52+
},
53+
"dependencies": {
54+
"a": {
55+
"version": "file:a"
56+
}
57+
}
58+
}
59+
3360
`
3461

3562
exports[`test/lib/commands/init.js TAP workspaces with arg but missing workspace folder > should print helper info 1`] = `

tap-snapshots/test/lib/load-all-commands.js.test.cjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -396,7 +396,7 @@ npm init [<@scope>/]<name> (same as \`npx [<@scope>/]create-<name>\`)
396396
Options:
397397
[-y|--yes] [-f|--force]
398398
[-w|--workspace <workspace-name> [-w|--workspace <workspace-name> ...]]
399-
[-ws|--workspaces] [--include-workspace-root]
399+
[-ws|--workspaces] [--no-workspaces-update] [--include-workspace-root]
400400
401401
aliases: create, innit
402402

tap-snapshots/test/lib/npm.js.test.cjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -486,7 +486,7 @@ All commands:
486486
Options:
487487
[-y|--yes] [-f|--force]
488488
[-w|--workspace <workspace-name> [-w|--workspace <workspace-name> ...]]
489-
[-ws|--workspaces] [--include-workspace-root]
489+
[-ws|--workspaces] [--no-workspaces-update] [--include-workspace-root]
490490
491491
aliases: create, innit
492492

test/lib/commands/init.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,7 @@ t.test('workspaces', t => {
288288
t.teardown(() => {
289289
npm._mockOutputs.length = 0
290290
})
291+
npm._mockOutputs.length = 0
291292
npm.localPrefix = t.testdir({
292293
'package.json': JSON.stringify({
293294
name: 'top-level',
@@ -306,6 +307,39 @@ t.test('workspaces', t => {
306307
t.matchSnapshot(npm._mockOutputs, 'should print helper info')
307308
})
308309

310+
t.test('post workspace-init reify', async t => {
311+
const _consolelog = console.log
312+
console.log = () => null
313+
t.teardown(() => {
314+
console.log = _consolelog
315+
npm._mockOutputs.length = 0
316+
delete npm.flatOptions.workspacesUpdate
317+
})
318+
npm.started = Date.now()
319+
npm._mockOutputs.length = 0
320+
npm.flatOptions.workspacesUpdate = true
321+
npm.localPrefix = t.testdir({
322+
'package.json': JSON.stringify({
323+
name: 'top-level',
324+
}),
325+
})
326+
327+
const Init = t.mock('../../../lib/commands/init.js', {
328+
...mocks,
329+
'init-package-json': (dir, initFile, config, cb) => {
330+
t.equal(dir, resolve(npm.localPrefix, 'a'), 'should use the ws path')
331+
return require('init-package-json')(dir, initFile, config, cb)
332+
},
333+
})
334+
const init = new Init(npm)
335+
await init.execWorkspaces([], ['a'])
336+
const output = npm._mockOutputs.map(arr => arr.map(i => i.replace(/[0-9]*ms$/, '100ms')))
337+
t.matchSnapshot(output, 'should print helper info')
338+
const lockFilePath = resolve(npm.localPrefix, 'package-lock.json')
339+
const lockFile = fs.readFileSync(lockFilePath, { encoding: 'utf8' })
340+
t.matchSnapshot(lockFile, 'should reify tree on init ws complete')
341+
})
342+
309343
t.test('no args, existing folder', async t => {
310344
t.teardown(() => {
311345
npm._mockOutputs.length = 0

0 commit comments

Comments
 (0)