@vinktarhq/cli 0.1.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 (121) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/LICENSE +21 -0
  3. package/README.md +378 -0
  4. package/dist/bin.d.ts +2 -0
  5. package/dist/bin.js +28 -0
  6. package/dist/bin.js.map +1 -0
  7. package/dist/bundler/core.d.ts +140 -0
  8. package/dist/bundler/core.js +320 -0
  9. package/dist/bundler/core.js.map +1 -0
  10. package/dist/bundler/esbuild.d.ts +44 -0
  11. package/dist/bundler/esbuild.js +85 -0
  12. package/dist/bundler/esbuild.js.map +1 -0
  13. package/dist/bundler/rollup.d.ts +84 -0
  14. package/dist/bundler/rollup.js +221 -0
  15. package/dist/bundler/rollup.js.map +1 -0
  16. package/dist/bundler/rspack.d.ts +8 -0
  17. package/dist/bundler/rspack.js +8 -0
  18. package/dist/bundler/rspack.js.map +1 -0
  19. package/dist/bundler/webpack.d.ts +83 -0
  20. package/dist/bundler/webpack.js +104 -0
  21. package/dist/bundler/webpack.js.map +1 -0
  22. package/dist/cjs/bundler/core.d.ts +140 -0
  23. package/dist/cjs/bundler/core.js +331 -0
  24. package/dist/cjs/bundler/esbuild.d.ts +44 -0
  25. package/dist/cjs/bundler/esbuild.js +87 -0
  26. package/dist/cjs/bundler/rollup.d.ts +84 -0
  27. package/dist/cjs/bundler/rollup.js +226 -0
  28. package/dist/cjs/bundler/rspack.d.ts +8 -0
  29. package/dist/cjs/bundler/rspack.js +12 -0
  30. package/dist/cjs/bundler/webpack.d.ts +83 -0
  31. package/dist/cjs/bundler/webpack.js +107 -0
  32. package/dist/cjs/cli.d.ts +17 -0
  33. package/dist/cjs/cli.js +358 -0
  34. package/dist/cjs/commands/doctor.d.ts +15 -0
  35. package/dist/cjs/commands/doctor.js +134 -0
  36. package/dist/cjs/commands/inject.d.ts +70 -0
  37. package/dist/cjs/commands/inject.js +123 -0
  38. package/dist/cjs/commands/resolve.d.ts +32 -0
  39. package/dist/cjs/commands/resolve.js +130 -0
  40. package/dist/cjs/commands/upload.d.ts +74 -0
  41. package/dist/cjs/commands/upload.js +254 -0
  42. package/dist/cjs/config.d.ts +51 -0
  43. package/dist/cjs/config.js +152 -0
  44. package/dist/cjs/debug-id.d.ts +176 -0
  45. package/dist/cjs/debug-id.js +323 -0
  46. package/dist/cjs/discover.d.ts +50 -0
  47. package/dist/cjs/discover.js +235 -0
  48. package/dist/cjs/env.d.ts +22 -0
  49. package/dist/cjs/env.js +52 -0
  50. package/dist/cjs/glob.d.ts +34 -0
  51. package/dist/cjs/glob.js +112 -0
  52. package/dist/cjs/http.d.ts +68 -0
  53. package/dist/cjs/http.js +211 -0
  54. package/dist/cjs/index.d.ts +17 -0
  55. package/dist/cjs/index.js +53 -0
  56. package/dist/cjs/limits.d.ts +35 -0
  57. package/dist/cjs/limits.js +38 -0
  58. package/dist/cjs/multipart.d.ts +28 -0
  59. package/dist/cjs/multipart.js +44 -0
  60. package/dist/cjs/package.json +3 -0
  61. package/dist/cjs/upload.d.ts +178 -0
  62. package/dist/cjs/upload.js +394 -0
  63. package/dist/cjs/url.d.ts +25 -0
  64. package/dist/cjs/url.js +43 -0
  65. package/dist/cjs/version.d.ts +10 -0
  66. package/dist/cjs/version.js +13 -0
  67. package/dist/cli.d.ts +17 -0
  68. package/dist/cli.js +354 -0
  69. package/dist/cli.js.map +1 -0
  70. package/dist/commands/doctor.d.ts +15 -0
  71. package/dist/commands/doctor.js +132 -0
  72. package/dist/commands/doctor.js.map +1 -0
  73. package/dist/commands/inject.d.ts +70 -0
  74. package/dist/commands/inject.js +120 -0
  75. package/dist/commands/inject.js.map +1 -0
  76. package/dist/commands/resolve.d.ts +32 -0
  77. package/dist/commands/resolve.js +128 -0
  78. package/dist/commands/resolve.js.map +1 -0
  79. package/dist/commands/upload.d.ts +74 -0
  80. package/dist/commands/upload.js +252 -0
  81. package/dist/commands/upload.js.map +1 -0
  82. package/dist/config.d.ts +51 -0
  83. package/dist/config.js +150 -0
  84. package/dist/config.js.map +1 -0
  85. package/dist/debug-id.d.ts +176 -0
  86. package/dist/debug-id.js +307 -0
  87. package/dist/debug-id.js.map +1 -0
  88. package/dist/discover.d.ts +50 -0
  89. package/dist/discover.js +229 -0
  90. package/dist/discover.js.map +1 -0
  91. package/dist/env.d.ts +22 -0
  92. package/dist/env.js +50 -0
  93. package/dist/env.js.map +1 -0
  94. package/dist/glob.d.ts +34 -0
  95. package/dist/glob.js +109 -0
  96. package/dist/glob.js.map +1 -0
  97. package/dist/http.d.ts +68 -0
  98. package/dist/http.js +203 -0
  99. package/dist/http.js.map +1 -0
  100. package/dist/index.d.ts +17 -0
  101. package/dist/index.js +18 -0
  102. package/dist/index.js.map +1 -0
  103. package/dist/limits.d.ts +35 -0
  104. package/dist/limits.js +36 -0
  105. package/dist/limits.js.map +1 -0
  106. package/dist/multipart.d.ts +28 -0
  107. package/dist/multipart.js +41 -0
  108. package/dist/multipart.js.map +1 -0
  109. package/dist/upload.d.ts +178 -0
  110. package/dist/upload.js +383 -0
  111. package/dist/upload.js.map +1 -0
  112. package/dist/url.d.ts +25 -0
  113. package/dist/url.js +40 -0
  114. package/dist/url.js.map +1 -0
  115. package/dist/version.d.ts +10 -0
  116. package/dist/version.js +11 -0
  117. package/dist/version.js.map +1 -0
  118. package/dist/vite.d.ts +74 -0
  119. package/dist/vite.js +166 -0
  120. package/dist/vite.js.map +1 -0
  121. package/package.json +141 -0
