ovsx 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/lib/check-license.d.ts.map +1 -1
- package/lib/check-license.js +3 -2
- package/lib/check-license.js.map +1 -1
- package/lib/get.js +5 -5
- package/lib/get.js.map +1 -1
- package/lib/http.d.ts +32 -0
- package/lib/http.d.ts.map +1 -0
- package/lib/http.js +170 -0
- package/lib/http.js.map +1 -0
- package/lib/main.js +44 -41
- package/lib/main.js.map +1 -1
- package/lib/oidc.d.ts.map +1 -1
- package/lib/oidc.js +5 -47
- package/lib/oidc.js.map +1 -1
- package/lib/publish.d.ts.map +1 -1
- package/lib/publish.js +100 -32
- package/lib/publish.js.map +1 -1
- package/lib/registry.d.ts +49 -11
- package/lib/registry.d.ts.map +1 -1
- package/lib/registry.js +115 -124
- package/lib/registry.js.map +1 -1
- package/lib/util.d.ts +16 -12
- package/lib/util.d.ts.map +1 -1
- package/lib/util.js +44 -67
- package/lib/util.js.map +1 -1
- package/lib/verify.js +2 -2
- package/lib/verify.js.map +1 -1
- package/lib/zip.d.ts.map +1 -1
- package/lib/zip.js +2 -10
- package/lib/zip.js.map +1 -1
- package/package.json +14 -11
- package/src/check-license.ts +4 -3
- package/src/get.ts +6 -6
- package/src/http.ts +176 -0
- package/src/main.ts +48 -43
- package/src/oidc.ts +5 -47
- package/src/publish.ts +136 -34
- package/src/registry.ts +132 -124
- package/src/util.ts +40 -69
- package/src/verify.ts +3 -3
- package/src/zip.ts +2 -12
- package/lib/version.d.ts +0 -2
- package/lib/version.d.ts.map +0 -1
- package/lib/version.js +0 -5
- package/lib/version.js.map +0 -1
- package/src/version.ts +0 -1
package/src/main.ts
CHANGED
|
@@ -9,12 +9,13 @@
|
|
|
9
9
|
********************************************************************************/
|
|
10
10
|
|
|
11
11
|
import * as commander from 'commander';
|
|
12
|
-
import
|
|
12
|
+
import { readFileSync } from 'fs';
|
|
13
|
+
import { join } from 'path';
|
|
13
14
|
import { createNamespace } from './create-namespace';
|
|
14
15
|
import { verifyPat } from './verify-pat';
|
|
15
16
|
import { publish } from './publish';
|
|
16
17
|
import { unpublish } from './unpublish';
|
|
17
|
-
import { handleError, parseNonNegativeInt } from './util';
|
|
18
|
+
import { handleError, levenshtein, parseNonNegativeInt } from './util';
|
|
18
19
|
import { getExtension } from './get';
|
|
19
20
|
import { list } from './list';
|
|
20
21
|
import { DEFAULT_SEARCH_SIZE, SORT_KEYS, SORT_ORDERS, search } from './search';
|
|
@@ -23,10 +24,13 @@ import { verify } from './verify';
|
|
|
23
24
|
import { verifySignature } from './verify-signature';
|
|
24
25
|
import login from './login';
|
|
25
26
|
import logout from './logout';
|
|
26
|
-
|
|
27
|
+
|
|
28
|
+
// Resolves from both src/ (tests) and lib/ (published build); npm always ships package.json.
|
|
29
|
+
const LIB_VERSION: string = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8')).version;
|
|
27
30
|
|
|
28
31
|
module.exports = function (argv: string[]): void {
|
|
29
32
|
const program = new commander.Command();
|
|
33
|
+
program.nameFromFilename(argv[1]);
|
|
30
34
|
program.usage('<command> [options]')
|
|
31
35
|
.option('-r, --registryUrl <url>', 'Use the registry API at this base URL.')
|
|
32
36
|
.option('-p, --pat <token>', 'Personal access token.')
|
|
@@ -36,17 +40,17 @@ module.exports = function (argv: string[]): void {
|
|
|
36
40
|
const createNamespaceCmd = program.command('create-namespace <name>');
|
|
37
41
|
createNamespaceCmd.description('Create a new namespace')
|
|
38
42
|
.action((name: string) => {
|
|
39
|
-
const { registryUrl, pat } = program.opts();
|
|
43
|
+
const { registryUrl, pat, debug } = program.opts();
|
|
40
44
|
createNamespace({ name, registryUrl, pat })
|
|
41
|
-
.catch(handleError(
|
|
45
|
+
.catch(handleError(debug));
|
|
42
46
|
});
|
|
43
47
|
|
|
44
48
|
const verifyTokenCmd = program.command('verify-pat [namespace]');
|
|
45
49
|
verifyTokenCmd.description('Verify that a personal access token can publish to a namespace')
|
|
46
50
|
.action((namespace?: string) => {
|
|
47
|
-
const { registryUrl, pat } = program.opts();
|
|
51
|
+
const { registryUrl, pat, debug } = program.opts();
|
|
48
52
|
verifyPat({ namespace, registryUrl, pat })
|
|
49
|
-
.catch(handleError(
|
|
53
|
+
.catch(handleError(debug));
|
|
50
54
|
});
|
|
51
55
|
|
|
52
56
|
const publishCmd = program.command('publish [extension.vsix]');
|
|
@@ -86,7 +90,7 @@ module.exports = function (argv: string[]): void {
|
|
|
86
90
|
console.warn("Ignoring option '--packageVersion' for prepackaged extension.");
|
|
87
91
|
if (extensionFile !== undefined && allowMissingRepository !== undefined)
|
|
88
92
|
console.warn("Ignoring option '--allow-missing-repository' for prepackaged extension.");
|
|
89
|
-
const { registryUrl, pat } = program.opts();
|
|
93
|
+
const { registryUrl, pat, debug } = program.opts();
|
|
90
94
|
publish({ extensionFile, registryUrl, pat, targets: typeof target === 'string' ? [target] : target, packagePath: typeof packagePath === 'string' ? [packagePath] : packagePath, baseContentUrl, baseImagesUrl, yarn, followSymlinks, preRelease, allowMissingRepository, dependencies, skipDuplicate, packageVersion, trustedPublishing, idToken, oidcAudience })
|
|
91
95
|
.then(results => {
|
|
92
96
|
const reasons = results.filter(result => result.status === 'rejected')
|
|
@@ -95,7 +99,7 @@ module.exports = function (argv: string[]): void {
|
|
|
95
99
|
if (reasons.length > 0) {
|
|
96
100
|
const message = 'See the documentation for more information:\n'
|
|
97
101
|
+ 'https://github.com/eclipse/openvsx/wiki/Publishing-Extensions';
|
|
98
|
-
const errorHandler = handleError(
|
|
102
|
+
const errorHandler = handleError(debug, message, false);
|
|
99
103
|
for (const reason of reasons) {
|
|
100
104
|
errorHandler(reason);
|
|
101
105
|
}
|
|
@@ -111,7 +115,7 @@ module.exports = function (argv: string[]): void {
|
|
|
111
115
|
.option('-t, --target <targets...>', 'Only delete the given target architectures of the given versions.')
|
|
112
116
|
.option('-f, --force', 'Skip the confirmation prompt.')
|
|
113
117
|
.action((extensionId: string | undefined, { versions, target, force }) => {
|
|
114
|
-
const { registryUrl, pat } = program.opts();
|
|
118
|
+
const { registryUrl, pat, debug } = program.opts();
|
|
115
119
|
unpublish({
|
|
116
120
|
extensionId,
|
|
117
121
|
versions: typeof versions === 'string' ? [versions] : versions,
|
|
@@ -119,7 +123,7 @@ module.exports = function (argv: string[]): void {
|
|
|
119
123
|
force,
|
|
120
124
|
registryUrl,
|
|
121
125
|
pat
|
|
122
|
-
}).catch(handleError(
|
|
126
|
+
}).catch(handleError(debug));
|
|
123
127
|
});
|
|
124
128
|
|
|
125
129
|
const searchCmd = program.command('search [text]');
|
|
@@ -136,17 +140,17 @@ module.exports = function (argv: string[]): void {
|
|
|
136
140
|
.option('--sort-order <order>', `Sort order: ${SORT_ORDERS.join(', ')}.`)
|
|
137
141
|
.option('--json', 'Print the raw results as JSON.')
|
|
138
142
|
.action((text: string | undefined, { category, target, size, offset, sortBy, sortOrder, json }) => {
|
|
139
|
-
const { registryUrl } = program.opts();
|
|
143
|
+
const { registryUrl, debug } = program.opts();
|
|
140
144
|
search({ text, category, target, size, offset, sortBy, sortOrder, json, registryUrl })
|
|
141
|
-
.catch(handleError(
|
|
145
|
+
.catch(handleError(debug));
|
|
142
146
|
});
|
|
143
147
|
|
|
144
148
|
const listCmd = program.command('list <namespace>');
|
|
145
149
|
listCmd.description('List the extensions published in a namespace.')
|
|
146
150
|
.option('--json', 'Print the raw namespace metadata as JSON.')
|
|
147
151
|
.action((namespace: string, { json }) => {
|
|
148
|
-
const { registryUrl } = program.opts();
|
|
149
|
-
list({ namespace, json, registryUrl }).catch(handleError(
|
|
152
|
+
const { registryUrl, debug } = program.opts();
|
|
153
|
+
list({ namespace, json, registryUrl }).catch(handleError(debug));
|
|
150
154
|
});
|
|
151
155
|
|
|
152
156
|
const showCmd = program.command('show <namespace.extension[@version]>');
|
|
@@ -155,8 +159,8 @@ module.exports = function (argv: string[]): void {
|
|
|
155
159
|
.option('--all-versions', 'List every published version instead of the most recent few.')
|
|
156
160
|
.option('--json', 'Print the raw metadata as JSON.')
|
|
157
161
|
.action((extensionId: string, { target, allVersions, json }) => {
|
|
158
|
-
const { registryUrl } = program.opts();
|
|
159
|
-
show({ extensionId, target, allVersions, json, registryUrl }).catch(handleError(
|
|
162
|
+
const { registryUrl, debug } = program.opts();
|
|
163
|
+
show({ extensionId, target, allVersions, json, registryUrl }).catch(handleError(debug));
|
|
160
164
|
});
|
|
161
165
|
|
|
162
166
|
const getCmd = program.command('get <namespace.extension>');
|
|
@@ -166,18 +170,18 @@ module.exports = function (argv: string[]): void {
|
|
|
166
170
|
.option('-o, --output <path>', 'Save the output in the specified file or directory.')
|
|
167
171
|
.option('--metadata', 'Print the extension\'s metadata instead of downloading it.')
|
|
168
172
|
.action((extensionId: string, { target, versionRange, output, metadata }) => {
|
|
169
|
-
const { registryUrl } = program.opts();
|
|
173
|
+
const { registryUrl, debug } = program.opts();
|
|
170
174
|
getExtension({ extensionId, target: target, version: versionRange, registryUrl, output, metadata })
|
|
171
|
-
.catch(handleError(
|
|
175
|
+
.catch(handleError(debug));
|
|
172
176
|
});
|
|
173
177
|
|
|
174
178
|
const verifyCmd = program.command('verify <extension.vsix>');
|
|
175
179
|
verifyCmd.description('Verify a downloaded package\'s signature against the registry\'s public key.')
|
|
176
180
|
.option('-t, --target <target>', 'Target architecture')
|
|
177
181
|
.action((packagePath: string, { target }) => {
|
|
178
|
-
const { registryUrl } = program.opts();
|
|
182
|
+
const { registryUrl, debug } = program.opts();
|
|
179
183
|
verify({ packagePath, target, registryUrl })
|
|
180
|
-
.catch(handleError(
|
|
184
|
+
.catch(handleError(debug));
|
|
181
185
|
});
|
|
182
186
|
|
|
183
187
|
const verifySignatureCmd = program.command('verify-signature');
|
|
@@ -187,43 +191,44 @@ module.exports = function (argv: string[]): void {
|
|
|
187
191
|
.requiredOption('-s, --signaturePath <path>', 'Path to the signature file.')
|
|
188
192
|
.requiredOption('-k, --publicKeyPath <path>', 'Path to the registry\'s public key file.')
|
|
189
193
|
.action(({ packagePath, manifestPath, signaturePath, publicKeyPath }) => {
|
|
194
|
+
const { debug } = program.opts();
|
|
190
195
|
verifySignature({ packagePath, manifestPath, signaturePath, publicKeyPath })
|
|
191
|
-
.catch(handleError(
|
|
196
|
+
.catch(handleError(debug));
|
|
192
197
|
});
|
|
193
198
|
|
|
194
199
|
const loginCmd = program.command('login <namespace>');
|
|
195
200
|
loginCmd.description('Adds a namespace to the list of known namespaces')
|
|
196
201
|
.action((namespace: string) => {
|
|
197
|
-
const { registryUrl, pat } = program.opts();
|
|
198
|
-
login({ namespace, registryUrl, pat }).catch(handleError(
|
|
202
|
+
const { registryUrl, pat, debug } = program.opts();
|
|
203
|
+
login({ namespace, registryUrl, pat }).catch(handleError(debug));
|
|
199
204
|
});
|
|
200
205
|
|
|
201
206
|
const logoutCmd = program.command('logout <namespace>');
|
|
202
207
|
logoutCmd.description('Removes a namespace from the list of known namespaces')
|
|
203
208
|
.action((namespace: string) => {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
program
|
|
208
|
-
.command('*', '', { noHelp: true })
|
|
209
|
-
.action((cmd: commander.Command) => {
|
|
210
|
-
const availableCommands = program.commands.map((c: any) => c._name) as string[];
|
|
211
|
-
const actualCommand = cmd.args[0];
|
|
212
|
-
if (actualCommand) {
|
|
213
|
-
const suggestion = availableCommands.find(c => leven(c, actualCommand) < c.length * 0.4);
|
|
214
|
-
if (suggestion)
|
|
215
|
-
console.error(`Unknown command '${actualCommand}', did you mean '${suggestion}'?\n`);
|
|
216
|
-
else
|
|
217
|
-
console.error(`Unknown command '${actualCommand}'.\n`);
|
|
218
|
-
} else {
|
|
219
|
-
console.error('Unknown command.');
|
|
220
|
-
}
|
|
221
|
-
program.help();
|
|
209
|
+
const { debug } = program.opts();
|
|
210
|
+
logout(namespace).catch(handleError(debug));
|
|
222
211
|
});
|
|
223
212
|
|
|
224
|
-
|
|
213
|
+
// 'command:*' is deprecated in favor of catching the commander.unknownCommand error, but that
|
|
214
|
+
// error is reported (message written, process exited) by commander itself before a catch runs,
|
|
215
|
+
// so it can't reproduce this suggestion wording. Still supported for backwards compatibility.
|
|
216
|
+
program.on('command:*', (operands: string[]) => {
|
|
217
|
+
const availableCommands = program.commands.map(c => c.name());
|
|
218
|
+
const actualCommand = operands[0];
|
|
219
|
+
const suggestion = availableCommands.find(c => levenshtein(c, actualCommand) < c.length * 0.4);
|
|
220
|
+
if (suggestion)
|
|
221
|
+
console.error(`Unknown command '${actualCommand}', did you mean '${suggestion}'?\n`);
|
|
222
|
+
else
|
|
223
|
+
console.error(`Unknown command '${actualCommand}'.\n`);
|
|
224
|
+
process.exitCode = 1;
|
|
225
|
+
program.help();
|
|
226
|
+
});
|
|
225
227
|
|
|
226
228
|
if (process.argv.length <= 2) {
|
|
229
|
+
// Commander 14 would otherwise print this to stderr and exit 1 during program.parse(argv) below.
|
|
227
230
|
program.help();
|
|
228
231
|
}
|
|
232
|
+
|
|
233
|
+
program.parse(argv);
|
|
229
234
|
};
|
package/src/oidc.ts
CHANGED
|
@@ -11,10 +11,9 @@
|
|
|
11
11
|
* SPDX-License-Identifier: EPL-2.0
|
|
12
12
|
*****************************************************************************/
|
|
13
13
|
|
|
14
|
-
import
|
|
15
|
-
import * as followRedirects from 'follow-redirects';
|
|
14
|
+
import { getJson } from './http';
|
|
16
15
|
import { TrustedPublishingOptions } from './trusted-publishing-options';
|
|
17
|
-
import { configuredTimeout
|
|
16
|
+
import { configuredTimeout } from './util';
|
|
18
17
|
|
|
19
18
|
/**
|
|
20
19
|
* Whether an OIDC ID token can be obtained without user interaction.
|
|
@@ -48,59 +47,18 @@ async function getGitHubActionsIdToken(audience: string): Promise<string> {
|
|
|
48
47
|
// the request URL already carries an api-version query parameter, so keep its query intact
|
|
49
48
|
const url = new URL(process.env.ACTIONS_ID_TOKEN_REQUEST_URL!);
|
|
50
49
|
url.searchParams.set('audience', audience);
|
|
50
|
+
// Sent with only these headers, so none of the registry's credentials reach the CI system's
|
|
51
|
+
// token service. OVSX_TIMEOUT still applies, as there are no registry options to read it from.
|
|
51
52
|
const response = await getJson<GitHubIdTokenResponse>(url, {
|
|
52
53
|
'Authorization': `Bearer ${process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN}`,
|
|
53
54
|
'Accept': 'application/json'
|
|
54
|
-
});
|
|
55
|
+
}, configuredTimeout());
|
|
55
56
|
if (!response.value) {
|
|
56
57
|
throw new Error('GitHub Actions did not return an OIDC ID token.');
|
|
57
58
|
}
|
|
58
59
|
return response.value;
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
/**
|
|
62
|
-
* Minimal JSON GET that is not bound to the registry: the request must not carry any registry
|
|
63
|
-
* credentials, as it is sent to the CI system's token service.
|
|
64
|
-
*/
|
|
65
|
-
function getJson<T>(url: URL, headers: http.OutgoingHttpHeaders): Promise<T> {
|
|
66
|
-
return new Promise((resolve, reject) => {
|
|
67
|
-
const protocol = url.protocol === 'https:' ? followRedirects.https : followRedirects.http;
|
|
68
|
-
// OVSX_TIMEOUT covers this request too. Read here rather than taken from the registry's
|
|
69
|
-
// options, since this one carries none of the registry's configuration - only the clock is
|
|
70
|
-
// shared, and having two of those to explain would be worse.
|
|
71
|
-
const timeout = configuredTimeout();
|
|
72
|
-
const request = protocol.request(url, { method: 'GET', headers, timeout }, response => {
|
|
73
|
-
response.setEncoding('utf-8');
|
|
74
|
-
let json = '';
|
|
75
|
-
// See Registry.getJsonResponse: a connection lost mid-body never fires 'end', so the
|
|
76
|
-
// response's own error has to be what settles the promise.
|
|
77
|
-
response.on('error', reject);
|
|
78
|
-
response.on('data', chunk => json += chunk);
|
|
79
|
-
response.on('end', () => {
|
|
80
|
-
if (response.statusCode !== undefined && (response.statusCode < 200 || response.statusCode > 299)) {
|
|
81
|
-
reject(statusError(response));
|
|
82
|
-
} else {
|
|
83
|
-
try {
|
|
84
|
-
resolve(JSON.parse(json));
|
|
85
|
-
} catch (err) {
|
|
86
|
-
reject(err);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
});
|
|
90
|
-
});
|
|
91
|
-
request.on('error', reject);
|
|
92
|
-
// The timeout option only raises an event, so the request has to be destroyed for it to mean
|
|
93
|
-
// anything; the error then arrives at the handler above.
|
|
94
|
-
request.on('timeout', () => {
|
|
95
|
-
// Rejected before the teardown, so the timeout is what the caller sees rather than
|
|
96
|
-
// whichever error destroying the request happens to raise first.
|
|
97
|
-
reject(new Error(`No response from ${redactUrl(url)} for ${timeout} ms.`));
|
|
98
|
-
request.destroy();
|
|
99
|
-
});
|
|
100
|
-
request.end();
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
|
|
104
62
|
interface GitHubIdTokenResponse {
|
|
105
63
|
count?: number;
|
|
106
64
|
value?: string;
|
package/src/publish.ts
CHANGED
|
@@ -8,15 +8,26 @@
|
|
|
8
8
|
* SPDX-License-Identifier: EPL-2.0
|
|
9
9
|
********************************************************************************/
|
|
10
10
|
import * as fs from 'fs';
|
|
11
|
+
import * as semver from 'semver';
|
|
11
12
|
import { createVSIX, IPackageOptions } from '@vscode/vsce';
|
|
12
13
|
import { getPAT } from './pat';
|
|
13
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
getTempFilePath,
|
|
16
|
+
addEnvOptions,
|
|
17
|
+
addTrustedPublishingEnvOptions,
|
|
18
|
+
formatBytes,
|
|
19
|
+
Manifest,
|
|
20
|
+
StatusError
|
|
21
|
+
} from './util';
|
|
14
22
|
import { Extension, Registry } from './registry';
|
|
15
23
|
import { checkLicense } from './check-license';
|
|
16
24
|
import { readVSIXPackage } from './zip';
|
|
17
25
|
import { PublishOptions, PublishCommonOptions } from './publish-options';
|
|
18
26
|
import { getTrustedPublishingToken, refreshTrustedPublishingToken, useTrustedPublishing } from './trusted-publishing';
|
|
19
27
|
|
|
28
|
+
/** Registries older than this cannot report the limit that applies to a namespace. */
|
|
29
|
+
const MIN_SIZE_LIMIT_REGISTRY_VERSION = '1.3.0';
|
|
30
|
+
|
|
20
31
|
/**
|
|
21
32
|
* Publishes an extension.
|
|
22
33
|
*/
|
|
@@ -24,8 +35,11 @@ export async function publish(options: PublishOptions = {}): Promise<PromiseSett
|
|
|
24
35
|
addEnvOptions(options);
|
|
25
36
|
addTrustedPublishingEnvOptions(options);
|
|
26
37
|
|
|
27
|
-
//
|
|
28
|
-
|
|
38
|
+
// Shared by every target/package below, rather than one per artifact: besides saving the extra
|
|
39
|
+
// `/api/version` round trips, it's also where the registry's reported version is cached (see
|
|
40
|
+
// Registry.tokenQuery), so a wide fan-out doesn't repeat that lookup once per target either.
|
|
41
|
+
const registry = new Registry(options);
|
|
42
|
+
const maxExtensionSize = await getMaxExtensionSize(registry);
|
|
29
43
|
|
|
30
44
|
const internalPublishOptions: InternalPublishOptions[] = [];
|
|
31
45
|
const packagePaths = options.packagePath || [undefined];
|
|
@@ -36,15 +50,15 @@ export async function publish(options: PublishOptions = {}): Promise<PromiseSett
|
|
|
36
50
|
}
|
|
37
51
|
}
|
|
38
52
|
|
|
39
|
-
return Promise.allSettled(internalPublishOptions.map(publishOptions => doPublish(publishOptions)));
|
|
53
|
+
return Promise.allSettled(internalPublishOptions.map(publishOptions => doPublish(registry, publishOptions)));
|
|
40
54
|
}
|
|
41
55
|
|
|
42
56
|
/**
|
|
43
|
-
* Looks up the registry's
|
|
44
|
-
*
|
|
57
|
+
* Looks up the registry's default extension size limit, so an oversized package can be flagged
|
|
58
|
+
* before the upload instead of after transferring the whole payload.
|
|
45
59
|
*
|
|
46
60
|
* Best-effort: registries that don't expose `/api/version`, or don't report a limit, return
|
|
47
|
-
* `undefined` here, and
|
|
61
|
+
* `undefined` here, and nothing is said about the size.
|
|
48
62
|
*/
|
|
49
63
|
async function getMaxExtensionSize(registry: Registry): Promise<number | undefined> {
|
|
50
64
|
try {
|
|
@@ -54,28 +68,43 @@ async function getMaxExtensionSize(registry: Registry): Promise<number | undefin
|
|
|
54
68
|
}
|
|
55
69
|
}
|
|
56
70
|
|
|
57
|
-
async function doPublish(options: InternalPublishOptions = {}): Promise<void> {
|
|
71
|
+
async function doPublish(registry: Registry, options: InternalPublishOptions = {}): Promise<void> {
|
|
58
72
|
// if the packagePath is a link to a vsix, don't need to package it
|
|
59
73
|
if (options.packagePath?.endsWith('.vsix')) {
|
|
60
74
|
options.extensionFile = options.packagePath;
|
|
61
75
|
delete options.packagePath;
|
|
62
76
|
delete options.target;
|
|
63
77
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
78
|
+
// Only a package created here is a temp file; one the user supplied must never be deleted.
|
|
79
|
+
const packaged = !options.extensionFile;
|
|
80
|
+
try {
|
|
81
|
+
if (packaged) {
|
|
82
|
+
await packageExtension(options, registry);
|
|
83
|
+
console.log(); // new line
|
|
84
|
+
} else if (options.preRelease) {
|
|
85
|
+
console.warn("Ignoring option '--pre-release' for prepackaged extension.");
|
|
86
|
+
}
|
|
87
|
+
await publishExtensionFile(registry, options);
|
|
88
|
+
} finally {
|
|
89
|
+
if (packaged && options.extensionFile) {
|
|
90
|
+
// Caught rather than awaited plainly: a failure here (e.g. the temp dir is locked or
|
|
91
|
+
// read-only) must not replace whatever error publishExtensionFile already threw above.
|
|
92
|
+
await fs.promises.rm(options.extensionFile, { force: true }).catch(err => {
|
|
93
|
+
console.warn(`Could not delete temporary file ${options.extensionFile}: ${err.message}`);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
70
96
|
}
|
|
97
|
+
}
|
|
71
98
|
|
|
72
|
-
|
|
99
|
+
async function publishExtensionFile(registry: Registry, options: InternalPublishOptions): Promise<void> {
|
|
100
|
+
// Read up front rather than only when a token has to be obtained: the size limit is looked up per
|
|
101
|
+
// namespace, and the namespace lives in the manifest.
|
|
102
|
+
const manifest = await readVSIXPackage(options.extensionFile!);
|
|
73
103
|
|
|
74
104
|
// Set only when this publish obtained the token itself through trusted publishing, which is the one
|
|
75
105
|
// case where a refusal can be answered by asking for a new token.
|
|
76
106
|
let exchanged: { namespace: string; extension: string } | undefined;
|
|
77
107
|
if (!options.pat) {
|
|
78
|
-
const manifest = await readVSIXPackage(options.extensionFile!);
|
|
79
108
|
if (useTrustedPublishing(options)) {
|
|
80
109
|
exchanged = { namespace: manifest.publisher, extension: manifest.name };
|
|
81
110
|
options.pat = await getTrustedPublishingToken(registry, manifest.publisher, manifest.name, options);
|
|
@@ -84,9 +113,13 @@ async function doPublish(options: InternalPublishOptions = {}): Promise<void> {
|
|
|
84
113
|
}
|
|
85
114
|
}
|
|
86
115
|
|
|
116
|
+
// After the token is resolved, because the lookup is authenticated - and by here a token always
|
|
117
|
+
// exists, whether supplied, fetched, or exchanged through trusted publishing.
|
|
118
|
+
const sizeLimit = await ensureWithinSizeLimit(registry, options, manifest);
|
|
119
|
+
|
|
87
120
|
let extension: Extension | undefined;
|
|
88
121
|
try {
|
|
89
|
-
extension = await doRegistryPublish(registry, options, exchanged);
|
|
122
|
+
extension = await doRegistryPublish(registry, options, exchanged, sizeLimit);
|
|
90
123
|
} catch (err) {
|
|
91
124
|
if (options.skipDuplicate && err.message.endsWith('is already published.')) {
|
|
92
125
|
console.log(err.message + ' Skipping publish.');
|
|
@@ -123,10 +156,11 @@ async function doPublish(options: InternalPublishOptions = {}): Promise<void> {
|
|
|
123
156
|
async function doRegistryPublish(
|
|
124
157
|
registry: Registry,
|
|
125
158
|
options: InternalPublishOptions,
|
|
126
|
-
exchanged: { namespace: string; extension: string } | undefined
|
|
159
|
+
exchanged: { namespace: string; extension: string } | undefined,
|
|
160
|
+
sizeLimit?: number
|
|
127
161
|
): Promise<Extension> {
|
|
128
162
|
try {
|
|
129
|
-
return await registry.publish(options.extensionFile!, options.pat
|
|
163
|
+
return await registry.publish(options.extensionFile!, options.pat!, sizeLimit);
|
|
130
164
|
} catch (err) {
|
|
131
165
|
if (!exchanged || (err as StatusError)?.status !== 401) {
|
|
132
166
|
throw err;
|
|
@@ -140,28 +174,95 @@ async function doRegistryPublish(
|
|
|
140
174
|
options,
|
|
141
175
|
options.pat!
|
|
142
176
|
);
|
|
143
|
-
return registry.publish(options.extensionFile!, options.pat);
|
|
177
|
+
return registry.publish(options.extensionFile!, options.pat, sizeLimit);
|
|
144
178
|
}
|
|
145
179
|
}
|
|
146
180
|
|
|
147
181
|
/**
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
182
|
+
* Refuses a package the registry would reject anyway, before uploading it, and reports how many
|
|
183
|
+
* bytes it has concluded may be sent - the caller raises the transport cap to that, so nothing we
|
|
184
|
+
* decided to upload is then refused locally.
|
|
185
|
+
*
|
|
186
|
+
* The limit is the one that applies to this package's namespace and extension, which a size override
|
|
187
|
+
* can raise above the registry default. Registries older than
|
|
188
|
+
* {@link MIN_SIZE_LIMIT_REGISTRY_VERSION} cannot report it, so there the check stays advisory: it
|
|
189
|
+
* warns against the default and publishes, rather than refusing an upload those registries would
|
|
190
|
+
* have accepted.
|
|
152
191
|
*/
|
|
153
|
-
async function ensureWithinSizeLimit(
|
|
154
|
-
|
|
155
|
-
|
|
192
|
+
async function ensureWithinSizeLimit(
|
|
193
|
+
registry: Registry,
|
|
194
|
+
options: InternalPublishOptions,
|
|
195
|
+
manifest: Manifest
|
|
196
|
+
): Promise<number | undefined> {
|
|
197
|
+
const { size } = await fs.promises.stat(options.extensionFile!);
|
|
198
|
+
const limit = await resolveSizeLimit(registry, options, manifest);
|
|
199
|
+
|
|
200
|
+
if (limit.kind === 'failed') {
|
|
201
|
+
// Always said, however small the package: a size override can lower a limit as well as raise
|
|
202
|
+
// it, so there is no size from which it follows that this upload is fine. Staying quiet here
|
|
203
|
+
// left a registry whose /api/-/* routes never arrive indistinguishable from a working one.
|
|
204
|
+
console.warn(
|
|
205
|
+
`Could not check the size limit for ${manifest.publisher}.${manifest.name} at ${registry.url} `
|
|
206
|
+
+ `(${limit.reason}). Publishing anyway: the registry enforces its own limit, so an oversized `
|
|
207
|
+
+ `package is refused after the upload rather than before it.`
|
|
208
|
+
);
|
|
209
|
+
} else if (limit.kind === 'unsupported') {
|
|
210
|
+
const fallback = options.maxExtensionSize;
|
|
211
|
+
if (fallback && size > fallback) {
|
|
212
|
+
console.warn(
|
|
213
|
+
`The extension package (${formatBytes(size)}) exceeds the default size limit of ${formatBytes(fallback)} `
|
|
214
|
+
+ `reported by the registry at ${registry.url}, which is too old to report the limit for a `
|
|
215
|
+
+ `single namespace. Publishing anyway: the namespace may have a different limit configured, `
|
|
216
|
+
+ `and the registry decides.`
|
|
217
|
+
);
|
|
218
|
+
}
|
|
156
219
|
}
|
|
157
220
|
|
|
158
|
-
|
|
159
|
-
|
|
221
|
+
if (limit.kind !== 'resolved') {
|
|
222
|
+
// The allowance is this package, not the built-in publish size: having decided to send it and
|
|
223
|
+
// let the registry answer, the transport cap must not be what refuses it instead. An older
|
|
224
|
+
// registry whose default is above 512 MiB would otherwise reject a package it would accept.
|
|
225
|
+
return size;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (size > limit.limit) {
|
|
160
229
|
throw new Error(
|
|
161
|
-
`The extension package (${formatBytes(size)}) exceeds the size limit of ${formatBytes(
|
|
162
|
-
+ `
|
|
230
|
+
`The extension package (${formatBytes(size)}) exceeds the size limit of ${formatBytes(limit.limit)} `
|
|
231
|
+
+ `for ${manifest.publisher}.${manifest.name} at ${registry.url}.`
|
|
163
232
|
);
|
|
164
233
|
}
|
|
234
|
+
return limit.limit;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* The limit for this namespace/extension. A registry too old to report one and a registry that could
|
|
239
|
+
* not answer are told apart: both publish anyway, but only the second one means a check the user is
|
|
240
|
+
* entitled to expect did not happen.
|
|
241
|
+
*/
|
|
242
|
+
type SizeLimitLookup =
|
|
243
|
+
| { kind: 'resolved'; limit: number }
|
|
244
|
+
| { kind: 'unsupported' }
|
|
245
|
+
| { kind: 'failed'; reason: string };
|
|
246
|
+
|
|
247
|
+
async function resolveSizeLimit(
|
|
248
|
+
registry: Registry,
|
|
249
|
+
options: InternalPublishOptions,
|
|
250
|
+
manifest: Manifest
|
|
251
|
+
): Promise<SizeLimitLookup> {
|
|
252
|
+
const reported = await registry.getRegistryVersion().catch(() => undefined);
|
|
253
|
+
const version = reported?.version ? semver.coerce(reported.version) : undefined;
|
|
254
|
+
if (!version || semver.lt(version, MIN_SIZE_LIMIT_REGISTRY_VERSION)) {
|
|
255
|
+
return { kind: 'unsupported' };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
const { maxSize } = await registry.getSizeLimit(manifest.publisher, manifest.name, options.pat!);
|
|
260
|
+
return { kind: 'resolved', limit: maxSize };
|
|
261
|
+
} catch (err) {
|
|
262
|
+
// The registry claims to be new enough but could not answer. It enforces the limit itself
|
|
263
|
+
// regardless, so publish rather than refusing on a failed lookup.
|
|
264
|
+
return { kind: 'failed', reason: err instanceof Error ? err.message : String(err) };
|
|
265
|
+
}
|
|
165
266
|
}
|
|
166
267
|
|
|
167
268
|
async function packageExtension(options: InternalPublishOptions, registry: Registry): Promise<void> {
|
|
@@ -169,7 +270,7 @@ async function packageExtension(options: InternalPublishOptions, registry: Regis
|
|
|
169
270
|
await checkLicense(options.packagePath!);
|
|
170
271
|
}
|
|
171
272
|
|
|
172
|
-
options.extensionFile =
|
|
273
|
+
options.extensionFile = getTempFilePath('.vsix');
|
|
173
274
|
const packageOptions: IPackageOptions = {
|
|
174
275
|
packagePath: options.extensionFile,
|
|
175
276
|
target: options.target,
|
|
@@ -208,8 +309,9 @@ interface InternalPublishOptions extends PublishCommonOptions {
|
|
|
208
309
|
dependencies?: boolean;
|
|
209
310
|
|
|
210
311
|
/**
|
|
211
|
-
* The registry's
|
|
212
|
-
* shared across every target/package being published.
|
|
312
|
+
* The registry's default extension size limit in bytes, looked up once via `/api/version` and
|
|
313
|
+
* shared across every target/package being published. Advisory: a namespace override can allow
|
|
314
|
+
* more. `undefined` when it couldn't be determined.
|
|
213
315
|
*/
|
|
214
316
|
maxExtensionSize?: number;
|
|
215
317
|
}
|