@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.
- package/CHANGELOG.md +61 -0
- package/LICENSE +21 -0
- package/README.md +378 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +28 -0
- package/dist/bin.js.map +1 -0
- package/dist/bundler/core.d.ts +140 -0
- package/dist/bundler/core.js +320 -0
- package/dist/bundler/core.js.map +1 -0
- package/dist/bundler/esbuild.d.ts +44 -0
- package/dist/bundler/esbuild.js +85 -0
- package/dist/bundler/esbuild.js.map +1 -0
- package/dist/bundler/rollup.d.ts +84 -0
- package/dist/bundler/rollup.js +221 -0
- package/dist/bundler/rollup.js.map +1 -0
- package/dist/bundler/rspack.d.ts +8 -0
- package/dist/bundler/rspack.js +8 -0
- package/dist/bundler/rspack.js.map +1 -0
- package/dist/bundler/webpack.d.ts +83 -0
- package/dist/bundler/webpack.js +104 -0
- package/dist/bundler/webpack.js.map +1 -0
- package/dist/cjs/bundler/core.d.ts +140 -0
- package/dist/cjs/bundler/core.js +331 -0
- package/dist/cjs/bundler/esbuild.d.ts +44 -0
- package/dist/cjs/bundler/esbuild.js +87 -0
- package/dist/cjs/bundler/rollup.d.ts +84 -0
- package/dist/cjs/bundler/rollup.js +226 -0
- package/dist/cjs/bundler/rspack.d.ts +8 -0
- package/dist/cjs/bundler/rspack.js +12 -0
- package/dist/cjs/bundler/webpack.d.ts +83 -0
- package/dist/cjs/bundler/webpack.js +107 -0
- package/dist/cjs/cli.d.ts +17 -0
- package/dist/cjs/cli.js +358 -0
- package/dist/cjs/commands/doctor.d.ts +15 -0
- package/dist/cjs/commands/doctor.js +134 -0
- package/dist/cjs/commands/inject.d.ts +70 -0
- package/dist/cjs/commands/inject.js +123 -0
- package/dist/cjs/commands/resolve.d.ts +32 -0
- package/dist/cjs/commands/resolve.js +130 -0
- package/dist/cjs/commands/upload.d.ts +74 -0
- package/dist/cjs/commands/upload.js +254 -0
- package/dist/cjs/config.d.ts +51 -0
- package/dist/cjs/config.js +152 -0
- package/dist/cjs/debug-id.d.ts +176 -0
- package/dist/cjs/debug-id.js +323 -0
- package/dist/cjs/discover.d.ts +50 -0
- package/dist/cjs/discover.js +235 -0
- package/dist/cjs/env.d.ts +22 -0
- package/dist/cjs/env.js +52 -0
- package/dist/cjs/glob.d.ts +34 -0
- package/dist/cjs/glob.js +112 -0
- package/dist/cjs/http.d.ts +68 -0
- package/dist/cjs/http.js +211 -0
- package/dist/cjs/index.d.ts +17 -0
- package/dist/cjs/index.js +53 -0
- package/dist/cjs/limits.d.ts +35 -0
- package/dist/cjs/limits.js +38 -0
- package/dist/cjs/multipart.d.ts +28 -0
- package/dist/cjs/multipart.js +44 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/upload.d.ts +178 -0
- package/dist/cjs/upload.js +394 -0
- package/dist/cjs/url.d.ts +25 -0
- package/dist/cjs/url.js +43 -0
- package/dist/cjs/version.d.ts +10 -0
- package/dist/cjs/version.js +13 -0
- package/dist/cli.d.ts +17 -0
- package/dist/cli.js +354 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/doctor.d.ts +15 -0
- package/dist/commands/doctor.js +132 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/inject.d.ts +70 -0
- package/dist/commands/inject.js +120 -0
- package/dist/commands/inject.js.map +1 -0
- package/dist/commands/resolve.d.ts +32 -0
- package/dist/commands/resolve.js +128 -0
- package/dist/commands/resolve.js.map +1 -0
- package/dist/commands/upload.d.ts +74 -0
- package/dist/commands/upload.js +252 -0
- package/dist/commands/upload.js.map +1 -0
- package/dist/config.d.ts +51 -0
- package/dist/config.js +150 -0
- package/dist/config.js.map +1 -0
- package/dist/debug-id.d.ts +176 -0
- package/dist/debug-id.js +307 -0
- package/dist/debug-id.js.map +1 -0
- package/dist/discover.d.ts +50 -0
- package/dist/discover.js +229 -0
- package/dist/discover.js.map +1 -0
- package/dist/env.d.ts +22 -0
- package/dist/env.js +50 -0
- package/dist/env.js.map +1 -0
- package/dist/glob.d.ts +34 -0
- package/dist/glob.js +109 -0
- package/dist/glob.js.map +1 -0
- package/dist/http.d.ts +68 -0
- package/dist/http.js +203 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/limits.d.ts +35 -0
- package/dist/limits.js +36 -0
- package/dist/limits.js.map +1 -0
- package/dist/multipart.d.ts +28 -0
- package/dist/multipart.js +41 -0
- package/dist/multipart.js.map +1 -0
- package/dist/upload.d.ts +178 -0
- package/dist/upload.js +383 -0
- package/dist/upload.js.map +1 -0
- package/dist/url.d.ts +25 -0
- package/dist/url.js +40 -0
- package/dist/url.js.map +1 -0
- package/dist/version.d.ts +10 -0
- package/dist/version.js +11 -0
- package/dist/version.js.map +1 -0
- package/dist/vite.d.ts +74 -0
- package/dist/vite.js +166 -0
- package/dist/vite.js.map +1 -0
- package/package.json +141 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { discover } from '../discover.js';
|
|
2
|
+
import { MAX_FILE_BYTES } from '../limits.js';
|
|
3
|
+
import { batch, DEFAULT_CONCURRENCY, index, materialise, mb, pooled, preflight, send, } from '../upload.js';
|
|
4
|
+
import { normalise } from '../url.js';
|
|
5
|
+
import { audit, inject } from './inject.js';
|
|
6
|
+
/**
|
|
7
|
+
* Upload every map under `root`, injecting debug ids first unless told not to.
|
|
8
|
+
*
|
|
9
|
+
* Three passes, in this order for a reason:
|
|
10
|
+
*
|
|
11
|
+
* 1. **Index.** Every map is built, measured and hashed, then thrown away. A build of two hundred
|
|
12
|
+
* chunks is hundreds of megabytes; keeping all of it resident until the first request went out
|
|
13
|
+
* was the previous shape.
|
|
14
|
+
* 2. **Ask.** One request tells us which bodies the server already has. A redeploy of an unchanged
|
|
15
|
+
* build then sends nothing at all, which is the difference between a source-map quota that
|
|
16
|
+
* lasts and one that fills up in a week.
|
|
17
|
+
* 3. **Send.** Batches, bounded by bytes, several in flight, each map read back from disk as its
|
|
18
|
+
* batch goes.
|
|
19
|
+
*
|
|
20
|
+
* Prints the NORMALISED url alongside each file, because that is the string the server will
|
|
21
|
+
* actually compare a runtime frame against. A mismatch between what you expect to be serving and
|
|
22
|
+
* what will be stored is otherwise invisible until stack traces silently stay minified, and by
|
|
23
|
+
* then the deploy is done and nobody connects the two.
|
|
24
|
+
*
|
|
25
|
+
* A summary line is printed on every exit path, including a thrown one. A build step whose last
|
|
26
|
+
* output is a stack trace tells you it failed and not what it had managed to do first.
|
|
27
|
+
*/
|
|
28
|
+
export async function upload(root, options, log, warn = log) {
|
|
29
|
+
let injected = 0;
|
|
30
|
+
if (options.inject !== false) {
|
|
31
|
+
if (options.dryRun) {
|
|
32
|
+
// A dry run touches nothing, but the id it prints is the one injection would derive, so
|
|
33
|
+
// the output is still what the real run will send.
|
|
34
|
+
const found = await audit(root, options);
|
|
35
|
+
if (found.notInjected > 0)
|
|
36
|
+
log(`Would inject debug ids into ${found.notInjected} chunk(s).`);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
const result = await inject(root, () => { }, options);
|
|
40
|
+
injected = result.injected;
|
|
41
|
+
if (result.injected > 0 || result.repaired > 0) {
|
|
42
|
+
log(`Injected debug ids into ${result.injected} chunk(s)` +
|
|
43
|
+
`${result.adopted > 0 ? `, ${result.adopted} adopted from the bundler` : ''}` +
|
|
44
|
+
`${result.repaired > 0 ? `, ${result.repaired} map(s) repaired` : ''}` +
|
|
45
|
+
`${result.skipped > 0 ? `, ${result.skipped} already stamped` : ''}.`);
|
|
46
|
+
}
|
|
47
|
+
for (const line of result.warnings)
|
|
48
|
+
warn(`WARNING: ${line}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const { artifacts, warnings } = await discover(root, options);
|
|
52
|
+
if (options.inject === false) {
|
|
53
|
+
for (const line of warnings)
|
|
54
|
+
warn(`WARNING: ${line}`);
|
|
55
|
+
}
|
|
56
|
+
const indexed = [];
|
|
57
|
+
const oversized = [];
|
|
58
|
+
const seenIds = new Map();
|
|
59
|
+
const seenMaps = new Set();
|
|
60
|
+
let skipped = 0;
|
|
61
|
+
let blank = 0;
|
|
62
|
+
let duplicates = 0;
|
|
63
|
+
for (const artifact of artifacts) {
|
|
64
|
+
// A map two chunks both point at belongs to neither of them, and `discover` has already
|
|
65
|
+
// refused it for both. This only catches the same map reached twice.
|
|
66
|
+
if (artifact.map !== null && seenMaps.has(artifact.map))
|
|
67
|
+
continue;
|
|
68
|
+
const found = await index(artifact, options.urlPrefix, options);
|
|
69
|
+
if (found === null) {
|
|
70
|
+
skipped += 1;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (found.oversized) {
|
|
74
|
+
oversized.push(found.entry);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (found.empty) {
|
|
78
|
+
blank += 1;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
// Byte-identical chunks emitted under two names derive the same id, and the server keeps one
|
|
82
|
+
// artifact per debug id: sending both means the second silently replaces the first, so the
|
|
83
|
+
// url stored for that id is whichever request happened to land last.
|
|
84
|
+
const already = seenIds.get(found.entry.debugId);
|
|
85
|
+
if (already !== undefined) {
|
|
86
|
+
duplicates += 1;
|
|
87
|
+
warn(`WARNING: ${found.entry.name} has the same debug id as ${already}; only the first is uploaded. ` +
|
|
88
|
+
'The two chunks are byte-identical, so frames from either resolve through the same map.');
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
seenIds.set(found.entry.debugId, found.entry.name);
|
|
92
|
+
if (artifact.map !== null)
|
|
93
|
+
seenMaps.add(artifact.map);
|
|
94
|
+
indexed.push({ artifact, entry: found.entry });
|
|
95
|
+
}
|
|
96
|
+
// Loud, but not fatal. Losing one chunk's symbolication is a bad afternoon; losing the whole
|
|
97
|
+
// build's because one chunk was large is a bad week, and that is what throwing here used to do.
|
|
98
|
+
for (const entry of oversized) {
|
|
99
|
+
warn(`WARNING: ${entry.name} is ${mb(entry.bytes)}, over the ${mb(MAX_FILE_BYTES)} per-file limit — skipped.`);
|
|
100
|
+
}
|
|
101
|
+
const base = { skipped, injected, oversized: oversized.length, empty: blank, duplicates, warnings };
|
|
102
|
+
if (indexed.length === 0) {
|
|
103
|
+
// The single most common way this package does nothing useful, and it used to say nothing at
|
|
104
|
+
// all: a bundler with source maps switched off produces a directory full of chunks and not
|
|
105
|
+
// one map, which looks from here exactly like a directory with nothing in it.
|
|
106
|
+
if (skipped > 0) {
|
|
107
|
+
warn(`WARNING: found ${skipped} JavaScript file(s) under ${root} and no source maps. ` +
|
|
108
|
+
'Nothing can be symbolicated without them — turn source maps on in your bundler ' +
|
|
109
|
+
"(Vite `build.sourcemap`, webpack `devtool: 'source-map'`, esbuild `--sourcemap`).");
|
|
110
|
+
}
|
|
111
|
+
const summary = nothing(base);
|
|
112
|
+
summarise(log, summary);
|
|
113
|
+
return summary;
|
|
114
|
+
}
|
|
115
|
+
const uninjected = indexed.filter((item) => !item.entry.injected);
|
|
116
|
+
for (const { entry } of indexed) {
|
|
117
|
+
log(` ${entry.name}`);
|
|
118
|
+
log(` served as ${normalise(entry.url)}`);
|
|
119
|
+
log(` debug id ${entry.debugId}${entry.injected ? '' : ' (not in the chunk)'}`);
|
|
120
|
+
}
|
|
121
|
+
// Loud on purpose. With `--no-inject` this is the one thing that decides whether these maps
|
|
122
|
+
// will resolve frames from a CDN-rewritten path, and it is easy to get here by running the
|
|
123
|
+
// upload on a machine that never ran `inject`.
|
|
124
|
+
if (uninjected.length > 0 && !options.dryRun) {
|
|
125
|
+
warn('');
|
|
126
|
+
for (const { entry } of uninjected) {
|
|
127
|
+
warn(`WARNING: ${entry.name.replace(/\.map$/, '')} has no debug id. Run without --no-inject, or run "sourcemaps inject" on the build first.`);
|
|
128
|
+
}
|
|
129
|
+
warn(`${uninjected.length} chunk(s) have no debug id; frames from them will only match by release + url.`);
|
|
130
|
+
}
|
|
131
|
+
if (options.dryRun) {
|
|
132
|
+
const bytes = indexed.reduce((total, item) => total + item.entry.bytes, 0);
|
|
133
|
+
log('');
|
|
134
|
+
log(`Dry run: ${indexed.length} map(s), ${mb(bytes)}. Nothing was uploaded.`);
|
|
135
|
+
return {
|
|
136
|
+
...base,
|
|
137
|
+
stored: 0,
|
|
138
|
+
uploaded: 0,
|
|
139
|
+
alreadyStored: 0,
|
|
140
|
+
bytes,
|
|
141
|
+
uninjected: uninjected.length,
|
|
142
|
+
storedMaps: [],
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
const stored = (items) => items
|
|
146
|
+
.filter((item) => item.artifact.map !== null)
|
|
147
|
+
.map((item) => ({ map: item.artifact.map, chunk: item.artifact.file, sha256: item.entry.mapSha256 }));
|
|
148
|
+
const { stored: already, limits } = await preflight(options, indexed.map((item) => item.entry.sha256));
|
|
149
|
+
// The server's own ceiling, which can be smaller than the protocol's: the same deployment
|
|
150
|
+
// advertises a 20 MiB limit and a 2 MiB `upload_max_filesize`, and only one of those is true of
|
|
151
|
+
// the socket. A part over it is dropped in silence, so it is worth saying here instead.
|
|
152
|
+
const overPart = limits === null ? [] : indexed.filter((item) => item.entry.bytes > limits.maxPartBytes);
|
|
153
|
+
for (const item of overPart) {
|
|
154
|
+
warn(`WARNING: ${item.entry.name} is ${mb(item.entry.bytes)} and this server accepts ${mb(limits.maxPartBytes)} per file — skipped.`);
|
|
155
|
+
}
|
|
156
|
+
const accepted = overPart.length === 0 ? indexed : indexed.filter((item) => !overPart.includes(item));
|
|
157
|
+
const pending = accepted.filter((item) => !already.has(item.entry.sha256));
|
|
158
|
+
const alreadyStored = accepted.length - pending.length;
|
|
159
|
+
if (alreadyStored > 0) {
|
|
160
|
+
log(`${alreadyStored} map(s) are already on the server and were not sent again.`);
|
|
161
|
+
}
|
|
162
|
+
if (pending.length === 0) {
|
|
163
|
+
const summary = {
|
|
164
|
+
...base,
|
|
165
|
+
oversized: oversized.length + overPart.length,
|
|
166
|
+
stored: 0,
|
|
167
|
+
uploaded: 0,
|
|
168
|
+
alreadyStored,
|
|
169
|
+
bytes: 0,
|
|
170
|
+
uninjected: uninjected.length,
|
|
171
|
+
storedMaps: stored(accepted),
|
|
172
|
+
};
|
|
173
|
+
summarise(log, summary);
|
|
174
|
+
return summary;
|
|
175
|
+
}
|
|
176
|
+
const groups = batch(pending, limits);
|
|
177
|
+
const bytes = pending.reduce((total, item) => total + item.entry.bytes, 0);
|
|
178
|
+
const sent = [];
|
|
179
|
+
let done = 0;
|
|
180
|
+
let count = 0;
|
|
181
|
+
try {
|
|
182
|
+
const results = await pooled(groups, options.concurrency ?? limits?.concurrency ?? DEFAULT_CONCURRENCY, async (group) => {
|
|
183
|
+
// Bodies are read here, not held from the index pass: one batch's worth of maps is in memory
|
|
184
|
+
// at a time, whatever the size of the build.
|
|
185
|
+
const loaded = await Promise.all(group.map(async (item) => ({ entry: item.entry, body: await materialise(item.artifact, options) })));
|
|
186
|
+
const result = await send(options, loaded, limits);
|
|
187
|
+
sent.push(...group);
|
|
188
|
+
done += 1;
|
|
189
|
+
if (groups.length > 1)
|
|
190
|
+
log(`Uploaded ${done} of ${groups.length} batches.`);
|
|
191
|
+
return result;
|
|
192
|
+
});
|
|
193
|
+
count = results.reduce((total, result) => total + result.stored, 0);
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
// Whatever did land is still on the server, and the caller may still delete those maps. The
|
|
197
|
+
// summary says how far it got before saying why it stopped.
|
|
198
|
+
summarise(log, {
|
|
199
|
+
...base,
|
|
200
|
+
oversized: oversized.length + overPart.length,
|
|
201
|
+
stored: count,
|
|
202
|
+
uploaded: sent.length,
|
|
203
|
+
alreadyStored,
|
|
204
|
+
bytes,
|
|
205
|
+
uninjected: uninjected.length,
|
|
206
|
+
storedMaps: stored([...accepted.filter((item) => already.has(item.entry.sha256)), ...sent]),
|
|
207
|
+
});
|
|
208
|
+
throw error;
|
|
209
|
+
}
|
|
210
|
+
const summary = {
|
|
211
|
+
...base,
|
|
212
|
+
oversized: oversized.length + overPart.length,
|
|
213
|
+
stored: count,
|
|
214
|
+
uploaded: pending.length,
|
|
215
|
+
alreadyStored,
|
|
216
|
+
bytes,
|
|
217
|
+
uninjected: uninjected.length,
|
|
218
|
+
storedMaps: stored(accepted),
|
|
219
|
+
};
|
|
220
|
+
summarise(log, summary);
|
|
221
|
+
return summary;
|
|
222
|
+
}
|
|
223
|
+
/** One line, always, so the last thing printed says what happened rather than what went wrong. */
|
|
224
|
+
function summarise(log, summary) {
|
|
225
|
+
const parts = [`${summary.uploaded} uploaded`];
|
|
226
|
+
if (summary.uploaded > 0 && summary.stored < summary.uploaded)
|
|
227
|
+
parts.push(`${summary.stored} new`);
|
|
228
|
+
if (summary.alreadyStored > 0)
|
|
229
|
+
parts.push(`${summary.alreadyStored} already stored`);
|
|
230
|
+
if (summary.skipped > 0)
|
|
231
|
+
parts.push(`${summary.skipped} without maps`);
|
|
232
|
+
if (summary.empty > 0)
|
|
233
|
+
parts.push(`${summary.empty} empty`);
|
|
234
|
+
if (summary.duplicates > 0)
|
|
235
|
+
parts.push(`${summary.duplicates} duplicate ids`);
|
|
236
|
+
if (summary.oversized > 0)
|
|
237
|
+
parts.push(`${summary.oversized} oversized`);
|
|
238
|
+
log('');
|
|
239
|
+
log(`Source maps: ${parts.join(', ')}.`);
|
|
240
|
+
}
|
|
241
|
+
function nothing(partial) {
|
|
242
|
+
return {
|
|
243
|
+
...partial,
|
|
244
|
+
stored: 0,
|
|
245
|
+
uploaded: 0,
|
|
246
|
+
alreadyStored: 0,
|
|
247
|
+
bytes: 0,
|
|
248
|
+
uninjected: 0,
|
|
249
|
+
storedMaps: [],
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
//# sourceMappingURL=upload.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upload.js","sourceRoot":"","sources":["../../src/commands/upload.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAuC,MAAM,gBAAgB,CAAC;AAC/E,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EACL,KAAK,EACL,mBAAmB,EACnB,KAAK,EACL,WAAW,EACX,EAAE,EACF,MAAM,EACN,SAAS,EACT,IAAI,GAIL,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAsD5C;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,CAAC,KAAK,UAAU,MAAM,CAC1B,IAAY,EACZ,OAA6B,EAC7B,GAA2B,EAC3B,OAA+B,GAAG;IAElC,IAAI,QAAQ,GAAG,CAAC,CAAC;IAEjB,IAAI,OAAO,CAAC,MAAM,KAAK,KAAK,EAAE,CAAC;QAC7B,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;YACnB,wFAAwF;YACxF,mDAAmD;YACnD,MAAM,KAAK,GAAG,MAAM,KAAK,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;YACzC,IAAI,KAAK,CAAC,WAAW,GAAG,CAAC;gBAAE,GAAG,CAAC,+BAA+B,KAAK,CAAC,WAAW,YAAY,CAAC,CAAC;QAC/F,CAAC;aAAM,CAAC;YACN,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE,GAAE,CAAC,EAAE,OAAO,CAAC,CAAC;YACrD,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC;YAC3B,IAAI,MAAM,CAAC,QAAQ,GAAG,CAAC,IAAI,MAAM,CAAC,QAAQ,GAAG,CAAC,EAAE,CAAC;gBAC/C,GAAG,CACD,2BAA2B,MAAM,CAAC,QAAQ,WAAW;oBACnD,GAAG,MAAM,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,OAAO,2BAA2B,CAAC,CAAC,CAAC,EAAE,EAAE;oBAC7E,GAAG,MAAM,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,QAAQ,kBAAkB,CAAC,CAAC,CAAC,EAAE,EAAE;oBACtE,GAAG,MAAM,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,OAAO,kBAAkB,CAAC,CAAC,CAAC,EAAE,GAAG,CACxE,CAAC;YACJ,CAAC;YACD,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,QAAQ;gBAAE,IAAI,CAAC,YAAY,IAAI,EAAE,CAAC,CAAC;QAC/D,CAAC;IACH,CAAC;IAED,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IAC9D,IAAI,OAAO,CAAC,MAAM,KAAK,KAAK,EAAE,CAAC;QAC7B,KAAK,MAAM,IAAI,IAAI,QAAQ;YAAE,IAAI,CAAC,YAAY,IAAI,EAAE,CAAC,CAAC;IACxD,CAAC;IAED,MAAM,OAAO,GAAsD,EAAE,CAAC;IACtE,MAAM,SAAS,GAAkB,EAAE,CAAC;IACpC,MAAM,OAAO,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC1C,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAU,CAAC;IACnC,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,IAAI,UAAU,GAAG,CAAC,CAAC;IAEnB,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;QACjC,wFAAwF;QACxF,qEAAqE;QACrE,IAAI,QAAQ,CAAC,GAAG,KAAK,IAAI,IAAI,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC;YAAE,SAAS;QAElE,MAAM,KAAK,GAAG,MAAM,KAAK,CAAC,QAAQ,EAAE,OAAO,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;QAChE,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;YACnB,OAAO,IAAI,CAAC,CAAC;YACb,SAAS;QACX,CAAC;QACD,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC;YACpB,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YAC5B,SAAS;QACX,CAAC;QACD,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC;YAChB,KAAK,IAAI,CAAC,CAAC;YACX,SAAS;QACX,CAAC;QAED,6FAA6F;QAC7F,2FAA2F;QAC3F,qEAAqE;QACrE,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QACjD,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC1B,UAAU,IAAI,CAAC,CAAC;YAChB,IAAI,CACF,YAAY,KAAK,CAAC,KAAK,CAAC,IAAI,6BAA6B,OAAO,gCAAgC;gBAC9F,wFAAwF,CAC3F,CAAC;YACF,SAAS;QACX,CAAC;QAED,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACnD,IAAI,QAAQ,CAAC,GAAG,KAAK,IAAI;YAAE,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;QACtD,OAAO,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC;IACjD,CAAC;IAED,6FAA6F;IAC7F,gGAAgG;IAChG,KAAK,MAAM,KAAK,IAAI,SAAS,EAAE,CAAC;QAC9B,IAAI,CAAC,YAAY,KAAK,CAAC,IAAI,OAAO,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,cAAc,EAAE,CAAC,cAAc,CAAC,4BAA4B,CAAC,CAAC;IACjH,CAAC;IAED,MAAM,IAAI,GAAG,EAAE,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,CAAC,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC;IAEpG,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,6FAA6F;QAC7F,2FAA2F;QAC3F,8EAA8E;QAC9E,IAAI,OAAO,GAAG,CAAC,EAAE,CAAC;YAChB,IAAI,CACF,kBAAkB,OAAO,6BAA6B,IAAI,uBAAuB;gBAC/E,iFAAiF;gBACjF,mFAAmF,CACtF,CAAC;QACJ,CAAC;QACD,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;QAC9B,SAAS,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QAExB,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,MAAM,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAElE,KAAK,MAAM,EAAE,KAAK,EAAE,IAAI,OAAO,EAAE,CAAC;QAChC,GAAG,CAAC,KAAK,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;QACvB,GAAG,CAAC,kBAAkB,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC9C,GAAG,CAAC,kBAAkB,KAAK,CAAC,OAAO,GAAG,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,uBAAuB,EAAE,CAAC,CAAC;IACzF,CAAC;IAED,4FAA4F;IAC5F,2FAA2F;IAC3F,+CAA+C;IAC/C,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;QAC7C,IAAI,CAAC,EAAE,CAAC,CAAC;QACT,KAAK,MAAM,EAAE,KAAK,EAAE,IAAI,UAAU,EAAE,CAAC;YACnC,IAAI,CAAC,YAAY,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,2FAA2F,CAAC,CAAC;QAChJ,CAAC;QACD,IAAI,CAAC,GAAG,UAAU,CAAC,MAAM,gFAAgF,CAAC,CAAC;IAC7G,CAAC;IAED,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;QACnB,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;QAC3E,GAAG,CAAC,EAAE,CAAC,CAAC;QACR,GAAG,CAAC,YAAY,OAAO,CAAC,MAAM,YAAY,EAAE,CAAC,KAAK,CAAC,yBAAyB,CAAC,CAAC;QAE9E,OAAO;YACL,GAAG,IAAI;YACP,MAAM,EAAE,CAAC;YACT,QAAQ,EAAE,CAAC;YACX,aAAa,EAAE,CAAC;YAChB,KAAK;YACL,UAAU,EAAE,UAAU,CAAC,MAAM;YAC7B,UAAU,EAAE,EAAE;SACf,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,CAAC,KAAgE,EAAe,EAAE,CAC/F,KAAK;SACF,MAAM,CAAC,CAAC,IAAI,EAAwE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,KAAK,IAAI,CAAC;SAClH,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC;IAE1G,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,SAAS,CAAC,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;IAEvG,0FAA0F;IAC1F,gGAAgG;IAChG,wFAAwF;IACxF,MAAM,QAAQ,GAAG,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,MAAM,CAAC,YAAY,CAAC,CAAC;IACzG,KAAK,MAAM,IAAI,IAAI,QAAQ,EAAE,CAAC;QAC5B,IAAI,CACF,YAAY,IAAI,CAAC,KAAK,CAAC,IAAI,OAAO,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,4BAA4B,EAAE,CAAC,MAAO,CAAC,YAAY,CAAC,sBAAsB,CACjI,CAAC;IACJ,CAAC;IAED,MAAM,QAAQ,GAAG,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;IACtG,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;IAC3E,MAAM,aAAa,GAAG,QAAQ,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAEvD,IAAI,aAAa,GAAG,CAAC,EAAE,CAAC;QACtB,GAAG,CAAC,GAAG,aAAa,4DAA4D,CAAC,CAAC;IACpF,CAAC;IAED,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,MAAM,OAAO,GAAkB;YAC7B,GAAG,IAAI;YACP,SAAS,EAAE,SAAS,CAAC,MAAM,GAAG,QAAQ,CAAC,MAAM;YAC7C,MAAM,EAAE,CAAC;YACT,QAAQ,EAAE,CAAC;YACX,aAAa;YACb,KAAK,EAAE,CAAC;YACR,UAAU,EAAE,UAAU,CAAC,MAAM;YAC7B,UAAU,EAAE,MAAM,CAAC,QAAQ,CAAC;SAC7B,CAAC;QACF,SAAS,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QAExB,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IACtC,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;IAC3E,MAAM,IAAI,GAAsD,EAAE,CAAC;IACnE,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,IAAI,KAAK,GAAG,CAAC,CAAC;IAEd,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,WAAW,IAAI,MAAM,EAAE,WAAW,IAAI,mBAAmB,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE;YACtH,6FAA6F;YAC7F,6CAA6C;YAC7C,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,GAAG,CAC9B,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC,CACpG,CAAC;YAEF,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;YACnD,IAAI,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,CAAC;YACpB,IAAI,IAAI,CAAC,CAAC;YACV,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC;gBAAE,GAAG,CAAC,YAAY,IAAI,OAAO,MAAM,CAAC,MAAM,WAAW,CAAC,CAAC;YAE5E,OAAO,MAAM,CAAC;QAChB,CAAC,CAAC,CAAC;QAEH,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,CAAC,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;IACtE,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,4FAA4F;QAC5F,4DAA4D;QAC5D,SAAS,CAAC,GAAG,EAAE;YACb,GAAG,IAAI;YACP,SAAS,EAAE,SAAS,CAAC,MAAM,GAAG,QAAQ,CAAC,MAAM;YAC7C,MAAM,EAAE,KAAK;YACb,QAAQ,EAAE,IAAI,CAAC,MAAM;YACrB,aAAa;YACb,KAAK;YACL,UAAU,EAAE,UAAU,CAAC,MAAM;YAC7B,UAAU,EAAE,MAAM,CAAC,CAAC,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,EAAE,GAAG,IAAI,CAAC,CAAC;SAC5F,CAAC,CAAC;QAEH,MAAM,KAAK,CAAC;IACd,CAAC;IAED,MAAM,OAAO,GAAkB;QAC7B,GAAG,IAAI;QACP,SAAS,EAAE,SAAS,CAAC,MAAM,GAAG,QAAQ,CAAC,MAAM;QAC7C,MAAM,EAAE,KAAK;QACb,QAAQ,EAAE,OAAO,CAAC,MAAM;QACxB,aAAa;QACb,KAAK;QACL,UAAU,EAAE,UAAU,CAAC,MAAM;QAC7B,UAAU,EAAE,MAAM,CAAC,QAAQ,CAAC;KAC7B,CAAC;IACF,SAAS,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IAExB,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,kGAAkG;AAClG,SAAS,SAAS,CAAC,GAA2B,EAAE,OAAsB;IACpE,MAAM,KAAK,GAAG,CAAC,GAAG,OAAO,CAAC,QAAQ,WAAW,CAAC,CAAC;IAC/C,IAAI,OAAO,CAAC,QAAQ,GAAG,CAAC,IAAI,OAAO,CAAC,MAAM,GAAG,OAAO,CAAC,QAAQ;QAAE,KAAK,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,MAAM,MAAM,CAAC,CAAC;IACnG,IAAI,OAAO,CAAC,aAAa,GAAG,CAAC;QAAE,KAAK,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,aAAa,iBAAiB,CAAC,CAAC;IACrF,IAAI,OAAO,CAAC,OAAO,GAAG,CAAC;QAAE,KAAK,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,OAAO,eAAe,CAAC,CAAC;IACvE,IAAI,OAAO,CAAC,KAAK,GAAG,CAAC;QAAE,KAAK,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC;IAC5D,IAAI,OAAO,CAAC,UAAU,GAAG,CAAC;QAAE,KAAK,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,UAAU,gBAAgB,CAAC,CAAC;IAC9E,IAAI,OAAO,CAAC,SAAS,GAAG,CAAC;QAAE,KAAK,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,SAAS,YAAY,CAAC,CAAC;IAExE,GAAG,CAAC,EAAE,CAAC,CAAC;IACR,GAAG,CAAC,gBAAgB,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC3C,CAAC;AAED,SAAS,OAAO,CAAC,OAOhB;IACC,OAAO;QACL,GAAG,OAAO;QACV,MAAM,EAAE,CAAC;QACT,QAAQ,EAAE,CAAC;QACX,aAAa,EAAE,CAAC;QAChB,KAAK,EAAE,CAAC;QACR,UAAU,EAAE,CAAC;QACb,UAAU,EAAE,EAAE;KACf,CAAC;AACJ,CAAC","sourcesContent":["import { discover, type Artifact, type DiscoverOptions } from '../discover.js';\nimport { MAX_FILE_BYTES } from '../limits.js';\nimport {\n batch,\n DEFAULT_CONCURRENCY,\n index,\n materialise,\n mb,\n pooled,\n preflight,\n send,\n type Limits,\n type UploadEntry,\n type UploadOptions,\n} from '../upload.js';\nimport { normalise } from '../url.js';\nimport { audit, inject } from './inject.js';\n\n/** One map the server is known to hold after this run, and the bytes it was holding it for. */\nexport interface StoredMap {\n /** Absolute path to the .map file. */\n readonly map: string;\n /** Absolute path to the chunk it belongs to. */\n readonly chunk: string;\n /** sha256 of the map file as it was read. A plugin re-checks this before deleting it. */\n readonly sha256: string;\n}\n\nexport interface UploadSummary {\n /** Newly stored server-side. Zero on a re-run of the same build, which is not a failure. */\n readonly stored: number;\n /** Maps actually sent in this run. */\n readonly uploaded: number;\n /** Files with no `.map` beside them. */\n readonly skipped: number;\n /** Maps the server already held, so they were not sent again. */\n readonly alreadyStored: number;\n /** Maps over the per-file ceiling. Skipped, never fatal. */\n readonly oversized: number;\n /** Chunks whose map resolves nothing at all, so there was no point sending it. */\n readonly empty: number;\n /** Chunks dropped because another chunk in the same build carries the same debug id. */\n readonly duplicates: number;\n /** Bytes actually sent. */\n readonly bytes: number;\n /** Chunks stamped by this run (zero on a re-run, or with `inject: false`). */\n readonly injected: number;\n /** Chunks sent without a debug id in them. Only ever non-zero with `inject: false`. */\n readonly uninjected: number;\n /**\n * The maps that are on the server after this run — the ones just sent plus the ones it already\n * held. What a caller may safely delete from the build output; deleting by a fresh directory\n * sweep instead is how the Vite plugin used to throw away maps it never touched.\n */\n readonly storedMaps: StoredMap[];\n /** Everything discovery skipped, and why. */\n readonly warnings: string[];\n /** What the server said it accepts, when it said anything. */\n readonly limits?: Limits | null;\n}\n\nexport interface UploadCommandOptions extends UploadOptions, DiscoverOptions {\n readonly dryRun: boolean;\n /**\n * Stamp the chunks and maps before uploading. Default true: a map uploaded from an un-injected\n * chunk can only ever match by release + url, and the server cannot link the two after the fact.\n */\n readonly inject?: boolean;\n}\n\n/**\n * Upload every map under `root`, injecting debug ids first unless told not to.\n *\n * Three passes, in this order for a reason:\n *\n * 1. **Index.** Every map is built, measured and hashed, then thrown away. A build of two hundred\n * chunks is hundreds of megabytes; keeping all of it resident until the first request went out\n * was the previous shape.\n * 2. **Ask.** One request tells us which bodies the server already has. A redeploy of an unchanged\n * build then sends nothing at all, which is the difference between a source-map quota that\n * lasts and one that fills up in a week.\n * 3. **Send.** Batches, bounded by bytes, several in flight, each map read back from disk as its\n * batch goes.\n *\n * Prints the NORMALISED url alongside each file, because that is the string the server will\n * actually compare a runtime frame against. A mismatch between what you expect to be serving and\n * what will be stored is otherwise invisible until stack traces silently stay minified, and by\n * then the deploy is done and nobody connects the two.\n *\n * A summary line is printed on every exit path, including a thrown one. A build step whose last\n * output is a stack trace tells you it failed and not what it had managed to do first.\n */\nexport async function upload(\n root: string,\n options: UploadCommandOptions,\n log: (line: string) => void,\n warn: (line: string) => void = log,\n): Promise<UploadSummary> {\n let injected = 0;\n\n if (options.inject !== false) {\n if (options.dryRun) {\n // A dry run touches nothing, but the id it prints is the one injection would derive, so\n // the output is still what the real run will send.\n const found = await audit(root, options);\n if (found.notInjected > 0) log(`Would inject debug ids into ${found.notInjected} chunk(s).`);\n } else {\n const result = await inject(root, () => {}, options);\n injected = result.injected;\n if (result.injected > 0 || result.repaired > 0) {\n log(\n `Injected debug ids into ${result.injected} chunk(s)` +\n `${result.adopted > 0 ? `, ${result.adopted} adopted from the bundler` : ''}` +\n `${result.repaired > 0 ? `, ${result.repaired} map(s) repaired` : ''}` +\n `${result.skipped > 0 ? `, ${result.skipped} already stamped` : ''}.`,\n );\n }\n for (const line of result.warnings) warn(`WARNING: ${line}`);\n }\n }\n\n const { artifacts, warnings } = await discover(root, options);\n if (options.inject === false) {\n for (const line of warnings) warn(`WARNING: ${line}`);\n }\n\n const indexed: Array<{ artifact: Artifact; entry: UploadEntry }> = [];\n const oversized: UploadEntry[] = [];\n const seenIds = new Map<string, string>();\n const seenMaps = new Set<string>();\n let skipped = 0;\n let blank = 0;\n let duplicates = 0;\n\n for (const artifact of artifacts) {\n // A map two chunks both point at belongs to neither of them, and `discover` has already\n // refused it for both. This only catches the same map reached twice.\n if (artifact.map !== null && seenMaps.has(artifact.map)) continue;\n\n const found = await index(artifact, options.urlPrefix, options);\n if (found === null) {\n skipped += 1;\n continue;\n }\n if (found.oversized) {\n oversized.push(found.entry);\n continue;\n }\n if (found.empty) {\n blank += 1;\n continue;\n }\n\n // Byte-identical chunks emitted under two names derive the same id, and the server keeps one\n // artifact per debug id: sending both means the second silently replaces the first, so the\n // url stored for that id is whichever request happened to land last.\n const already = seenIds.get(found.entry.debugId);\n if (already !== undefined) {\n duplicates += 1;\n warn(\n `WARNING: ${found.entry.name} has the same debug id as ${already}; only the first is uploaded. ` +\n 'The two chunks are byte-identical, so frames from either resolve through the same map.',\n );\n continue;\n }\n\n seenIds.set(found.entry.debugId, found.entry.name);\n if (artifact.map !== null) seenMaps.add(artifact.map);\n indexed.push({ artifact, entry: found.entry });\n }\n\n // Loud, but not fatal. Losing one chunk's symbolication is a bad afternoon; losing the whole\n // build's because one chunk was large is a bad week, and that is what throwing here used to do.\n for (const entry of oversized) {\n warn(`WARNING: ${entry.name} is ${mb(entry.bytes)}, over the ${mb(MAX_FILE_BYTES)} per-file limit — skipped.`);\n }\n\n const base = { skipped, injected, oversized: oversized.length, empty: blank, duplicates, warnings };\n\n if (indexed.length === 0) {\n // The single most common way this package does nothing useful, and it used to say nothing at\n // all: a bundler with source maps switched off produces a directory full of chunks and not\n // one map, which looks from here exactly like a directory with nothing in it.\n if (skipped > 0) {\n warn(\n `WARNING: found ${skipped} JavaScript file(s) under ${root} and no source maps. ` +\n 'Nothing can be symbolicated without them — turn source maps on in your bundler ' +\n \"(Vite `build.sourcemap`, webpack `devtool: 'source-map'`, esbuild `--sourcemap`).\",\n );\n }\n const summary = nothing(base);\n summarise(log, summary);\n\n return summary;\n }\n\n const uninjected = indexed.filter((item) => !item.entry.injected);\n\n for (const { entry } of indexed) {\n log(` ${entry.name}`);\n log(` served as ${normalise(entry.url)}`);\n log(` debug id ${entry.debugId}${entry.injected ? '' : ' (not in the chunk)'}`);\n }\n\n // Loud on purpose. With `--no-inject` this is the one thing that decides whether these maps\n // will resolve frames from a CDN-rewritten path, and it is easy to get here by running the\n // upload on a machine that never ran `inject`.\n if (uninjected.length > 0 && !options.dryRun) {\n warn('');\n for (const { entry } of uninjected) {\n warn(`WARNING: ${entry.name.replace(/\\.map$/, '')} has no debug id. Run without --no-inject, or run \"sourcemaps inject\" on the build first.`);\n }\n warn(`${uninjected.length} chunk(s) have no debug id; frames from them will only match by release + url.`);\n }\n\n if (options.dryRun) {\n const bytes = indexed.reduce((total, item) => total + item.entry.bytes, 0);\n log('');\n log(`Dry run: ${indexed.length} map(s), ${mb(bytes)}. Nothing was uploaded.`);\n\n return {\n ...base,\n stored: 0,\n uploaded: 0,\n alreadyStored: 0,\n bytes,\n uninjected: uninjected.length,\n storedMaps: [],\n };\n }\n\n const stored = (items: ReadonlyArray<{ artifact: Artifact; entry: UploadEntry }>): StoredMap[] =>\n items\n .filter((item): item is { artifact: Artifact & { map: string }; entry: UploadEntry } => item.artifact.map !== null)\n .map((item) => ({ map: item.artifact.map, chunk: item.artifact.file, sha256: item.entry.mapSha256 }));\n\n const { stored: already, limits } = await preflight(options, indexed.map((item) => item.entry.sha256));\n\n // The server's own ceiling, which can be smaller than the protocol's: the same deployment\n // advertises a 20 MiB limit and a 2 MiB `upload_max_filesize`, and only one of those is true of\n // the socket. A part over it is dropped in silence, so it is worth saying here instead.\n const overPart = limits === null ? [] : indexed.filter((item) => item.entry.bytes > limits.maxPartBytes);\n for (const item of overPart) {\n warn(\n `WARNING: ${item.entry.name} is ${mb(item.entry.bytes)} and this server accepts ${mb(limits!.maxPartBytes)} per file — skipped.`,\n );\n }\n\n const accepted = overPart.length === 0 ? indexed : indexed.filter((item) => !overPart.includes(item));\n const pending = accepted.filter((item) => !already.has(item.entry.sha256));\n const alreadyStored = accepted.length - pending.length;\n\n if (alreadyStored > 0) {\n log(`${alreadyStored} map(s) are already on the server and were not sent again.`);\n }\n\n if (pending.length === 0) {\n const summary: UploadSummary = {\n ...base,\n oversized: oversized.length + overPart.length,\n stored: 0,\n uploaded: 0,\n alreadyStored,\n bytes: 0,\n uninjected: uninjected.length,\n storedMaps: stored(accepted),\n };\n summarise(log, summary);\n\n return summary;\n }\n\n const groups = batch(pending, limits);\n const bytes = pending.reduce((total, item) => total + item.entry.bytes, 0);\n const sent: Array<{ artifact: Artifact; entry: UploadEntry }> = [];\n let done = 0;\n let count = 0;\n\n try {\n const results = await pooled(groups, options.concurrency ?? limits?.concurrency ?? DEFAULT_CONCURRENCY, async (group) => {\n // Bodies are read here, not held from the index pass: one batch's worth of maps is in memory\n // at a time, whatever the size of the build.\n const loaded = await Promise.all(\n group.map(async (item) => ({ entry: item.entry, body: await materialise(item.artifact, options) })),\n );\n\n const result = await send(options, loaded, limits);\n sent.push(...group);\n done += 1;\n if (groups.length > 1) log(`Uploaded ${done} of ${groups.length} batches.`);\n\n return result;\n });\n\n count = results.reduce((total, result) => total + result.stored, 0);\n } catch (error) {\n // Whatever did land is still on the server, and the caller may still delete those maps. The\n // summary says how far it got before saying why it stopped.\n summarise(log, {\n ...base,\n oversized: oversized.length + overPart.length,\n stored: count,\n uploaded: sent.length,\n alreadyStored,\n bytes,\n uninjected: uninjected.length,\n storedMaps: stored([...accepted.filter((item) => already.has(item.entry.sha256)), ...sent]),\n });\n\n throw error;\n }\n\n const summary: UploadSummary = {\n ...base,\n oversized: oversized.length + overPart.length,\n stored: count,\n uploaded: pending.length,\n alreadyStored,\n bytes,\n uninjected: uninjected.length,\n storedMaps: stored(accepted),\n };\n summarise(log, summary);\n\n return summary;\n}\n\n/** One line, always, so the last thing printed says what happened rather than what went wrong. */\nfunction summarise(log: (line: string) => void, summary: UploadSummary): void {\n const parts = [`${summary.uploaded} uploaded`];\n if (summary.uploaded > 0 && summary.stored < summary.uploaded) parts.push(`${summary.stored} new`);\n if (summary.alreadyStored > 0) parts.push(`${summary.alreadyStored} already stored`);\n if (summary.skipped > 0) parts.push(`${summary.skipped} without maps`);\n if (summary.empty > 0) parts.push(`${summary.empty} empty`);\n if (summary.duplicates > 0) parts.push(`${summary.duplicates} duplicate ids`);\n if (summary.oversized > 0) parts.push(`${summary.oversized} oversized`);\n\n log('');\n log(`Source maps: ${parts.join(', ')}.`);\n}\n\nfunction nothing(partial: {\n skipped: number;\n injected: number;\n oversized: number;\n empty: number;\n duplicates: number;\n warnings: string[];\n}): UploadSummary {\n return {\n ...partial,\n stored: 0,\n uploaded: 0,\n alreadyStored: 0,\n bytes: 0,\n uninjected: 0,\n storedMaps: [],\n };\n}\n"]}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One place that decides what every setting is, and one place that refuses a bad one.
|
|
3
|
+
*
|
|
4
|
+
* ## Precedence
|
|
5
|
+
*
|
|
6
|
+
* flag → environment → `.env` file → compiled-in default, highest first, with the server's
|
|
7
|
+
* published limits filling in the sizes further down the line. It reads in that order for the
|
|
8
|
+
* reason every tool does: the thing you typed last should win, and a file checked into a repo must
|
|
9
|
+
* never quietly outrank a secret the CI supplied.
|
|
10
|
+
*
|
|
11
|
+
* ## Why blank is an error and not a default
|
|
12
|
+
*
|
|
13
|
+
* Every value is trimmed, and a value that is blank AFTER trimming is refused BY NAME rather than
|
|
14
|
+
* silently falling through to the next source. `VINKTAR_CLI_KEY=$(echo "$KEY")` leaves a trailing
|
|
15
|
+
* newline; `--release ""` from a shell variable that was never set leaves an empty string. Both
|
|
16
|
+
* used to be indistinguishable from "not set", so the CLI carried on and uploaded the whole build
|
|
17
|
+
* under the release named "".
|
|
18
|
+
*/
|
|
19
|
+
export interface Sources {
|
|
20
|
+
readonly flags: ReadonlyMap<string, string | boolean>;
|
|
21
|
+
/** Values of repeatable flags, in the order they were given. */
|
|
22
|
+
readonly repeated: ReadonlyMap<string, readonly string[]>;
|
|
23
|
+
readonly env: Record<string, string | undefined>;
|
|
24
|
+
/** Values read from a `.env` file, which rank BELOW the process environment. */
|
|
25
|
+
readonly dotenv?: Record<string, string>;
|
|
26
|
+
}
|
|
27
|
+
export interface Resolved {
|
|
28
|
+
readonly host: string;
|
|
29
|
+
readonly key: string;
|
|
30
|
+
readonly release: string;
|
|
31
|
+
readonly dist: string;
|
|
32
|
+
readonly urlPrefix: string;
|
|
33
|
+
readonly concurrency: number | undefined;
|
|
34
|
+
readonly timeoutMs: number | undefined;
|
|
35
|
+
readonly maxRetries: number | undefined;
|
|
36
|
+
readonly headers: Record<string, string>;
|
|
37
|
+
readonly ignore: string[];
|
|
38
|
+
readonly extensions: string[] | undefined;
|
|
39
|
+
readonly rewriteSources: boolean;
|
|
40
|
+
readonly quiet: boolean;
|
|
41
|
+
readonly debug: boolean;
|
|
42
|
+
readonly strict: boolean;
|
|
43
|
+
readonly allowFailure: boolean;
|
|
44
|
+
readonly dryRun: boolean;
|
|
45
|
+
readonly inject: boolean;
|
|
46
|
+
/** Fatal problems, phrased for someone who is about to fix one. */
|
|
47
|
+
readonly errors: string[];
|
|
48
|
+
/** Worth saying, not worth stopping for. */
|
|
49
|
+
readonly warnings: string[];
|
|
50
|
+
}
|
|
51
|
+
export declare function resolve(sources: Sources): Resolved;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { DEFAULT_HOST, MAX_NAME_BYTES } from './limits.js';
|
|
2
|
+
/** Characters a name may not contain: the control range, plus DEL. */
|
|
3
|
+
const CONTROL = new RegExp('[\\u0000-\\u001f\\u007f]');
|
|
4
|
+
export function resolve(sources) {
|
|
5
|
+
const errors = [];
|
|
6
|
+
const warnings = [];
|
|
7
|
+
const text = (flag, ...names) => {
|
|
8
|
+
const value = sources.flags.get(flag);
|
|
9
|
+
if (typeof value === 'string')
|
|
10
|
+
return check(value, `--${flag}`, errors);
|
|
11
|
+
for (const name of names) {
|
|
12
|
+
const found = sources.env[name] ?? sources.dotenv?.[name];
|
|
13
|
+
if (found !== undefined && found !== '')
|
|
14
|
+
return check(found, `$${name}`, errors);
|
|
15
|
+
}
|
|
16
|
+
return '';
|
|
17
|
+
};
|
|
18
|
+
const switched = (name, ...envNames) => {
|
|
19
|
+
if (sources.flags.get(name) === true)
|
|
20
|
+
return true;
|
|
21
|
+
if (sources.flags.get(`no-${name}`) === true)
|
|
22
|
+
return false;
|
|
23
|
+
return envNames.some((envName) => truthy(sources.env[envName] ?? sources.dotenv?.[envName]));
|
|
24
|
+
};
|
|
25
|
+
const host = (text('host', 'VINKTAR_HOST') || DEFAULT_HOST).replace(/\/+$/, '');
|
|
26
|
+
try {
|
|
27
|
+
const parsed = new URL(host);
|
|
28
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
29
|
+
errors.push(`The host must be http or https, not "${parsed.protocol.replace(':', '')}".`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
errors.push(`"${host}" is not a URL. Pass --host like https://in.vinktar.com.`);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* `VINKTAR_CLI_KEY` first: source-map upload needs the `cli` scope, and a CI that already
|
|
37
|
+
* exports `VINKTAR_KEY` for the browser bundle is exporting a PUBLIC write key — which this
|
|
38
|
+
* endpoint refuses with `cli_scope_required`, a message that reads like a bad key rather than
|
|
39
|
+
* the right key with the wrong scope.
|
|
40
|
+
*/
|
|
41
|
+
const key = text('key', 'VINKTAR_CLI_KEY', 'VINKTAR_KEY');
|
|
42
|
+
if (key.startsWith('vnk_pk_')) {
|
|
43
|
+
warnings.push('That looks like a public write key. Source-map upload needs the "cli" scope; set $VINKTAR_CLI_KEY to a key that has it.');
|
|
44
|
+
}
|
|
45
|
+
const release = text('release', 'VINKTAR_RELEASE');
|
|
46
|
+
nameable(release, 'release', errors);
|
|
47
|
+
const dist = text('dist', 'VINKTAR_DIST');
|
|
48
|
+
nameable(dist, 'dist', errors);
|
|
49
|
+
const ignore = [
|
|
50
|
+
...(sources.repeated.get('ignore') ?? []),
|
|
51
|
+
...split(sources.env['VINKTAR_IGNORE'] ?? sources.dotenv?.['VINKTAR_IGNORE']),
|
|
52
|
+
];
|
|
53
|
+
return {
|
|
54
|
+
host,
|
|
55
|
+
key,
|
|
56
|
+
release,
|
|
57
|
+
dist,
|
|
58
|
+
urlPrefix: text('url-prefix', 'VINKTAR_URL_PREFIX') || '~/',
|
|
59
|
+
concurrency: count(text('concurrency', 'VINKTAR_UPLOAD_CONCURRENCY'), 'concurrency', 1, 32, errors),
|
|
60
|
+
timeoutMs: count(text('timeout', 'VINKTAR_HTTP_TIMEOUT'), 'timeout', 1, 3_600, errors, 1_000),
|
|
61
|
+
maxRetries: count(text('retries', 'VINKTAR_HTTP_MAX_RETRIES'), 'retries', 1, 10, errors),
|
|
62
|
+
headers: headersFrom(sources.repeated.get('header') ?? [], errors),
|
|
63
|
+
ignore,
|
|
64
|
+
extensions: extensionsFrom(text('ext', 'VINKTAR_EXTENSIONS')),
|
|
65
|
+
rewriteSources: sources.flags.get('no-rewrite-sources') !== true,
|
|
66
|
+
quiet: switched('quiet', 'VINKTAR_QUIET'),
|
|
67
|
+
debug: switched('debug', 'VINKTAR_DEBUG') ||
|
|
68
|
+
(sources.env['VINKTAR_LOG_LEVEL'] ?? '').trim().toLowerCase() === 'debug',
|
|
69
|
+
strict: switched('strict', 'VINKTAR_STRICT'),
|
|
70
|
+
allowFailure: switched('allow-failure', 'VINKTAR_ALLOW_FAILURE'),
|
|
71
|
+
dryRun: sources.flags.get('dry-run') === true,
|
|
72
|
+
inject: sources.flags.get('no-inject') !== true,
|
|
73
|
+
errors,
|
|
74
|
+
warnings,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/** Trim, and refuse what is left if there is nothing left. */
|
|
78
|
+
function check(value, source, errors) {
|
|
79
|
+
const trimmed = value.trim();
|
|
80
|
+
if (trimmed === '') {
|
|
81
|
+
errors.push(`${source} is empty. Remove it, or give it a value.`);
|
|
82
|
+
return '';
|
|
83
|
+
}
|
|
84
|
+
return trimmed;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* A release or dist name, validated here so the bytes never leave.
|
|
88
|
+
*
|
|
89
|
+
* The server truncated an over-long release silently for a while, which is the worst of the
|
|
90
|
+
* available options: the upload succeeds, the maps are stored under a name no SDK will ever
|
|
91
|
+
* report, and nothing in the output says so.
|
|
92
|
+
*/
|
|
93
|
+
function nameable(value, field, errors) {
|
|
94
|
+
if (value === '')
|
|
95
|
+
return;
|
|
96
|
+
if (Buffer.byteLength(value) > MAX_NAME_BYTES) {
|
|
97
|
+
errors.push(`The ${field} is longer than ${MAX_NAME_BYTES} bytes; the server will not store it under that name.`);
|
|
98
|
+
}
|
|
99
|
+
if (/[/\\]/.test(value))
|
|
100
|
+
errors.push(`The ${field} may not contain a slash.`);
|
|
101
|
+
if (/\s/.test(value))
|
|
102
|
+
errors.push(`The ${field} may not contain whitespace.`);
|
|
103
|
+
if (CONTROL.test(value))
|
|
104
|
+
errors.push(`The ${field} contains a control character.`);
|
|
105
|
+
if (value === '.' || value === '..')
|
|
106
|
+
errors.push(`"${value}" is not a usable ${field}.`);
|
|
107
|
+
}
|
|
108
|
+
function count(value, option, min, max, errors, scale = 1) {
|
|
109
|
+
if (value === '')
|
|
110
|
+
return undefined;
|
|
111
|
+
const parsed = Number(value);
|
|
112
|
+
if (!Number.isInteger(parsed) || parsed < min || parsed > max) {
|
|
113
|
+
errors.push(`--${option} must be a whole number between ${min} and ${max}, not "${value}".`);
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
return parsed * scale;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* `--header 'Name: value'`, repeatable.
|
|
120
|
+
*
|
|
121
|
+
* The key header is added AFTER these by the HTTP layer, so `--header X-Vinktar-Key: …` cannot
|
|
122
|
+
* displace it: the flag exists for a gateway in front of ingest, not to smuggle a second identity
|
|
123
|
+
* past the one the rest of the CLI validated.
|
|
124
|
+
*/
|
|
125
|
+
function headersFrom(values, errors) {
|
|
126
|
+
const found = {};
|
|
127
|
+
for (const value of values) {
|
|
128
|
+
const colon = value.indexOf(':');
|
|
129
|
+
if (colon <= 0) {
|
|
130
|
+
errors.push(`--header "${value}" is not "Name: value".`);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
found[value.slice(0, colon).trim()] = value.slice(colon + 1).trim();
|
|
134
|
+
}
|
|
135
|
+
return found;
|
|
136
|
+
}
|
|
137
|
+
function extensionsFrom(value) {
|
|
138
|
+
const found = split(value);
|
|
139
|
+
return found.length === 0 ? undefined : found;
|
|
140
|
+
}
|
|
141
|
+
function split(value) {
|
|
142
|
+
return (value ?? '')
|
|
143
|
+
.split(',')
|
|
144
|
+
.map((entry) => entry.trim())
|
|
145
|
+
.filter((entry) => entry !== '');
|
|
146
|
+
}
|
|
147
|
+
function truthy(value) {
|
|
148
|
+
return value !== undefined && ['1', 'true', 'yes', 'on'].includes(value.trim().toLowerCase());
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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","sourcesContent":["import { DEFAULT_HOST, MAX_NAME_BYTES } from './limits.js';\n\n/**\n * One place that decides what every setting is, and one place that refuses a bad one.\n *\n * ## Precedence\n *\n * flag → environment → `.env` file → compiled-in default, highest first, with the server's\n * published limits filling in the sizes further down the line. It reads in that order for the\n * reason every tool does: the thing you typed last should win, and a file checked into a repo must\n * never quietly outrank a secret the CI supplied.\n *\n * ## Why blank is an error and not a default\n *\n * Every value is trimmed, and a value that is blank AFTER trimming is refused BY NAME rather than\n * silently falling through to the next source. `VINKTAR_CLI_KEY=$(echo \"$KEY\")` leaves a trailing\n * newline; `--release \"\"` from a shell variable that was never set leaves an empty string. Both\n * used to be indistinguishable from \"not set\", so the CLI carried on and uploaded the whole build\n * under the release named \"\".\n */\n\nexport interface Sources {\n readonly flags: ReadonlyMap<string, string | boolean>;\n /** Values of repeatable flags, in the order they were given. */\n readonly repeated: ReadonlyMap<string, readonly string[]>;\n readonly env: Record<string, string | undefined>;\n /** Values read from a `.env` file, which rank BELOW the process environment. */\n readonly dotenv?: Record<string, string>;\n}\n\nexport interface Resolved {\n readonly host: string;\n readonly key: string;\n readonly release: string;\n readonly dist: string;\n readonly urlPrefix: string;\n readonly concurrency: number | undefined;\n readonly timeoutMs: number | undefined;\n readonly maxRetries: number | undefined;\n readonly headers: Record<string, string>;\n readonly ignore: string[];\n readonly extensions: string[] | undefined;\n readonly rewriteSources: boolean;\n readonly quiet: boolean;\n readonly debug: boolean;\n readonly strict: boolean;\n readonly allowFailure: boolean;\n readonly dryRun: boolean;\n readonly inject: boolean;\n /** Fatal problems, phrased for someone who is about to fix one. */\n readonly errors: string[];\n /** Worth saying, not worth stopping for. */\n readonly warnings: string[];\n}\n\n/** Characters a name may not contain: the control range, plus DEL. */\nconst CONTROL = new RegExp('[\\\\u0000-\\\\u001f\\\\u007f]');\n\nexport function resolve(sources: Sources): Resolved {\n const errors: string[] = [];\n const warnings: string[] = [];\n\n const text = (flag: string, ...names: string[]): string => {\n const value = sources.flags.get(flag);\n if (typeof value === 'string') return check(value, `--${flag}`, errors);\n\n for (const name of names) {\n const found = sources.env[name] ?? sources.dotenv?.[name];\n if (found !== undefined && found !== '') return check(found, `$${name}`, errors);\n }\n\n return '';\n };\n\n const switched = (name: string, ...envNames: string[]): boolean => {\n if (sources.flags.get(name) === true) return true;\n if (sources.flags.get(`no-${name}`) === true) return false;\n\n return envNames.some((envName) => truthy(sources.env[envName] ?? sources.dotenv?.[envName]));\n };\n\n const host = (text('host', 'VINKTAR_HOST') || DEFAULT_HOST).replace(/\\/+$/, '');\n try {\n const parsed = new URL(host);\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {\n errors.push(`The host must be http or https, not \"${parsed.protocol.replace(':', '')}\".`);\n }\n } catch {\n errors.push(`\"${host}\" is not a URL. Pass --host like https://in.vinktar.com.`);\n }\n\n /**\n * `VINKTAR_CLI_KEY` first: source-map upload needs the `cli` scope, and a CI that already\n * exports `VINKTAR_KEY` for the browser bundle is exporting a PUBLIC write key — which this\n * endpoint refuses with `cli_scope_required`, a message that reads like a bad key rather than\n * the right key with the wrong scope.\n */\n const key = text('key', 'VINKTAR_CLI_KEY', 'VINKTAR_KEY');\n if (key.startsWith('vnk_pk_')) {\n warnings.push(\n 'That looks like a public write key. Source-map upload needs the \"cli\" scope; set $VINKTAR_CLI_KEY to a key that has it.',\n );\n }\n\n const release = text('release', 'VINKTAR_RELEASE');\n nameable(release, 'release', errors);\n\n const dist = text('dist', 'VINKTAR_DIST');\n nameable(dist, 'dist', errors);\n\n const ignore = [\n ...(sources.repeated.get('ignore') ?? []),\n ...split(sources.env['VINKTAR_IGNORE'] ?? sources.dotenv?.['VINKTAR_IGNORE']),\n ];\n\n return {\n host,\n key,\n release,\n dist,\n urlPrefix: text('url-prefix', 'VINKTAR_URL_PREFIX') || '~/',\n concurrency: count(text('concurrency', 'VINKTAR_UPLOAD_CONCURRENCY'), 'concurrency', 1, 32, errors),\n timeoutMs: count(text('timeout', 'VINKTAR_HTTP_TIMEOUT'), 'timeout', 1, 3_600, errors, 1_000),\n maxRetries: count(text('retries', 'VINKTAR_HTTP_MAX_RETRIES'), 'retries', 1, 10, errors),\n headers: headersFrom(sources.repeated.get('header') ?? [], errors),\n ignore,\n extensions: extensionsFrom(text('ext', 'VINKTAR_EXTENSIONS')),\n rewriteSources: sources.flags.get('no-rewrite-sources') !== true,\n quiet: switched('quiet', 'VINKTAR_QUIET'),\n debug:\n switched('debug', 'VINKTAR_DEBUG') ||\n (sources.env['VINKTAR_LOG_LEVEL'] ?? '').trim().toLowerCase() === 'debug',\n strict: switched('strict', 'VINKTAR_STRICT'),\n allowFailure: switched('allow-failure', 'VINKTAR_ALLOW_FAILURE'),\n dryRun: sources.flags.get('dry-run') === true,\n inject: sources.flags.get('no-inject') !== true,\n errors,\n warnings,\n };\n}\n\n/** Trim, and refuse what is left if there is nothing left. */\nfunction check(value: string, source: string, errors: string[]): string {\n const trimmed = value.trim();\n if (trimmed === '') {\n errors.push(`${source} is empty. Remove it, or give it a value.`);\n\n return '';\n }\n\n return trimmed;\n}\n\n/**\n * A release or dist name, validated here so the bytes never leave.\n *\n * The server truncated an over-long release silently for a while, which is the worst of the\n * available options: the upload succeeds, the maps are stored under a name no SDK will ever\n * report, and nothing in the output says so.\n */\nfunction nameable(value: string, field: 'release' | 'dist', errors: string[]): void {\n if (value === '') return;\n\n if (Buffer.byteLength(value) > MAX_NAME_BYTES) {\n errors.push(`The ${field} is longer than ${MAX_NAME_BYTES} bytes; the server will not store it under that name.`);\n }\n if (/[/\\\\]/.test(value)) errors.push(`The ${field} may not contain a slash.`);\n if (/\\s/.test(value)) errors.push(`The ${field} may not contain whitespace.`);\n if (CONTROL.test(value)) errors.push(`The ${field} contains a control character.`);\n if (value === '.' || value === '..') errors.push(`\"${value}\" is not a usable ${field}.`);\n}\n\nfunction count(\n value: string,\n option: string,\n min: number,\n max: number,\n errors: string[],\n scale = 1,\n): number | undefined {\n if (value === '') return undefined;\n\n const parsed = Number(value);\n if (!Number.isInteger(parsed) || parsed < min || parsed > max) {\n errors.push(`--${option} must be a whole number between ${min} and ${max}, not \"${value}\".`);\n\n return undefined;\n }\n\n return parsed * scale;\n}\n\n/**\n * `--header 'Name: value'`, repeatable.\n *\n * The key header is added AFTER these by the HTTP layer, so `--header X-Vinktar-Key: …` cannot\n * displace it: the flag exists for a gateway in front of ingest, not to smuggle a second identity\n * past the one the rest of the CLI validated.\n */\nfunction headersFrom(values: readonly string[], errors: string[]): Record<string, string> {\n const found: Record<string, string> = {};\n\n for (const value of values) {\n const colon = value.indexOf(':');\n if (colon <= 0) {\n errors.push(`--header \"${value}\" is not \"Name: value\".`);\n continue;\n }\n found[value.slice(0, colon).trim()] = value.slice(colon + 1).trim();\n }\n\n return found;\n}\n\nfunction extensionsFrom(value: string): string[] | undefined {\n const found = split(value);\n\n return found.length === 0 ? undefined : found;\n}\n\nfunction split(value: string | undefined): string[] {\n return (value ?? '')\n .split(',')\n .map((entry) => entry.trim())\n .filter((entry) => entry !== '');\n}\n\nfunction truthy(value: string | undefined): boolean {\n return value !== undefined && ['1', 'true', 'yes', 'on'].includes(value.trim().toLowerCase());\n}\n"]}
|