Skip to content

Commit a1f9be8

Browse files
committed
Support publishing any kind of spec, not just directories
Credit: @isaacs Close: #2074 Reviewed-by: @nlf
1 parent 7d88f17 commit a1f9be8

File tree

3 files changed

+140
-52
lines changed

3 files changed

+140
-52
lines changed

lib/publish.js

Lines changed: 41 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ const semver = require('semver')
66
const pack = require('libnpmpack')
77
const libpub = require('libnpmpublish').publish
88
const runScript = require('@npmcli/run-script')
9+
const pacote = require('pacote')
10+
const npa = require('npm-package-arg')
911

1012
const npm = require('./npm.js')
1113
const output = require('./utils/output.js')
@@ -49,6 +51,12 @@ const publish = async args => {
4951
return tarball
5052
}
5153

54+
// if it's a directory, read it from the file system
55+
// otherwise, get the full metadata from whatever it is
56+
const getManifest = (spec, opts) =>
57+
spec.type === 'directory' ? readJson(`${spec.fetchSpec}/package.json`)
58+
: pacote.manifest(spec, { ...opts, fullMetadata: true })
59+
5260
// for historical reasons, publishConfig in package.json can contain
5361
// ANY config keys that npm supports in .npmrc files and elsewhere.
5462
// We *may* want to revisit this at some point, and have a minimal set
@@ -61,50 +69,57 @@ const publishConfigToOpts = publishConfig =>
6169

6270
const publish_ = async (arg, opts) => {
6371
const { unicode, dryRun, json } = opts
64-
const manifest = await readJson(`${arg}/package.json`)
72+
// you can publish name@version, ./foo.tgz, etc.
73+
// even though the default is the 'file:.' cwd.
74+
const spec = npa(arg)
75+
const manifest = await getManifest(spec, opts)
6576

6677
if (manifest.publishConfig)
6778
Object.assign(opts, publishConfigToOpts(manifest.publishConfig))
6879

69-
// prepublishOnly
70-
await runScript({
71-
event: 'prepublishOnly',
72-
path: arg,
73-
stdio: 'inherit',
74-
pkg: manifest,
75-
})
80+
// only run scripts for directory type publishes
81+
if (spec.type === 'directory') {
82+
await runScript({
83+
event: 'prepublishOnly',
84+
path: spec.fetchSpec,
85+
stdio: 'inherit',
86+
pkg: manifest,
87+
})
88+
}
7689

77-
const tarballData = await pack(arg, opts)
90+
const tarballData = await pack(spec, opts)
7891
const pkgContents = await getContents(manifest, tarballData)
7992

93+
// note that logTar calls npmlog.notice(), so if we ARE in silent mode,
94+
// this will do nothing, but we still want it in the debuglog if it fails.
8095
if (!json)
8196
logTar(pkgContents, { log, unicode })
8297

8398
if (!dryRun) {
8499
// The purpose of re-reading the manifest is in case it changed,
85100
// so that we send the latest and greatest thing to the registry
86101
// note that publishConfig might have changed as well!
87-
const manifest = await readJson(`${arg}/package.json`, opts)
102+
const manifest = await getManifest(spec, opts)
88103
if (manifest.publishConfig)
89104
Object.assign(opts, publishConfigToOpts(manifest.publishConfig))
90-
await otplease(opts, opts => libpub(arg, manifest, opts))
105+
await otplease(opts, opts => libpub(manifest, tarballData, opts))
91106
}
92107

93-
// publish
94-
await runScript({
95-
event: 'publish',
96-
path: arg,
97-
stdio: 'inherit',
98-
pkg: manifest,
99-
})
100-
101-
// postpublish
102-
await runScript({
103-
event: 'postpublish',
104-
path: arg,
105-
stdio: 'inherit',
106-
pkg: manifest,
107-
})
108+
if (spec.type === 'directory') {
109+
await runScript({
110+
event: 'publish',
111+
path: spec.fetchSpec,
112+
stdio: 'inherit',
113+
pkg: manifest,
114+
})
115+
116+
await runScript({
117+
event: 'postpublish',
118+
path: spec.fetchSpec,
119+
stdio: 'inherit',
120+
pkg: manifest,
121+
})
122+
}
108123

109124
return pkgContents
110125
}