@@ -0,0 +1,394 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_CONCURRENCY = exports.UploadError = void 0;
4
+ exports.index = index;
5
+ exports.materialise = materialise;
6
+ exports.rewrite = rewrite;
7
+ exports.preflight = preflight;
8
+ exports.send = send;
9
+ exports.batch = batch;
10
+ exports.pooled = pooled;
11
+ exports.mb = mb;
12
+ const node_crypto_1 = require("node:crypto");
13
+ const promises_1 = require("node:fs/promises");
14
+ const node_path_1 = require("node:path");
15
+ const node_zlib_1 = require("node:zlib");
16
+ const debug_id_js_1 = require("./debug-id.js");
17
+ const discover_js_1 = require("./discover.js");
18
+ const http_js_1 = require("./http.js");
19
+ const limits_js_1 = require("./limits.js");
20
+ const multipart_js_1 = require("./multipart.js");
21
+ const url_js_1 = require("./url.js");
22
+ class UploadError extends Error {
23
+ status;
24
+ code;
25
+ hint;
26
+ constructor(message, status, code, hint) {
27
+ super(message);
28
+ this.status = status;
29
+ this.code = code;
30
+ this.hint = hint;
31
+ this.name = 'UploadError';
32
+ }
33
+ }
34
+ exports.UploadError = UploadError;
35
+ /**
36
+ * Index one artifact: what it is, what it hashes to, and how big it will be on the wire.
37
+ *
38
+ * The body is built and then DISCARDED. Holding it would mean the whole build sitting in memory
39
+ * before the first request left — a few hundred chunks of a real application is hundreds of
40
+ * megabytes — and it is cheap to rebuild from a local file at the moment it is actually sent.
41
+ *
42
+ * Returns null when there is nothing to upload, rather than throwing: a directory of chunks where
43
+ * only some have maps is normal, not an error.
44
+ */
45
+ async function index(artifact, urlPrefix, options = {}) {
46
+ if (artifact.map === null)
47
+ return null;
48
+ const { body, injected, raw } = await build(artifact, options);
49
+ const bytes = Buffer.byteLength(body);
50
+ return {
51
+ entry: {
52
+ name: (0, node_path_1.basename)(artifact.map),
53
+ url: (0, url_js_1.toUrl)(urlPrefix, artifact.relative),
54
+ debugId: debugIdOf(body),
55
+ bytes,
56
+ sha256: (0, node_crypto_1.createHash)('sha256').update(body).digest('hex'),
57
+ mapSha256: (0, node_crypto_1.createHash)('sha256').update(raw).digest('hex'),
58
+ injected,
59
+ },
60
+ // Reported, not thrown. One 20 MB chunk used to abort the whole upload, so a single
61
+ // pathological bundle cost symbolication for every other file in the build.
62
+ oversized: bytes > limits_js_1.MAX_FILE_BYTES,
63
+ // A map with no mappings and no sources answers no question; Vite emits one per HTML entry.
64
+ empty: (0, discover_js_1.isEmptyMap)(raw),
65
+ };
66
+ }
67
+ /**
68
+ * The exact bytes that go on the wire for this artifact.
69
+ *
70
+ * Deterministic, which is what lets `index` throw the body away and this rebuild it later: the id
71
+ * already injected into the chunk wins, so a second call cannot produce a different map than the
72
+ * one that was hashed.
73
+ */
74
+ async function materialise(artifact, options = {}) {
75
+ return (await build(artifact, options)).body;
76
+ }
77
+ async function build(artifact, options) {
78
+ if (artifact.map === null)
79
+ throw new Error(`${artifact.relative} has no source map`);
80
+ const [code, rawMap] = await Promise.all([
81
+ (0, promises_1.readFile)(artifact.file, 'utf8'),
82
+ (0, promises_1.readFile)(artifact.map, 'utf8'),
83
+ ]);
84
+ // Prefer the id already injected into the chunk: that is the one the SDK will report, and
85
+ // deriving a fresh one here would produce a map nothing ever asks for. The map's own id is the
86
+ // next best thing — a bundler that stamps ids natively writes it there and nowhere else.
87
+ const injected = (0, debug_id_js_1.existingDebugId)(code);
88
+ const debugId = injected ?? (0, debug_id_js_1.mapDebugId)(rawMap) ?? (0, debug_id_js_1.deriveDebugId)(code);
89
+ const prepared = options.rewriteSources === false ? rawMap : rewrite(rawMap, options.root);
90
+ return { body: (0, debug_id_js_1.injectIntoMap)(prepared, debugId), injected: injected !== null, raw: rawMap };
91
+ }
92
+ const SCHEME = /^(?:webpack|rollup|vite|ng|rspack|turbopack|file):\/{0,3}/i;
93
+ /**
94
+ * Tidy `sources` in the uploaded copy, never on disk.
95
+ *
96
+ * Two things come out of a bundler that should not reach a server. `webpack:///./src/App.tsx` is
97
+ * a protocol nothing can open, and it is what the UI would print beside every frame. And an
98
+ * absolute path is the build machine's directory layout — `/home/runner/work/acme/acme/src/…`, or
99
+ * worse, someone's home directory — which is both noise and information the ingest never asked
100
+ * for.
101
+ *
102
+ * The uploaded copy only. Rewriting the file would change bytes another tool may already have
103
+ * hashed, and the map on disk is the one a developer opens locally, where an absolute path is
104
+ * exactly what they want.
105
+ */
106
+ function rewrite(mapJson, root) {
107
+ let parsed;
108
+ try {
109
+ parsed = JSON.parse(mapJson);
110
+ }
111
+ catch {
112
+ return mapJson;
113
+ }
114
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
115
+ return mapJson;
116
+ const map = parsed;
117
+ const sources = map['sources'];
118
+ if (!Array.isArray(sources))
119
+ return mapJson;
120
+ return JSON.stringify({
121
+ ...map,
122
+ sources: sources.map((source) => {
123
+ if (typeof source !== 'string')
124
+ return source;
125
+ let value = source.replace(SCHEME, '');
126
+ if (root !== undefined && root !== '' && (0, node_path_1.isAbsolute)(value)) {
127
+ const inside = (0, node_path_1.relative)(root, value);
128
+ // Only when it is genuinely inside the root: `../../../home/other` is not a tidier path,
129
+ // it is the same path spelled less clearly.
130
+ if (!inside.startsWith('..') && !(0, node_path_1.isAbsolute)(inside))
131
+ value = inside;
132
+ }
133
+ return value.split(node_path_1.sep).join('/').replace(/^\.\//, '');
134
+ }),
135
+ });
136
+ }
137
+ function debugIdOf(body) {
138
+ const parsed = JSON.parse(body);
139
+ return typeof parsed === 'object' && parsed !== null
140
+ ? String(parsed['debugId'] ?? '')
141
+ : '';
142
+ }
143
+ /**
144
+ * Ask the server which of these bodies it already holds, and what it will accept.
145
+ *
146
+ * The dedupe half is purely an optimisation, so every failure mode answers "none": an older ingest
147
+ * with no such route, a proxy in the way, a network blip. Uploading something the server already
148
+ * has is a wasted request; NOT uploading something it does not have is a build with no
149
+ * symbolication, and those are not the same mistake.
150
+ *
151
+ * The limits half matters more than it looks. The protocol says 20 MiB per file and 60 MiB per
152
+ * request, but what a deployment actually accepts is the smaller of that and its PHP
153
+ * `upload_max_filesize` / `post_max_size` — and a POST over `post_max_size` is not rejected, the
154
+ * body is silently dropped and the server answers `missing_release`, which reads like a CLI bug.
155
+ * Asking is the difference between a clear message and an afternoon.
156
+ */
157
+ async function preflight(options, hashes) {
158
+ try {
159
+ const reply = await (0, http_js_1.send)({
160
+ method: 'POST',
161
+ url: `${options.host}/v1/sourcemaps/check`,
162
+ key: options.key,
163
+ headers: { 'Content-Type': 'application/json', ...options.headers },
164
+ body: JSON.stringify({ sha256: hashes }),
165
+ timeoutMs: options.timeoutMs ?? REQUEST_TIMEOUT_MS,
166
+ ...(options.plugin === undefined ? {} : { plugin: options.plugin }),
167
+ ...(options.env === undefined ? {} : { env: options.env }),
168
+ });
169
+ if (reply.status < 200 || reply.status >= 300)
170
+ return { stored: new Set(), limits: null };
171
+ const payload = JSON.parse(reply.text);
172
+ const stored = payload['stored'];
173
+ return {
174
+ stored: Array.isArray(stored) ? new Set(stored.filter((v) => typeof v === 'string')) : new Set(),
175
+ limits: readLimits(payload['limits']),
176
+ };
177
+ }
178
+ catch {
179
+ return { stored: new Set(), limits: null };
180
+ }
181
+ }
182
+ /**
183
+ * Read the server's limits defensively.
184
+ *
185
+ * Every field falls back to the compiled-in protocol value, so a deployment that publishes half
186
+ * the object, or an older one that publishes none of it, still gets a working client rather than
187
+ * a batch sized `NaN`.
188
+ */
189
+ function readLimits(value) {
190
+ if (typeof value !== 'object' || value === null)
191
+ return null;
192
+ const raw = value;
193
+ const number = (name, fallback) => {
194
+ const found = raw[name];
195
+ return typeof found === 'number' && Number.isFinite(found) && found > 0 ? found : fallback;
196
+ };
197
+ const maxFileBytes = number('maxFileBytes', limits_js_1.MAX_FILE_BYTES);
198
+ return {
199
+ maxFileBytes,
200
+ // `maxPartBytes` is the ini-aware one: the same deployment can advertise a 20 MiB protocol
201
+ // ceiling and a 2 MiB `upload_max_filesize`, and only one of those is true of the socket.
202
+ maxPartBytes: number('maxPartBytes', maxFileBytes),
203
+ maxRequestBytes: number('maxRequestBytes', limits_js_1.MAX_REQUEST_BYTES),
204
+ // The server calls it `maxBatchBytes`; the older draft of the protocol called it
205
+ // `recommendedBatchBytes`, and both spellings are read so the deploy order of the two halves
206
+ // is not load-bearing.
207
+ recommendedBatchBytes: number('maxBatchBytes', number('recommendedBatchBytes', limits_js_1.RECOMMENDED_BATCH_BYTES)),
208
+ concurrency: number('concurrency', exports.DEFAULT_CONCURRENCY),
209
+ compression: Array.isArray(raw['compression'])
210
+ ? raw['compression'].filter((entry) => typeof entry === 'string')
211
+ : [],
212
+ };
213
+ }
214
+ /** Four at a time: enough to hide the latency, few enough not to trip the ingest rate limit. */
215
+ exports.DEFAULT_CONCURRENCY = 4;
216
+ /**
217
+ * Send one batch.
218
+ *
219
+ * Batches are bounded by the request ceiling rather than by count: maps vary from kilobytes to
220
+ * megabytes, so a fixed count would either waste requests or overshoot.
221
+ *
222
+ * Each part is gzipped when the server says it accepts that. `Content-Encoding` does NOT survive
223
+ * multipart parsing — PHP hands the handler the raw part and nothing else — so a compressed part
224
+ * is simply a part whose bytes are gzip, which the server detects by its magic number. Source maps
225
+ * are JSON with long runs of repeated source text and compress by 80–90%, which is the difference
226
+ * between one request and six.
227
+ */
228
+ async function send(options, batch, limits = null) {
229
+ const compress = limits?.compression.includes('gzip') === true;
230
+ const parts = [
231
+ { name: 'release', value: options.release },
232
+ ...(options.dist === undefined || options.dist === '' ? [] : [{ name: 'dist', value: options.dist }]),
233
+ ];
234
+ for (const { entry, body } of batch) {
235
+ parts.push({
236
+ name: 'files[]',
237
+ value: compress ? (0, node_zlib_1.gzipSync)(Buffer.from(body, 'utf8'), { level: 6 }) : body,
238
+ filename: entry.name,
239
+ contentType: 'application/json',
240
+ });
241
+ parts.push({ name: 'urls[]', value: entry.url });
242
+ parts.push({ name: 'debug_ids[]', value: entry.debugId });
243
+ }
244
+ const { body, contentType } = (0, multipart_js_1.multipart)(parts);
245
+ const attempts = Math.max(1, options.maxRetries ?? MAX_ATTEMPTS);
246
+ const isDefaultHost = options.host === limits_js_1.DEFAULT_HOST;
247
+ // Bounded and retried. A deploy step that hangs on a half-open socket, or fails its whole run
248
+ // on one 502 from a proxy in front of ingest, is worse than a build without symbolication;
249
+ // the response table says these are transient, so they get the protocol's retry, with backoff.
250
+ for (let attempt = 0;; attempt += 1) {
251
+ let reply;
252
+ try {
253
+ reply = await (0, http_js_1.send)({
254
+ method: 'POST',
255
+ url: `${options.host}/v1/sourcemaps`,
256
+ key: options.key,
257
+ headers: { 'Content-Type': contentType, ...options.headers },
258
+ body,
259
+ timeoutMs: (0, http_js_1.timeoutFor)(body.length, options.timeoutMs ?? REQUEST_TIMEOUT_MS),
260
+ ...(options.plugin === undefined ? {} : { plugin: options.plugin }),
261
+ ...(options.env === undefined ? {} : { env: options.env }),
262
+ });
263
+ }
264
+ catch (error) {
265
+ if (attempt < attempts - 1 && (0, http_js_1.retriable)(error, isDefaultHost)) {
266
+ await sleep((0, http_js_1.backoffMs)(attempt));
267
+ continue;
268
+ }
269
+ throw new UploadError(`Could not reach ${options.host}${attempt > 0 ? ` after ${attempt + 1} attempts` : ''}: ${error instanceof Error ? error.message : String(error)}`, 0, 'network', 'Check the host and that the machine running this can reach it.');
270
+ }
271
+ let payload = {};
272
+ try {
273
+ payload = JSON.parse(reply.text);
274
+ }
275
+ catch {
276
+ // A non-JSON body means something in front of the app answered — a proxy, a WAF, a 502 page.
277
+ }
278
+ if (reply.status === 201) {
279
+ return {
280
+ stored: Number(payload['stored'] ?? batch.length),
281
+ artifacts: Array.isArray(payload['artifacts']) ? payload['artifacts'] : [],
282
+ };
283
+ }
284
+ if (http_js_1.RETRIABLE_STATUS.has(reply.status) && attempt < attempts - 1) {
285
+ await sleep((0, http_js_1.backoffMs)(attempt, reply.header('retry-after')));
286
+ continue;
287
+ }
288
+ throw describe(reply.status, payload, reply.text);
289
+ }
290
+ }
291
+ /** Three tries: one for the blip, one for the restart, then the truth. */
292
+ const MAX_ATTEMPTS = 3;
293
+ const REQUEST_TIMEOUT_MS = 30_000;
294
+ function sleep(ms) {
295
+ return new Promise((resolve) => setTimeout(resolve, ms));
296
+ }
297
+ /**
298
+ * Turn a rejection into something actionable.
299
+ *
300
+ * Every one of these is a mistake someone will make, and the raw code alone tells them nothing.
301
+ * `cli_scope_required` in particular looks like a bad key when it is really the right key with the
302
+ * wrong scope, and without the hint that is a long afternoon.
303
+ */
304
+ function describe(status, payload, raw) {
305
+ const code = String(payload['error'] ?? payload['message'] ?? '') || `http_${status}`;
306
+ switch (code) {
307
+ case 'cli_scope_required':
308
+ return new UploadError('That key cannot upload source maps.', status, code, `Source-map upload needs a key with the "${limits_js_1.REQUIRED_SCOPE}" scope. A write key is not enough — create one in project settings.`);
309
+ case 'missing_api_key':
310
+ case 'invalid_api_key':
311
+ return new UploadError('The write key was rejected.', status, code, 'Check --key, or the VINKTAR_CLI_KEY environment variable.');
312
+ case 'missing_release':
313
+ return new UploadError('A release is required.', status, code,
314
+ // The second sentence is the one that saves the afternoon: an oversized POST is not
315
+ // rejected by PHP, the body is dropped, and the server then honestly reports no release.
316
+ 'Pass --release with the same value your SDK reports. If you did pass one, the request was over the server\'s post_max_size and its body was discarded before PHP saw it — lower the batch size or raise post_max_size.');
317
+ case 'invalid_release':
318
+ case 'invalid_dist':
319
+ return new UploadError(`The ${code === 'invalid_release' ? 'release' : 'dist'} name was rejected.`, status, code, 'It must be at most 64 bytes and contain no slashes, control characters, or leading or trailing whitespace.');
320
+ case 'missing_url_or_debug_id':
321
+ return new UploadError('A file had neither a URL nor a debug id, so the whole upload was rejected.', status, code, 'This is a bug in the CLI — please report it with the command you ran.');
322
+ case 'quota_exceeded': {
323
+ const quota = Number(payload['quota_bytes'] ?? 0);
324
+ const used = Number(payload['used_bytes'] ?? 0);
325
+ return new UploadError(`Source-map storage is full: ${mb(used)} of ${mb(quota)} used.`, status, code, 'Delete old releases from project settings, or upgrade the plan for more storage.');
326
+ }
327
+ case 'file_too_large':
328
+ return new UploadError(`A map is over the ${mb(Number(payload['max_bytes'] ?? limits_js_1.MAX_FILE_BYTES))} per-file limit.`, status, code, 'Split the bundle, or exclude that chunk with --ignore.');
329
+ case 'payload_too_large':
330
+ return new UploadError(`The request was too large${payload['max_bytes'] === undefined ? '' : ` (the server accepts ${mb(Number(payload['max_bytes']))})`}.`, status, code, 'Lower --concurrency, or raise the server\'s post_max_size.');
331
+ default:
332
+ return new UploadError(`Ingest rejected the upload (HTTP ${status}).`, status, code, raw.slice(0, 200) || undefined);
333
+ }
334
+ }
335
+ /**
336
+ * Group indexed maps into requests that fit.
337
+ *
338
+ * The budget is approached with headroom, because multipart framing and the field names add bytes
339
+ * the file sizes do not account for, and an oversized request costs the whole batch. Sized on the
340
+ * UNCOMPRESSED bodies even when the parts will be gzipped: compression only ever makes the request
341
+ * smaller, so the bound stays true and one batch's worth of maps is still what sits in memory.
342
+ *
343
+ * Generic over whatever the caller is carrying alongside the entry, so a batch can hold the
344
+ * artifact each map came from and the body can be read back from disk as its batch is sent —
345
+ * nothing here pins the build in memory.
346
+ */
347
+ function batch(items, limits = null) {
348
+ // Without a word from the server this stays deliberately small — see CONSERVATIVE_BATCH_BYTES.
349
+ const budget = limits === null
350
+ ? limits_js_1.CONSERVATIVE_BATCH_BYTES
351
+ : Math.max(1, Math.min(limits.recommendedBatchBytes, Math.floor(limits.maxRequestBytes * 0.9)));
352
+ const framing = (0, multipart_js_1.overheadFor)({ name: 'files[]', value: '', filename: 'x'.repeat(64), contentType: 'application/json' });
353
+ const batches = [];
354
+ let current = [];
355
+ let size = 0;
356
+ for (const item of items) {
357
+ const cost = item.entry.bytes + framing;
358
+ if (current.length > 0 && size + cost > budget) {
359
+ batches.push(current);
360
+ current = [];
361
+ size = 0;
362
+ }
363
+ current.push(item);
364
+ size += cost;
365
+ }
366
+ if (current.length > 0)
367
+ batches.push(current);
368
+ return batches;
369
+ }
370
+ /**
371
+ * Run `work` over `items` with a bounded number in flight.
372
+ *
373
+ * Source maps are large and the server is usually far away, so a build of two hundred chunks spent
374
+ * most of its upload waiting on a socket. Bounded rather than unbounded: an unbounded fan-out of
375
+ * 6 MB requests is how a deploy step gets itself rate-limited.
376
+ */
377
+ async function pooled(items, limit, work) {
378
+ const results = new Array(items.length);
379
+ let next = 0;
380
+ const runner = async () => {
381
+ for (;;) {
382
+ const i = next;
383
+ next += 1;
384
+ if (i >= items.length)
385
+ return;
386
+ results[i] = await work(items[i], i);
387
+ }
388
+ };
389
+ await Promise.all(Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, runner));
390
+ return results;
391
+ }
392
+ function mb(bytes) {
393
+ return `${(bytes / 1_048_576).toFixed(1)} MB`;
394
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Derives the `urls[]` value the server matches a frame against.
3
+ *
4
+ * `urls[]` is not optional in practice, and this is the one part of the contract most likely to be
5
+ * got wrong: `docs/API.md` omits the field entirely, but if a file's resolved debug id is empty
6
+ * AND its normalised url is empty, the server answers 400 and rolls back the WHOLE upload — not
7
+ * just that file.
8
+ *
9
+ * The server normalises what it stores by stripping the query, the fragment, and then the scheme
10
+ * and host, leaving a path. It applies the identical normalisation to the `file` on a runtime
11
+ * frame before comparing. So `https://cdn.example.com/assets/app.js?v=2` and `/assets/app.js`
12
+ * match, and a prefix that is a full origin is just as correct as one that is a bare path.
13
+ */
14
+ /**
15
+ * @param prefix what the file is served under: `/assets/`, `https://cdn.example.com/`, or `~/`
16
+ * @param relative the file's path relative to the upload root
17
+ */
18
+ export declare function toUrl(prefix: string, relative: string): string;
19
+ /**
20
+ * The server's own normalisation, reproduced so `--dry-run` can show what will actually be stored.
21
+ *
22
+ * Printing this is the difference between "the upload succeeded but nothing symbolicates" and a
23
+ * mismatch someone can see before spending a minute uploading.
24
+ */
25
+ export declare function normalise(url: string): string;
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ /**
3
+ * Derives the `urls[]` value the server matches a frame against.
4
+ *
5
+ * `urls[]` is not optional in practice, and this is the one part of the contract most likely to be
6
+ * got wrong: `docs/API.md` omits the field entirely, but if a file's resolved debug id is empty
7
+ * AND its normalised url is empty, the server answers 400 and rolls back the WHOLE upload — not
8
+ * just that file.
9
+ *
10
+ * The server normalises what it stores by stripping the query, the fragment, and then the scheme
11
+ * and host, leaving a path. It applies the identical normalisation to the `file` on a runtime
12
+ * frame before comparing. So `https://cdn.example.com/assets/app.js?v=2` and `/assets/app.js`
13
+ * match, and a prefix that is a full origin is just as correct as one that is a bare path.
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.toUrl = toUrl;
17
+ exports.normalise = normalise;
18
+ /**
19
+ * @param prefix what the file is served under: `/assets/`, `https://cdn.example.com/`, or `~/`
20
+ * @param relative the file's path relative to the upload root
21
+ */
22
+ function toUrl(prefix, relative) {
23
+ const left = prefix.endsWith('/') ? prefix : `${prefix}/`;
24
+ const right = relative.startsWith('/') ? relative.slice(1) : relative;
25
+ return `${left}${right}`;
26
+ }
27
+ /**
28
+ * The server's own normalisation, reproduced so `--dry-run` can show what will actually be stored.
29
+ *
30
+ * Printing this is the difference between "the upload succeeded but nothing symbolicates" and a
31
+ * mismatch someone can see before spending a minute uploading.
32
+ */
33
+ function normalise(url) {
34
+ let value = url.split('#')[0] ?? '';
35
+ value = value.split('?')[0] ?? '';
36
+ const scheme = value.match(/^[a-z][a-z0-9+.-]*:\/\/[^/]*/i);
37
+ if (scheme)
38
+ value = value.slice(scheme[0].length);
39
+ // `~/` is the conventional "wherever this is served from" prefix; it is not part of the path.
40
+ if (value.startsWith('~'))
41
+ value = value.slice(1);
42
+ return value.startsWith('/') ? value : `/${value}`;
43
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The published version, kept by hand.
3
+ *
4
+ * Its own module so the HTTP layer can put it in a `User-Agent` without importing the CLI's
5
+ * argument parser, which would make every plugin drag the command surface into a build.
6
+ *
7
+ * A test asserts it matches `package.json`, and `prepublishOnly` runs that test — reading the
8
+ * manifest at runtime instead would mean resolving a path that moves between `src` and `dist`.
9
+ */
10
+ export declare const VERSION = "0.1.0";
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.VERSION = void 0;
4
+ /**
5
+ * The published version, kept by hand.
6
+ *
7
+ * Its own module so the HTTP layer can put it in a `User-Agent` without importing the CLI's
8
+ * argument parser, which would make every plugin drag the command surface into a build.
9
+ *
10
+ * A test asserts it matches `package.json`, and `prepublishOnly` runs that test — reading the
11
+ * manifest at runtime instead would mean resolving a path that moves between `src` and `dist`.
12
+ */
13
+ exports.VERSION = '0.1.0';
package/dist/cli.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ import { VERSION } from './version.js';
2
+ export interface Args {
3
+ readonly positional: string[];
4
+ readonly flags: Map<string, string | boolean>;
5
+ /** Values of repeatable flags, in order. `--ignore` and `--header` are the two. */
6
+ readonly repeated: Map<string, string[]>;
7
+ }
8
+ export declare function parse(argv: readonly string[]): Args;
9
+ /**
10
+ * Exit codes: 0 fine, 1 the thing did not work, 2 it worked and something is wrong anyway.
11
+ *
12
+ * Two rather than one for `--strict`, so a pipeline can tell "the upload failed" from "the upload
13
+ * succeeded and half your chunks have no maps" — which are different problems with different
14
+ * owners, and merging them means neither gets fixed.
15
+ */
16
+ export declare function run(argv: readonly string[], log?: (message?: any, ...optionalParams: any[]) => void, fail?: (message?: any, ...optionalParams: any[]) => void): Promise<number>;
17
+ export { VERSION };