ovsx 1.2.0 → 1.4.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.
Files changed (76) hide show
  1. package/README.md +7 -1
  2. package/lib/check-license.d.ts.map +1 -1
  3. package/lib/check-license.js +5 -4
  4. package/lib/check-license.js.map +1 -1
  5. package/lib/create-namespace.d.ts.map +1 -1
  6. package/lib/create-namespace.js +3 -1
  7. package/lib/create-namespace.js.map +1 -1
  8. package/lib/get.js +5 -5
  9. package/lib/get.js.map +1 -1
  10. package/lib/http.d.ts +32 -0
  11. package/lib/http.d.ts.map +1 -0
  12. package/lib/http.js +170 -0
  13. package/lib/http.js.map +1 -0
  14. package/lib/index.d.ts +0 -3
  15. package/lib/index.d.ts.map +1 -1
  16. package/lib/index.js +0 -3
  17. package/lib/index.js.map +1 -1
  18. package/lib/main.js +50 -61
  19. package/lib/main.js.map +1 -1
  20. package/lib/oidc.d.ts.map +1 -1
  21. package/lib/oidc.js +5 -47
  22. package/lib/oidc.js.map +1 -1
  23. package/lib/pat.d.ts +6 -1
  24. package/lib/pat.d.ts.map +1 -1
  25. package/lib/pat.js +16 -7
  26. package/lib/pat.js.map +1 -1
  27. package/lib/publish.d.ts +6 -0
  28. package/lib/publish.d.ts.map +1 -1
  29. package/lib/publish.js +147 -40
  30. package/lib/publish.js.map +1 -1
  31. package/lib/registry.d.ts +50 -12
  32. package/lib/registry.d.ts.map +1 -1
  33. package/lib/registry.js +131 -126
  34. package/lib/registry.js.map +1 -1
  35. package/lib/trusted-publishing-options.d.ts +5 -0
  36. package/lib/trusted-publishing-options.d.ts.map +1 -1
  37. package/lib/trusted-publishing.js +28 -4
  38. package/lib/trusted-publishing.js.map +1 -1
  39. package/lib/unpublish.d.ts.map +1 -1
  40. package/lib/unpublish.js +5 -8
  41. package/lib/unpublish.js.map +1 -1
  42. package/lib/util.d.ts +19 -15
  43. package/lib/util.d.ts.map +1 -1
  44. package/lib/util.js +51 -71
  45. package/lib/util.js.map +1 -1
  46. package/lib/verify-pat.d.ts.map +1 -1
  47. package/lib/verify-pat.js +3 -1
  48. package/lib/verify-pat.js.map +1 -1
  49. package/lib/verify.js +2 -2
  50. package/lib/verify.js.map +1 -1
  51. package/lib/zip.d.ts.map +1 -1
  52. package/lib/zip.js +2 -10
  53. package/lib/zip.js.map +1 -1
  54. package/package.json +19 -14
  55. package/src/check-license.ts +5 -4
  56. package/src/create-namespace.ts +3 -1
  57. package/src/get.ts +6 -6
  58. package/src/http.ts +176 -0
  59. package/src/index.ts +0 -3
  60. package/src/main.ts +54 -63
  61. package/src/oidc.ts +5 -47
  62. package/src/pat.ts +22 -9
  63. package/src/publish.ts +187 -44
  64. package/src/registry.ts +153 -126
  65. package/src/trusted-publishing-options.ts +5 -0
  66. package/src/trusted-publishing.ts +33 -5
  67. package/src/unpublish.ts +4 -7
  68. package/src/util.ts +47 -73
  69. package/src/verify-pat.ts +3 -1
  70. package/src/verify.ts +3 -3
  71. package/src/zip.ts +2 -12
  72. package/lib/version.d.ts +0 -2
  73. package/lib/version.d.ts.map +0 -1
  74. package/lib/version.js +0 -5
  75. package/lib/version.js.map +0 -1
  76. package/src/version.ts +0 -1
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 { createTempFile, addEnvOptions, addTrustedPublishingEnvOptions, formatBytes, StatusError } from './util';
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,27 +35,74 @@ export async function publish(options: PublishOptions = {}): Promise<PromiseSett
24
35
  addEnvOptions(options);
