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