Skip to content

Commit d8d374d

Browse files
wraithgarlukekarrys
authored andcommitted
fix: consolidate split-package-names
It was only ever used by `npm edit` so it's inline now. Rewrote `npm edit` tests to be real
1 parent 52dfaf2 commit d8d374d

File tree

10 files changed

+150
-177
lines changed

10 files changed

+150
-177
lines changed

lib/commands/edit.js

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,32 @@
33

44
const { resolve } = require('path')
55
const fs = require('graceful-fs')
6-
const { spawn } = require('child_process')
7-
const splitPackageNames = require('../utils/split-package-names.js')
6+
const cp = require('child_process')
87
const completion = require('../utils/completion/installed-shallow.js')
98
const BaseCommand = require('../base-command.js')
109

10+
const splitPackageNames = (path) => {
11+
return path.split('/')
12+
// combine scoped parts
13+
.reduce((parts, part) => {
14+
if (parts.length === 0) {
15+
return [part]
16+
}
17+
18+
const lastPart = parts[parts.length - 1]
19+
// check if previous part is the first part of a scoped package
20+
if (lastPart[0] === '@' && !lastPart.includes('/')) {
21+
parts[parts.length - 1] += '/' + part
22+
} else {
23+
parts.push(part)
24+
}
25+
26+
return parts
27+
}, [])
28+
.join('/node_modules/')
29+
.replace(/(\/node_modules)+/, '/node_modules')
30+
}
31+
1132
class Edit extends BaseCommand {
1233
static description = 'Edit an installed package'
1334
static name = 'edit'
@@ -36,7 +57,7 @@ class Edit extends BaseCommand {
3657
return reject(err)
3758
}
3859
const [bin, ...args] = this.npm.config.get('editor').split(/\s+/)
39-
const editor = spawn(bin, [...args, dir], { stdio: 'inherit' })
60+
const editor = cp.spawn(bin, [...args, dir], { stdio: 'inherit' })
4061
editor.on('exit', (code) => {
4162
if (code) {
4263
return reject(new Error(`editor process exited with code: ${code}`))

lib/utils/split-package-names.js

Lines changed: 0 additions & 25 deletions
This file was deleted.

test/fixtures/tspawk.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
'use strict'
2+
3+
const spawk = require('spawk')
4+
5+
module.exports = tspawk
6+
7+
function tspawk (t) {
8+
spawk.preventUnmatched()
9+
t.teardown(function () {
10+
spawk.unload()
11+
})
12+
t.afterEach(function () {
13+
spawk.done()
14+
})
15+
return spawk
16+
}

test/lib/commands/config.js

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
const { join } = require('path')
22
const { promisify } = require('util')
33
const fs = require('fs')
4-
const spawk = require('spawk')
4+
const tspawk = require('../../fixtures/tspawk')
55
const t = require('tap')
66

7-
spawk.preventUnmatched()
7+
const spawk = tspawk(t)
88

99
const readFile = promisify(fs.readFile)
1010

@@ -369,10 +369,6 @@ t.test('config edit', async t => {
369369
'.npmrc': 'foo=bar\nbar=baz',
370370
})
371371

372-
t.teardown(() => {
373-
spawk.clean()
374-
})
375-
376372
const EDITOR = 'vim'
377373
const editor = spawk.spawn(EDITOR).exit(0)
378374

@@ -394,10 +390,6 @@ t.test('config edit', async t => {
394390
})
395391

396392
t.test('config edit - editor exits non-0', async t => {
397-
t.teardown(() => {
398-
spawk.clean()
399-
})
400-
401393
const EDITOR = 'vim'
402394
const editor = spawk.spawn(EDITOR).exit(1)
403395

test/lib/commands/edit.js

Lines changed: 100 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -1,138 +1,137 @@
11
const t = require('tap')
2-
const { resolve } = require('path')
3-
const { EventEmitter } = require('events')
2+
const path = require('path')
3+
const tspawk = require('../../fixtures/tspawk')
4+
const { load: loadMockNpm } = require('../../fixtures/mock-npm')
45

5-
let editorBin = null
6-
let editorArgs = null
7-
let editorOpts = null
8-
let EDITOR_CODE = 0
9-
const childProcess = {
10-
spawn: (bin, args, opts) => {
11-
// save for assertions
12-
editorBin = bin
13-
editorArgs = args
14-
editorOpts = opts
6+
const spawk = tspawk(t)
157

16-
const editorEvents = new EventEmitter()
17-
process.nextTick(() => {
18-
editorEvents.emit('exit', EDITOR_CODE)
19-
})
20-
return editorEvents
21-
},
22-
}
8+
// TODO this ... smells. npm "script-shell" config mentions defaults but those
9+
// are handled by run-script, not npm. So for now we have to tie tests to some
10+
// pretty specific internals of runScript
11+
const makeSpawnArgs = require('@npmcli/run-script/lib/make-spawn-args.js')
2312

24-
let rebuildArgs = null
25-
let rebuildFail = null
26-
let EDITOR = 'vim'
27-
const npm = {
13+
const npmConfig = {
2814
config: {
29-
get: () => EDITOR,
15+
'ignore-scripts': false,
16+
editor: 'testeditor',
3017
},
31-
dir: resolve(__dirname, '../../../node_modules'),
32-
exec: async (cmd, args) => {
33-
rebuildArgs = args
34-
if (rebuildFail) {
35-
throw rebuildFail
36-
}
18+
prefixDir: {
19+
node_modules: {
20+
semver: {
21+
'package.json': JSON.stringify({
22+
scripts: {
23+
install: 'testinstall',
24+
},
25+
}),
26+
node_modules: {
27+
abbrev: {},
28+
},
29+
},
30+
'@npmcli': {
31+
'scoped-package': {},
32+
},
33+
},
3734
},
3835
}
3936

40-
const gracefulFs = require('graceful-fs')
41-
const Edit = t.mock('../../../lib/commands/edit.js', {
42-
child_process: childProcess,
43-
'graceful-fs': gracefulFs,
44-
})
45-
const edit = new Edit(npm)
46-
4737
t.test('npm edit', async t => {
48-
t.teardown(() => {
49-
rebuildArgs = null
50-
editorBin = null
51-
editorArgs = null
52-
editorOpts = null
53-
})
38+
const { npm, joinedOutput } = await loadMockNpm(t, npmConfig)
5439

55-
await edit.exec(['semver'])
56-
const path = resolve(__dirname, '../../../node_modules/semver')
57-
t.strictSame(editorBin, EDITOR, 'used the correct editor')
58-
t.strictSame(editorArgs, [path], 'edited the correct directory')
59-
t.strictSame(editorOpts, { stdio: 'inherit' }, 'passed the correct opts')
60-
t.strictSame(rebuildArgs, [path], 'passed the correct path to rebuild')
40+
const semverPath = path.resolve(npm.prefix, 'node_modules', 'semver')
41+
const [scriptShell] = makeSpawnArgs({
42+
event: 'install',
43+
path: npm.prefix,
44+
})
45+
spawk.spawn('testeditor', [semverPath])
46+
spawk.spawn(
47+
scriptShell,
48+
args => args.includes('testinstall'),
49+
{ cwd: semverPath }
50+
)
51+
await npm.exec('edit', ['semver'])
52+
t.match(joinedOutput(), 'rebuilt dependencies successfully')
6153
})
6254

63-
t.test('rebuild fails', async t => {
64-
t.teardown(() => {
65-
rebuildFail = null
66-
rebuildArgs = null
67-
editorBin = null
68-
editorArgs = null
69-
editorOpts = null
55+
t.test('rebuild failure', async t => {
56+
const { npm } = await loadMockNpm(t, npmConfig)
57+
const semverPath = path.resolve(npm.prefix, 'node_modules', 'semver')
58+
const [scriptShell] = makeSpawnArgs({
59+
event: 'install',
60+
path: npm.prefix,
7061
})
62+
spawk.spawn('testeditor', [semverPath])
63+
spawk.spawn(
64+
scriptShell,
65+
args => args.includes('testinstall'),
66+
{ cwd: semverPath }
67+
).exit(1).stdout('test error')
68+
await t.rejects(
69+
npm.exec('edit', ['semver']),
70+
{ message: 'command failed' }
71+
)
72+
})
7173

72-
rebuildFail = new Error('test error')
74+
t.test('editor failure', async t => {
75+
const { npm } = await loadMockNpm(t, npmConfig)
76+
const semverPath = path.resolve(npm.prefix, 'node_modules', 'semver')
77+
spawk.spawn('testeditor', [semverPath]).exit(1).stdout('test editor failure')
7378
await t.rejects(
74-
edit.exec(['semver']),
75-
{ message: 'test error' }
79+
npm.exec('edit', ['semver']),
80+
{ message: 'editor process exited with code: 1' }
7681
)
77-
const path = resolve(__dirname, '../../../node_modules/semver')
78-
t.strictSame(editorBin, EDITOR, 'used the correct editor')
79-
t.strictSame(editorArgs, [path], 'edited the correct directory')
80-
t.strictSame(editorOpts, { stdio: 'inherit' }, 'passed the correct opts')
81-
t.strictSame(rebuildArgs, [path], 'passed the correct path to rebuild')
8282
})
8383

8484
t.test('npm edit editor has flags', async t => {
85-
EDITOR = 'code -w'
86-
t.teardown(() => {
87-
rebuildArgs = null
88-
editorBin = null
89-
editorArgs = null
90-
editorOpts = null
91-
EDITOR = 'vim'
85+
const { npm } = await loadMockNpm(t, {
86+
...npmConfig,
87+
config: {
88+
...npmConfig.config,
89+
editor: 'testeditor --flag',
90+
},
9291
})
9392

94-
await edit.exec(['semver'])
95-
96-
const path = resolve(__dirname, '../../../node_modules/semver')
97-
t.strictSame(editorBin, 'code', 'used the correct editor')
98-
t.strictSame(editorArgs, ['-w', path], 'edited the correct directory, keeping flags')
99-
t.strictSame(editorOpts, { stdio: 'inherit' }, 'passed the correct opts')
100-
t.strictSame(rebuildArgs, [path], 'passed the correct path to rebuild')
93+
const semverPath = path.resolve(npm.prefix, 'node_modules', 'semver')
94+
const [scriptShell] = makeSpawnArgs({
95+
event: 'install',
96+
path: npm.prefix,
97+
})
98+
spawk.spawn('testeditor', ['--flag', semverPath])
99+
spawk.spawn(
100+
scriptShell,
101+
args => args.includes('testinstall'),
102+
{ cwd: semverPath }
103+
)
104+
await npm.exec('edit', ['semver'])
101105
})
102106

103107
t.test('npm edit no args', async t => {
108+
const { npm } = await loadMockNpm(t)
104109
await t.rejects(
105-
edit.exec([]),
106-
/npm edit/,
110+
npm.exec('edit', []),
111+
{ code: 'EUSAGE' },
107112
'throws usage error'
108113
)
109114
})
110115

111-
t.test('npm edit lstat error propagates', async t => {
112-
const _lstat = gracefulFs.lstat
113-
gracefulFs.lstat = (dir, cb) => {
114-
return cb(new Error('lstat failed'))
115-
}
116-
t.teardown(() => {
117-
gracefulFs.lstat = _lstat
118-
})
116+
t.test('npm edit nonexistent package', async t => {
117+
const { npm } = await loadMockNpm(t, npmConfig)
119118

120119
await t.rejects(
121-
edit.exec(['semver']),
122-
/lstat failed/,
123-
'user received correct error'
120+
npm.exec('edit', ['abbrev']),
121+
/lstat/
124122
)
125123
})
126124

127-
t.test('npm edit editor exit code error propagates', async t => {
128-
EDITOR_CODE = 137
129-
t.teardown(() => {
130-
EDITOR_CODE = 0
131-
})
125+
t.test('scoped package', async t => {
126+
const { npm } = await loadMockNpm(t, npmConfig)
127+
const scopedPath = path.resolve(npm.prefix, 'node_modules', '@npmcli', 'scoped-package')
128+
spawk.spawn('testeditor', [scopedPath])
129+
await npm.exec('edit', ['@npmcli/scoped-package'])
130+
})
132131

133-
await t.rejects(
134-
edit.exec(['semver']),
135-
/exited with code: 137/,
136-
'user received correct error'
137-
)
132+
t.test('subdependency', async t => {
133+
const { npm } = await loadMockNpm(t, npmConfig)
134+
const subdepPath = path.resolve(npm.prefix, 'node_modules', 'semver', 'node_modules', 'abbrev')
135+
spawk.spawn('testeditor', [subdepPath])
136+
await npm.exec('edit', ['semver/abbrev'])
138137
})

test/lib/commands/restart.js

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
const t = require('tap')
2-
const spawk = require('spawk')
2+
const tspawk = require('../../fixtures/tspawk')
33
const { load: loadMockNpm } = require('../../fixtures/mock-npm')
44

5-
spawk.preventUnmatched()
6-
t.teardown(() => {
7-
spawk.unload()
8-
})
5+
const spawk = tspawk(t)
96

107
// TODO this ... smells. npm "script-shell" config mentions defaults but those
118
// are handled by run-script, not npm. So for now we have to tie tests to some

test/lib/commands/start.js

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
const t = require('tap')
2-
const spawk = require('spawk')
2+
const tspawk = require('../../fixtures/tspawk')
33
const { load: loadMockNpm } = require('../../fixtures/mock-npm')
44

5-
spawk.preventUnmatched()
6-
t.teardown(() => {
7-
spawk.unload()
8-
})
5+
const spawk = tspawk(t)
96

107
// TODO this ... smells. npm "script-shell" config mentions defaults but those
118
// are handled by run-script, not npm. So for now we have to tie tests to some

0 commit comments

Comments
 (0)