25
36
  addTrustedPublishingEnvOptions(options);
26
37
 
27
- // Looked up once and shared by every target/package below, rather than once per artifact.
28
- const maxExtensionSize = await getMaxExtensionSize(new Registry(options));
29
-
30
38
  const internalPublishOptions: InternalPublishOptions[] = [];
39
+ const prepackagedFiles: string[] = [];
31
40
  const packagePaths = options.packagePath || [undefined];
32
41
  const targets = options.targets || [undefined];
33
42
  for (const packagePath of packagePaths) {
34
- for (const target of targets) {
35
- internalPublishOptions.push({ ...options, packagePath: packagePath, target: target, maxExtensionSize });
43
+ const extensionFile = options.extensionFile ?? (packagePath?.endsWith('.vsix') ? packagePath : undefined);
44
+ if (extensionFile !== undefined) {
45
+ prepackagedFiles.push(extensionFile);
46
+ // A package is already built for its target, so it is published once, not once per target.
47
+ internalPublishOptions.push({ ...options, extensionFile, packagePath: undefined, target: undefined });
48
+ } else {
49
+ for (const target of targets) {
50
+ internalPublishOptions.push({ ...options, packagePath, target });
51
+ }
36
52
  }
37
53
  }
54
+ // Before the registry is contacted, so a slow or failing lookup cannot keep these from being seen.
55
+ warnIgnoredPackagingOptions(options, prepackagedFiles);
38
56
 
39
- return Promise.allSettled(internalPublishOptions.map(publishOptions => doPublish(publishOptions)));
57
+ // Shared by every target/package below, rather than one per artifact: besides saving the extra
58
+ // `/api/version` round trips, it's also where the registry's reported version is cached (see
59
+ // Registry.tokenQuery), so a wide fan-out doesn't repeat that lookup once per target either.
60
+ const registry = new Registry(options);
61
+ const maxExtensionSize = await getMaxExtensionSize(registry);
62
+
63
+ return Promise.allSettled(internalPublishOptions.map(publishOptions =>
64
+ doPublish(registry, { ...publishOptions, maxExtensionSize })));
65
+ }
66
+
67
+ /**
68
+ * Names the files in the warning: next to a directory being packaged, the options still apply to that.
69
+ */
70
+ function warnIgnoredPackagingOptions(options: PublishOptions, prepackagedFiles: string[]): void {
71
+ const files = [...new Set(prepackagedFiles)];
72
+ if (files.length === 0) {
73
+ return;
74
+ }
75
+ const subject = `${files.length === 1 ? 'extension' : 'extensions'} ${files.map(file => `'${file}'`).join(', ')}`;
76
+ for (const [flag, key] of PACKAGING_OPTIONS) {
77
+ // An explicit `false` is not a flag that was given.
78
+ if (options[key] !== undefined && options[key] !== false) {
79
+ console.warn(`Ignoring option '${flag}' for prepackaged ${subject}.`);
80
+ }
81
+ }
40
82
  }
41
83
 
42
84
  /**
43
- * Looks up the registry's configured extension size limit, so an oversized package can be rejected
44
- * locally before the upload instead of after transferring the whole payload.
85
+ * Options that only affect packaging, with the CLI flag that sets each, in the order they are reported
86
+ * as ignored for a prepackaged extension. `--no-dependencies` is left out: the CLI always sets
87
+ * `dependencies`, so whether it was given cannot be told apart.
88
+ */
89
+ export const PACKAGING_OPTIONS: ReadonlyArray<readonly [string, keyof PublishOptions]> = [
90
+ ['--target', 'targets'],
91
+ ['--baseContentUrl', 'baseContentUrl'],
92
+ ['--baseImagesUrl', 'baseImagesUrl'],
93
+ ['--yarn', 'yarn'],
94
+ ['--follow-symlinks', 'followSymlinks'],
95
+ ['--packageVersion', 'packageVersion'],
96
+ ['--allow-missing-repository', 'allowMissingRepository'],
97
+ ['--pre-release', 'preRelease']
98
+ ];
99
+
100
+ /**
101
+ * Looks up the registry's default extension size limit, so an oversized package can be flagged
102
+ * before the upload instead of after transferring the whole payload.
45
103
  *
46
104
  * Best-effort: registries that don't expose `/api/version`, or don't report a limit, return
47
- * `undefined` here, and the upload proceeds to let the server enforce its own limit as before.
105
+ * `undefined` here, and nothing is said about the size.
48
106
  */