node_modules/@npmcli/config/lib/set-envs.js

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/lib/publish.js

Lines changed: 97 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ const config = { list: [defaults] }
77
const fs = require('fs')
88

99
t.test('should publish with libnpmpublish, respecting publishConfig', (t) => {
10+
t.plan(5)
11+
1012
const publishConfig = { registry: 'https://some.registry' }
1113
const testDir = t.testdir({
1214
'package.json': JSON.stringify({
@@ -41,27 +43,27 @@ t.test('should publish with libnpmpublish, respecting publishConfig', (t) => {
4143
name: 'my-cool-pkg',
4244
version: '1.0.0',
4345
}))
44-
return ''
46+
return Buffer.from('')
4547
},
4648
libnpmpublish: {
47-
publish: (arg, manifest, opts) => {
48-
t.ok(arg, 'gets path')
49-
t.ok(manifest, 'gets manifest')
49+
publish: (manifest, tarData, opts) => {
50+
t.match(manifest, { name: 'my-cool-pkg', version: '1.0.0' }, 'gets manifest')
51+
t.isa(tarData, Buffer, 'tarData is a buffer')
5052
t.ok(opts, 'gets opts object')
5153
t.same(opts.registry, publishConfig.registry, 'publishConfig is passed through')
52-
t.ok(true, 'libnpmpublish is called')
5354
},
5455
},
5556
})
5657

5758
publish([testDir], (er) => {
5859
if (er)
5960
throw er
60-
t.end()
61+
t.pass('got to callback')
6162
})
6263
})
6364

6465
t.test('re-loads publishConfig if added during script process', (t) => {
66+
t.plan(5)
6567
const publishConfig = { registry: 'https://some.registry' }
6668
const testDir = t.testdir({
6769
'package.json': JSON.stringify({
@@ -94,27 +96,28 @@ t.test('re-loads publishConfig if added during script process', (t) => {
9496
version: '1.0.0',
9597
publishConfig,
9698
}))
97-
return ''
99+
return Buffer.from('')
98100
},
99101
libnpmpublish: {
100-
publish: (arg, manifest, opts) => {
101-
t.ok(arg, 'gets path')
102-
t.ok(manifest, 'gets manifest')
102+
publish: (manifest, tarData, opts) => {
103+
t.match(manifest, { name: 'my-cool-pkg', version: '1.0.0' }, 'gets manifest')
104+
t.isa(tarData, Buffer, 'tarData is a buffer')
103105
t.ok(opts, 'gets opts object')
104106
t.same(opts.registry, publishConfig.registry, 'publishConfig is passed through')
105-
t.ok(true, 'libnpmpublish is called')
106107
},
107108
},
108109
})
109110

110111
publish([testDir], (er) => {
111112
if (er)
112113
throw er
113-
t.end()
114+
t.pass('got to callback')
114115
})
115116
})
116117

117118
t.test('should not log if silent', (t) => {
119+
t.plan(2)
120+
118121
const testDir = t.testdir({
119122
'package.json': JSON.stringify({
120123
name: 'my-cool-pkg',
@@ -133,29 +136,38 @@ t.test('should not log if silent', (t) => {
133136
},
134137
'../../lib/utils/tar.js': {
135138
getContents: () => ({}),
136-
logTar: () => {},
139+
logTar: () => {
140+
t.pass('called logTar (but nothing actually printed)')
141+
},
137142
},
138143
'../../lib/utils/otplease.js': (opts, fn) => {
139144
return Promise.resolve().then(() => fn(opts))
140145
},
146+
'../../lib/utils/output.js': () => {
147+
throw new Error('should not output in silent mode')
148+
},
141149
npmlog: {
142150
verbose: () => {},
151+
notice: () => {},
143152
level: 'silent',
144153
},
145154
libnpmpack: async () => '',
146155
libnpmpublish: {
147-
publish: () => {},
156+
publish: (manifest, tarData, opts) => {
157+
throw new Error('should not call libnpmpublish!')
158+
},
148159
},
149160
})
150161

151162
publish([testDir], (er) => {
152163
if (er)
153164
throw er
154-
t.end()
165+
t.pass('got to callback')
155166
})
156167
})
157168

158169
t.test('should log tarball contents', (t) => {
170+
t.plan(3)
159171
const testDir = t.testdir({
160172
'package.json': JSON.stringify({
161173
name: 'my-cool-pkg',
@@ -177,29 +189,32 @@ t.test('should log tarball contents', (t) => {
177189
id: 'someid',
178190
}),
179191
logTar: () => {
180-
t.ok(true, 'logTar is called')
192+
t.pass('logTar is called')
181193
},
182194
},
183195
'../../lib/utils/output.js': () => {
184-
t.ok(true, 'output fn is called')
196+
t.pass('output fn is called')
185197
},
186198
'../../lib/utils/otplease.js': (opts, fn) => {
187199
return Promise.resolve().then(() => fn(opts))
188200
},
189201
libnpmpack: async () => '',
190202
libnpmpublish: {
191-
publish: () => {},
203+
publish: () => {
204+
throw new Error('should not call libnpmpublish!')
205+
},
192206
},
193207
})
194208

195209
publish([testDir], (er) => {
196210
if (er)
197211
throw er
198-
t.end()
212+
t.pass('got to callback')
199213
})
200214
})
201215

202216
t.test('shows usage with wrong set of arguments', (t) => {
217+
t.plan(1)
203218
const publish = requireInject('../../lib/publish.js', {
204219
'../../lib/npm.js': {
205220
flatOptions: {
@@ -210,13 +225,11 @@ t.test('shows usage with wrong set of arguments', (t) => {
210225
},
211226
})
212227

213-
publish(['a', 'b', 'c'], (er) => {
214-
t.matchSnapshot(er, 'should print usage')
215-
t.end()
216-
})
228+
publish(['a', 'b', 'c'], (er) => t.matchSnapshot(er, 'should print usage'))
217229
})
218230

219231
t.test('throws when invalid tag', (t) => {
232+
t.plan(1)
220233
const publish = requireInject('../../lib/publish.js', {
221234
'../../lib/npm.js': {
222235
flatOptions: {
@@ -228,7 +241,66 @@ t.test('throws when invalid tag', (t) => {
228241
})
229242

230243
publish([], (err) => {
231-
t.ok(err, 'throws when tag name is a valid SemVer range')
232-
t.end()
244+
t.match(err, {
245+
message: /Tag name must not be a valid SemVer range: /,
246+
}, 'throws when tag name is a valid SemVer range')
247+
})
248+
})
249+
250+
t.test('can publish a tarball', t => {
251+
t.plan(3)
252+
const testDir = t.testdir({
253+
package: {
254+
'package.json': JSON.stringify({
255+
name: 'my-cool-tarball',
256+
version: '1.2.3',
257+
}),
258+
},
259+
})
260+
const tar = require('tar')
261+
tar.c({
262+
cwd: testDir,
263+
file: `${testDir}/package.tgz`,
264+
sync: true,
265+
}, ['package'])
266+
267+
// no cheating! read it from the tarball.
268+
fs.unlinkSync(`${testDir}/package/package.json`)
269+
fs.rmdirSync(`${testDir}/package`)
270+
271+
const tarFile = fs.readFileSync(`${testDir}/package.tgz`)
272+
const publish = requireInject('../../lib/publish.js', {
273+
'../../lib/npm.js': {
274+
flatOptions: {
275+
json: true,
276+
defaultTag: 'latest',
277+
},
278+
config,
279+
},
280+
'../../lib/utils/tar.js': {
281+
getContents: () => ({
282+
id: 'someid',
283+
}),
284+
logTar: () => {},
285+
},
286+
'../../lib/utils/output.js': () => {},
287+
'../../lib/utils/otplease.js': (opts, fn) => {
288+
return Promise.resolve().then(() => fn(opts))
289+
},
290+
libnpmpublish: {
291+
publish: (manifest, tarData, opts) => {
292+
t.match(manifest, {
293+
name: 'my-cool-tarball',
294+
version: '1.2.3',
295+
}, 'sent manifest to lib pub')
296+
t.strictSame(tarData, tarFile, 'sent the tarball data to lib pub')
297+
},
298+
},
299+
})
300+
301+
publish([`${testDir}/package.tgz`], (er) => {
302+
if (er)
303+
throw er
304+
t.pass('got to callback')
233305
})
234306
})

0 commit comments

Comments
 (0)