-
-
Notifications
You must be signed in to change notification settings - Fork 152
/
Copy pathpublish.js
200 lines (174 loc) · 5.63 KB
/
publish.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
'use strict';
module.exports = publish;
const { createReadStream, promises: fs } = require('fs');
const packlist = require('npm-packlist');
const figgy = require('figgy-pudding');
const FormData = require('form-data');
const fetch = require('../fetch');
const semver = require('semver');
const path = require('path');
const zlib = require('zlib');
const loadPackageToml = require('../load-package-toml');
const parseSpec = require('../canonicalize-spec');
const publishOpts = figgy({
registries: true,
registry: true,
token: true,
require2fa: true,
requiretfa: true,
tfa: true,
log: { default: require('npmlog') }
});
async function publish(opts) {
opts = publishOpts(opts);
const { location, content } = await loadPackageToml(process.cwd());
const spec = parseSpec(content.name, opts.registry);
const host =
`https://${spec.host}` in opts.registries
? `https://${spec.host}`
: `http://${spec.host}` in opts.registries
? `http://${spec.host}`
: null;
if (!host) {
opts.log.error(
`You need to log in to "https://${
spec.host
}" publish packages. Run \`ds login --registry "https://${spec.host}"\`.`
);
return 1;
}
const { token } = opts.registries[host];
if (!token) {
opts.log.error(
`You need to log in to "${host}" publish packages. Run \`ds login --registry "${host}"\`.`
);
return 1;
}
// refuse to publish if:
// there's no token
// Package.toml can't be found
// Package.toml name isnt in the form "foo/bar"
// Package.toml contains "private": true
// Package.toml contains no semver "version"
// try to determine whether the package exists or not
// create it if it doesn't
// then create a packlist
// then create a multipart request and send it
if (content.private) {
opts.log.error(
'This Package.toml is marked private. Cowardly refusing to publish.'
);
return 1;
}
const bits = content.name.split('/');
if (bits.length !== 2) {
opts.log.error('Packages published to entropic MUST be namespaced.');
return 1;
}
if (bits[0].split('@').length !== 2) {
opts.log.error(
'Expected the namespace portion of the package name to contain a hostname (e.g.: "[email protected]/billions")'
);
return 1;
}
if (content.version !== semver.clean(content.version || '')) {
opts.log.error('Expected valid semver "version" field at top level.');
return 1;
}
const pkgReq = await fetch(`${host}/v1/packages/package/${spec.canonical}`);
const mustCreate = pkgReq.status === 404;
if (mustCreate) {
const request = await fetch(
`${host}/v1/packages/package/${spec.canonical}`,
{
body:
opts.require2fa || opts.requiretfa || opts.tfa
? '{"require_tfa": true}'
: '{}',
method: 'PUT',
headers: {
authorization: `Bearer ${token}`,
'content-type': 'application/json'
}
}
);
const body = await request.json();
if (request.status > 399) {
opts.log.error('Failed to create package:');
opts.log.error(body.message || body);
return 1;
}
} else if (pkgReq.status < 300) {
const result = await pkgReq.json();
if (result.versions[content.version]) {
opts.log.warn('It looks like this version has already been published.');
opts.log.warn('Trying anyway, because hope springs eternal.');
}
}
const files = await packlist({ path: location });
const form = new FormData();
form.append('dependencies', JSON.stringify(content.dependencies || {}));
form.append('devDependencies', JSON.stringify(content.devDependencies || {}));
form.append(
'optionalDependencies',
JSON.stringify(content.optionalDependencies || {})
);
form.append(
'peerDependencies',
JSON.stringify(content.peerDependencies || {})
);
form.append(
'bundledDependencies',
JSON.stringify(content.bundledDependencies || {})
);
const keyed = xs => {
const ext = path.extname(xs);
const basename = path.basename(xs).replace(ext, '');
const dirname = path.dirname(basename);
return `${ext || 'NUL'}${basename}${dirname}`;
};
// CD: re-sort the list, putting files with the same extension nearby, then
// files with the same extension and basename, finally comparing directory
// names. Since we're shoving this through zlib, the closer we can put
// similar data, the better. Doing this on a sample package netted a 10%
// decrease in request body size. This is cribbed from git.
files.sort((lhs, rhs) => {
return keyed(lhs).localeCompare(keyed(rhs));
});
for (const file of files) {
const encoded = encodeURIComponent(
'package/' + file.split(path.sep).join('/')
);
// use append's ability to append a lazily evaluated function so we don't
// try to open, say, 10K fds at once.
form.append(
'entry[]',
next => next(createReadStream(path.join(location, file))),
{
filename: encoded
}
);
}
form.append('x-clacks-overhead', 'GNU/Terry Pratchett'); // this is load bearing, obviously
const request = await fetch(
`${host}/v1/packages/package/${
spec.canonical
}/versions/${encodeURIComponent(content.version)}`,
{
method: 'PUT',
body: form.pipe(zlib.createDeflate()),
headers: {
'transfer-encoding': 'chunked',
'content-encoding': 'deflate',
authorization: `Bearer ${token}`,
...form.getHeaders()
}
}
);
const body = await request.json();
if (!request.ok) {
opts.log.error(body.message || body);
return 1;
}
console.log(`+ ${spec.canonical} @ ${content.version}`);
}