49
107
  async function getMaxExtensionSize(registry: Registry): Promise<number | undefined> {
50
108
  try {
@@ -54,39 +112,53 @@ async function getMaxExtensionSize(registry: Registry): Promise<number | undefin
54
112
  }
55
113
  }
56
114
 
57
- async function doPublish(options: InternalPublishOptions = {}): Promise<void> {
58
- // if the packagePath is a link to a vsix, don't need to package it
59
- if (options.packagePath?.endsWith('.vsix')) {
60
- options.extensionFile = options.packagePath;
61
- delete options.packagePath;
62
- delete options.target;
63
- }
64
- const registry = new Registry(options);
65
- if (!options.extensionFile) {
66
- await packageExtension(options, registry);
67
- console.log(); // new line
68
- } else if (options.preRelease) {
69
- console.warn("Ignoring option '--pre-release' for prepackaged extension.");
115
+ async function doPublish(registry: Registry, options: InternalPublishOptions = {}): Promise<void> {
116
+ // Only a package created here is a temp file; one the user supplied must never be deleted.
117
+ const packaged = !options.extensionFile;
118
+ try {
119
+ if (packaged) {
120
+ await packageExtension(options, registry);
121
+ console.log(); // new line
122
+ }
123
+ await publishExtensionFile(registry, options);
124
+ } finally {
125
+ if (packaged && options.extensionFile) {
126
+ // Caught rather than awaited plainly: a failure here (e.g. the temp dir is locked or
127
+ // read-only) must not replace whatever error publishExtensionFile already threw above.
128
+ await fs.promises.rm(options.extensionFile, { force: true }).catch(err => {
129
+ console.warn(`Could not delete temporary file ${options.extensionFile}: ${err.message}`);
130
+ });
131
+ }
70
132
  }
133
+ }
71
134
 
72
- await ensureWithinSizeLimit(options.extensionFile!, options.maxExtensionSize, registry.url);
135
+ async function publishExtensionFile(registry: Registry, options: InternalPublishOptions): Promise<void> {
136
+ // Read up front rather than only when a token has to be obtained: the size limit is looked up per
137
+ // namespace, and the namespace lives in the manifest.
138
+ const manifest = await readVSIXPackage(options.extensionFile!);
73
139
 
74
140
  // Set only when this publish obtained the token itself through trusted publishing, which is the one
75
141
  // case where a refusal can be answered by asking for a new token.
76
142
  let exchanged: { namespace: string; extension: string } | undefined;
77
143
  if (!options.pat) {
78
- const manifest = await readVSIXPackage(options.extensionFile!);
79
144
  if (useTrustedPublishing(options)) {
80
145
  exchanged = { namespace: manifest.publisher, extension: manifest.name };
81
146
  options.pat = await getTrustedPublishingToken(registry, manifest.publisher, manifest.name, options);
82
147
  } else {
83
- options.pat = await getPAT(manifest.publisher, options);
148
+ // getPAT has verified a prompted token already, so it can be stored before publishing
149
+ const { pat, save } = await getPAT(manifest.publisher, options);
150
+ options.pat = pat;
151
+ await save();
84
152
  }
85
153
  }
86
154
 
155
+ // After the token is resolved, because the lookup is authenticated - and by here a token always
156
+ // exists, whether supplied, fetched, or exchanged through trusted publishing.
157
+ const sizeLimit = await ensureWithinSizeLimit(registry, options, manifest);
158
+
87
159
  let extension: Extension | undefined;
88
160
  try {
89
- extension = await doRegistryPublish(registry, options, exchanged);
161
+ extension = await doRegistryPublish(registry, options, exchanged, sizeLimit);
90
162
  } catch (err) {
91
163
  if (options.skipDuplicate && err.message.endsWith('is already published.')) {
92
164
  console.log(err.message + ' Skipping publish.');
@@ -123,10 +195,11 @@ async function doPublish(options: InternalPublishOptions = {}): Promise<void> {
123
195
  async function doRegistryPublish(
124
196
  registry: Registry,
125
197
  options: InternalPublishOptions,
126
- exchanged: { namespace: string; extension: string } | undefined
198
+ exchanged: { namespace: string; extension: string } | undefined,
199
+ sizeLimit?: number
127
200
  ): Promise<Extension> {
128
201
  try {
129
- return await registry.publish(options.extensionFile!, options.pat!);
202
+ return await registry.publish(options.extensionFile!, options.pat!, sizeLimit);
130
203
  } catch (err) {
131
204
  if (!exchanged || (err as StatusError)?.status !== 401) {
132
205
  throw err;
@@ -140,28 +213,97 @@ async function doRegistryPublish(
140
213
  options,
141
214
  options.pat!
142
215
  );
143
- return registry.publish(options.extensionFile!, options.pat);
216
+ return registry.publish(options.extensionFile!, options.pat, sizeLimit);
144
217
  }
145
218
  }
146
219
 
147
220
  /**
148
- * Fails fast with an actionable message when the packaged extension is already known to exceed the
149
- * registry's configured size limit, rather than uploading the whole file only to have the server
150
- * reject it. `maxSize` is `undefined` when the limit couldn't be determined, in which case the check
151
- * is skipped and the upload is left to the server to accept or reject.
221
+ * Refuses a package the registry would reject anyway, before uploading it, and reports how many
222
+ * bytes it has concluded may be sent - the caller raises the transport cap to that, so nothing we
223
+ * decided to upload is then refused locally.
224
+ *
225
+ * The limit is the one that applies to this package's namespace and extension, which a size override
226
+ * can raise above the registry default. Registries older than
227
+ * {@link MIN_SIZE_LIMIT_REGISTRY_VERSION} cannot report it, so there the check stays advisory: it
228
+ * warns against the default and publishes, rather than refusing an upload those registries would
229
+ * have accepted.
152
230
  */
153
- async function ensureWithinSizeLimit(extensionFile: string, maxSize: number | undefined, registryUrl: string): Promise<void> {
154
- if (!maxSize) {
155
- return;
231
+ async function ensureWithinSizeLimit(
232
+ registry: Registry,
233
+ options: InternalPublishOptions,
234
+ manifest: Manifest
235
+ ): Promise<number | undefined> {
236
+ const { size } = await fs.promises.stat(options.extensionFile!);
237
+ const limit = await resolveSizeLimit(registry, options, manifest);
238
+
239
+ if (limit.kind === 'failed') {
240
+ // Always said, however small the package: a size override can lower a limit as well as raise
241
+ // it, so there is no size from which it follows that this upload is fine. Staying quiet here
242
+ // left a registry whose /api/-/* routes never arrive indistinguishable from a working one.
243
+ console.warn(
244
+ `Could not check the size limit for ${manifest.publisher}.${manifest.name} at ${registry.url} `
245
+ + `(${limit.reason}). Publishing anyway: the registry enforces its own limit, so an oversized `
246
+ + `package is refused after the upload rather than before it.`
247
+ );
248
+ } else if (limit.kind === 'unsupported') {
249
+ const fallback = options.maxExtensionSize;
250
+ if (fallback && size > fallback) {
251
+ console.warn(
252
+ `The extension package (${formatBytes(size)}, ${size} bytes) exceeds the default size limit of `
253
+ + `${formatBytes(fallback)} (${fallback} bytes) `
254
+ + `reported by the registry at ${registry.url}, which is too old to report the limit for a `
255
+ + `single namespace. Publishing anyway: the namespace may have a different limit configured, `
256
+ + `and the registry decides.`
257
+ );
258
+ }
156
259
  }
157
260
 
158
- const { size } = await fs.promises.stat(extensionFile);
159
- if (size > maxSize) {
261
+ if (limit.kind !== 'resolved') {
262
+ // The allowance is this package, not the built-in publish size: having decided to send it and
263
+ // let the registry answer, the transport cap must not be what refuses it instead. An older
264
+ // registry whose default is above 512 MiB would otherwise reject a package it would accept.
265
+ return size;
266
+ }
267
+
268
+ if (size > limit.limit) {
160
269
  throw new Error(
161
- `The extension package (${formatBytes(size)}) exceeds the size limit of ${formatBytes(maxSize)} `
162
- + `accepted by the registry at ${registryUrl}.`
270
+ `The extension package (${formatBytes(size)}, ${size} bytes) exceeds the size limit of `
271
+ + `${formatBytes(limit.limit)} (${limit.limit} bytes) `
272
+ + `for ${manifest.publisher}.${manifest.name} at ${registry.url}.`
163
273
  );
164
274
  }
275
+ return limit.limit;
276
+ }
277
+
278
+ /**
279
+ * The limit for this namespace/extension. A registry too old to report one and a registry that could
280
+ * not answer are told apart: both publish anyway, but only the second one means a check the user is
281
+ * entitled to expect did not happen.
282
+ */
283
+ type SizeLimitLookup =
284
+ | { kind: 'resolved'; limit: number }
285
+ | { kind: 'unsupported' }
286
+ | { kind: 'failed'; reason: string };
287
+
288
+ async function resolveSizeLimit(
289
+ registry: Registry,
290
+ options: InternalPublishOptions,
291
+ manifest: Manifest
292
+ ): Promise<SizeLimitLookup> {
293
+ const reported = await registry.getRegistryVersion().catch(() => undefined);
294
+ const version = reported?.version ? semver.coerce(reported.version) : undefined;
295
+ if (!version || semver.lt(version, MIN_SIZE_LIMIT_REGISTRY_VERSION)) {
296
+ return { kind: 'unsupported' };
297
+ }
298
+
299
+ try {
300
+ const { maxSize } = await registry.getSizeLimit(manifest.publisher, manifest.name, options.pat!);
301
+ return { kind: 'resolved', limit: maxSize };
302
+ } catch (err) {
303
+ // The registry claims to be new enough but could not answer. It enforces the limit itself
304
+ // regardless, so publish rather than refusing on a failed lookup.
305
+ return { kind: 'failed', reason: err instanceof Error ? err.message : String(err) };
306
+ }
165
307
  }
166
308
 
167
309
  async function packageExtension(options: InternalPublishOptions, registry: Registry): Promise<void> {
@@ -169,7 +311,7 @@ async function packageExtension(options: InternalPublishOptions, registry: Regis
169
311
  await checkLicense(options.packagePath!);
170
312
  }
171
313
 
172
- options.extensionFile = await createTempFile({ postfix: '.vsix' });
314
+ options.extensionFile = getTempFilePath('.vsix');
173
315
  const packageOptions: IPackageOptions = {
174
316
  packagePath: options.extensionFile,
175
317
  target: options.target,
@@ -208,8 +350,9 @@ interface InternalPublishOptions extends PublishCommonOptions {
208
350
  dependencies?: boolean;
209
351
 
210
352
  /**
211
- * The registry's configured extension size limit in bytes, looked up once via `/api/version` and
212
- * shared across every target/package being published. `undefined` when it couldn't be determined.
353
+ * The registry's default extension size limit in bytes, looked up once via `/api/version` and
354
+ * shared across every target/package being published. Advisory: a namespace override can allow
355
+ * more. `undefined` when it couldn't be determined.
213
356
  */
214
357
  maxExtensionSize?: number;
215
358
  }